# Candidate Exploration System

This module provides an inheritance-based candidate exploration system for discovering and collecting candidates in the Local Deep Research framework.

## Architecture

The system is built around inheritance and provides multiple exploration strategies:

### Base Class
- **`BaseCandidateExplorer`**: Abstract base class defining the exploration interface

### Concrete Implementations
- **`ParallelExplorer`**: Runs multiple searches in parallel for speed and breadth
- **`AdaptiveExplorer`**: Learns which search strategies work best and adapts
- **`ConstraintGuidedExplorer`**: Uses constraints to guide the exploration process
- **`DiversityExplorer`**: Prioritizes finding diverse candidates across categories

### Supporting Components
- **`ExplorationResult`**: Data class containing exploration results and metadata
- **`ExplorationStrategy`**: Enum defining different exploration approaches

## Usage Examples

### Using ParallelExplorer
```python
from candidate_exploration import ParallelExplorer

explorer = ParallelExplorer(
    model=llm,
    search_engine=search,
    max_workers=5,           # Parallel search threads
    queries_per_round=8,     # Queries generated per round
    max_rounds=3             # Maximum exploration rounds
)

result = explorer.explore(
    initial_query="hiking locations",
    constraints=constraints,
    entity_type="location"
)
```

### Using AdaptiveExplorer
```python
from candidate_exploration import AdaptiveExplorer

explorer = AdaptiveExplorer(
    model=llm,
    search_engine=search,
    initial_strategies=["direct_search", "synonym_expansion", "category_exploration"],
    adaptation_threshold=5   # Adapt after 5 searches
)

result = explorer.explore("scenic viewpoints", constraints, "viewpoint")
```

### Using ConstraintGuidedExplorer
```python
from candidate_exploration import ConstraintGuidedExplorer

explorer = ConstraintGuidedExplorer(
    model=llm,
    search_engine=search,
    constraint_weight_threshold=0.7,  # Focus on high-weight constraints
    early_validation=True              # Validate during exploration
)

result = explorer.explore("mountain peaks", constraints, "mountain")
```

### Using DiversityExplorer
```python
from candidate_exploration import DiversityExplorer

explorer = DiversityExplorer(
    model=llm,
    search_engine=search,
    diversity_threshold=0.7,    # Minimum diversity score
    category_limit=10,          # Max per category
    similarity_threshold=0.8    # Similarity threshold
)

result = explorer.explore("natural landmarks", constraints, "landmark")
```

## Creating Custom Variants

To create your own exploration strategy:

1. **Inherit from BaseCandidateExplorer**:
```python
from .base_explorer import BaseCandidateExplorer, ExplorationResult

class MyCustomExplorer(BaseCandidateExplorer):
    def __init__(self, *args, my_param=0.5, **kwargs):
        super().__init__(*args, **kwargs)
        self.my_param = my_param
```

2. **Implement required methods**:
```python
    def explore(self, initial_query, constraints=None, entity_type=None):
        # Your exploration implementation
        return ExplorationResult(...)

    def generate_exploration_queries(self, base_query, found_candidates, constraints=None):
        # Your query generation logic
        return ["query1", "query2", "query3"]
```

3. **Add custom exploration logic**:
```python
    def _my_custom_search_strategy(self, query, context):
        # Your custom search approach
        pass
```

## Integration with Strategies

Use in your strategy by initializing the explorer:

```python
class MyStrategy(BaseStrategy):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Choose your explorer
        self.explorer = AdaptiveExplorer(
            model=self.model,
            search_engine=self.search,
            max_candidates=50,
            max_search_time=120.0
        )

    def find_candidates(self, query, constraints):
        result = self.explorer.explore(
            initial_query=query,
            constraints=constraints,
            entity_type=self._detect_entity_type(query)
        )

        return result.candidates
```

## Available Explorers

### ParallelExplorer
- **Best for**: Fast, broad candidate discovery
- **Strategy**: Breadth-first parallel search
- **Parameters**: `max_workers`, `queries_per_round`, `max_rounds`
- **Output**: Many candidates found quickly

### AdaptiveExplorer
- **Best for**: Learning optimal search approaches
- **Strategy**: Adapts based on search success
- **Parameters**: `initial_strategies`, `adaptation_threshold`
- **Output**: Candidates found using best-performing strategies

### ConstraintGuidedExplorer
- **Best for**: Constraint-driven discovery
- **Strategy**: Constraint-guided search prioritization
- **Parameters**: `constraint_weight_threshold`, `early_validation`
- **Output**: Candidates likely to satisfy constraints

### DiversityExplorer
- **Best for**: Diverse candidate sets
- **Strategy**: Diversity-focused exploration
- **Parameters**: `diversity_threshold`, `category_limit`, `similarity_threshold`
- **Output**: Diverse candidates across categories

## ExplorationResult Structure

```python
@dataclass
class ExplorationResult:
    candidates: List[Candidate]           # Found candidates
    total_searched: int                   # Number of searches performed
    unique_candidates: int                # Number of unique candidates
    exploration_paths: List[str]          # Search path descriptions
    metadata: Dict                        # Strategy-specific metadata
    elapsed_time: float                   # Time taken for exploration
    strategy_used: ExplorationStrategy    # Strategy that was used
```

## Performance Considerations

### Speed vs. Quality Trade-offs
- **ParallelExplorer**: Fastest, good breadth
- **AdaptiveExplorer**: Medium speed, learns over time
- **ConstraintGuidedExplorer**: Medium speed, higher constraint satisfaction
- **DiversityExplorer**: Slower, but most diverse results

### Memory Usage
- All explorers track found candidates to avoid duplicates
- Large candidate sets may use significant memory
- Consider using `max_candidates` parameter to limit memory usage

### Search Engine Load
- Parallel explorers generate more concurrent search requests
- Consider rate limiting or using fewer `max_workers`
- Monitor search engine response times

## Extending the System

The inheritance-based design makes it easy to:

1. **Create domain-specific explorers** (e.g., GeoExplorer, PersonExplorer)
2. **Combine exploration strategies** (e.g., parallel + adaptive)
3. **Add new search patterns** and query generation methods
4. **Implement caching strategies** for discovered candidates
5. **Add quality scoring** for candidate ranking

## Best Practices

1. **Choose the right explorer** for your use case
2. **Set appropriate limits** (`max_candidates`, `max_search_time`)
3. **Provide good constraints** when using ConstraintGuidedExplorer
4. **Monitor diversity scores** when using DiversityExplorer
5. **Let AdaptiveExplorer learn** over multiple runs for best results
