| """
|
| 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})"
|
|
|