| """ |
| Base class for all reasoning agents in the forge. |
| |
| Each agent must implement analyze() and get_analysis_templates(). |
| The base class provides keyword matching and template selection utilities, |
| and optionally uses real LLM inference via adapters. |
| """ |
|
|
| from abc import ABC, abstractmethod |
| import random |
| import re |
| import logging |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class ReasoningAgent(ABC): |
| """Abstract base class for all reasoning agents.""" |
|
|
| name: str = "BaseAgent" |
| perspective: str = "general" |
| adapter_name: str = None |
|
|
| def __init__(self, orchestrator=None): |
| """ |
| Args: |
| orchestrator: Optional CodetteOrchestrator for real LLM inference. |
| If None, falls back to template-based responses. |
| """ |
| self._templates = self.get_analysis_templates() |
| self._keyword_map = self.get_keyword_map() |
| self.orchestrator = orchestrator |
|
|
| def analyze(self, concept: str) -> str: |
| """Analyze a concept from this agent's perspective. |
| |
| Uses real LLM inference if orchestrator is available, |
| otherwise falls back to template-based responses. |
| |
| Args: |
| concept: The concept text to analyze. |
| |
| Returns: |
| A substantive analysis string from this agent's perspective. |
| """ |
| |
| if self.orchestrator and self.adapter_name: |
| try: |
| return self._analyze_with_llm(concept) |
| except Exception as e: |
| logger.warning(f"{self.name} LLM inference failed: {e}, falling back to templates") |
|
|
| |
| return self._analyze_with_template(concept) |
|
|
| def _analyze_with_llm(self, concept: str) -> str: |
| """Call the LLM with this agent's adapter for real reasoning. |
| |
| Args: |
| concept: The concept to analyze. |
| |
| Returns: |
| LLM-generated analysis from this agent's perspective. |
| """ |
| if not self.orchestrator or not self.adapter_name: |
| raise ValueError("Orchestrator and adapter_name required for LLM inference") |
|
|
| |
| template = self.select_template(concept) |
| system_prompt = template.replace("{concept}", concept) |
|
|
| |
| import os |
| verbose = os.environ.get('CODETTE_VERBOSE', '0') == '1' |
| if verbose: |
| logger.info(f"\n[{self.name}] Analyzing '{concept[:50]}...'") |
| logger.info(f" Adapter: {self.adapter_name}") |
| logger.info(f" System prompt: {system_prompt[:100]}...") |
|
|
| |
| response, tokens, _ = self.orchestrator.generate( |
| query=concept, |
| adapter_name=self.adapter_name, |
| system_prompt=system_prompt, |
| enable_tools=False |
| ) |
|
|
| if verbose: |
| logger.info(f" Generated: {len(response)} chars, {tokens} tokens") |
| logger.info(f" Response preview: {response[:150]}...") |
|
|
| return response.strip() |
|
|
| def _analyze_with_template(self, concept: str) -> str: |
| """Fallback: generate response using template substitution. |
| |
| Args: |
| concept: The concept to analyze. |
| |
| Returns: |
| Template-based analysis. |
| """ |
| template = self.select_template(concept) |
| return template.replace("{concept}", concept) |
|
|
| @abstractmethod |
| def get_analysis_templates(self) -> list[str]: |
| """Return diverse analysis templates. |
| |
| Each template should contain {concept} placeholder and produce |
| genuine expert-level reasoning, not placeholder text. |
| |
| Returns: |
| List of template strings. |
| """ |
| raise NotImplementedError |
|
|
| def get_keyword_map(self) -> dict[str, list[int]]: |
| """Return a mapping of keywords to preferred template indices. |
| |
| Override in subclasses to steer template selection based on |
| concept content. Keys are lowercase keywords/phrases, values |
| are lists of template indices that work well for that keyword. |
| |
| Returns: |
| Dictionary mapping keywords to template index lists. |
| """ |
| return {} |
|
|
| def select_template(self, concept: str) -> str: |
| """Select the best template for the given concept. |
| |
| Uses keyword matching to find relevant templates. Falls back |
| to random selection if no keywords match. |
| |
| Args: |
| concept: The concept text. |
| |
| Returns: |
| A single template string. |
| """ |
| concept_lower = concept.lower() |
| scored_indices: dict[int, int] = {} |
|
|
| for keyword, indices in self._keyword_map.items(): |
| if keyword in concept_lower: |
| for idx in indices: |
| if 0 <= idx < len(self._templates): |
| scored_indices[idx] = scored_indices.get(idx, 0) + 1 |
|
|
| if scored_indices: |
| max_score = max(scored_indices.values()) |
| best = [i for i, s in scored_indices.items() if s == max_score] |
| chosen = random.choice(best) |
| return self._templates[chosen] |
|
|
| return random.choice(self._templates) |
|
|
| def extract_key_terms(self, concept: str) -> list[str]: |
| """Extract significant terms from the concept for template filling. |
| |
| Args: |
| concept: The concept text. |
| |
| Returns: |
| List of key terms found in the concept. |
| """ |
| stop_words = { |
| "the", "a", "an", "is", "are", "was", "were", "be", "been", |
| "being", "have", "has", "had", "do", "does", "did", "will", |
| "would", "could", "should", "may", "might", "can", "shall", |
| "of", "in", "to", "for", "with", "on", "at", "from", "by", |
| "about", "as", "into", "through", "during", "before", "after", |
| "above", "below", "between", "and", "but", "or", "nor", "not", |
| "so", "yet", "both", "either", "neither", "each", "every", |
| "this", "that", "these", "those", "it", "its", "they", "them", |
| "their", "we", "our", "you", "your", "he", "she", "his", "her", |
| "how", "what", "when", "where", "which", "who", "why", |
| } |
| words = re.findall(r'\b[a-zA-Z]{3,}\b', concept.lower()) |
| return [w for w in words if w not in stop_words] |
|
|
| def __repr__(self) -> str: |
| return f"{self.__class__.__name__}(name={self.name!r}, perspective={self.perspective!r})" |
|
|