Spaces:
Running on Zero
Running on Zero
| #!/usr/bin/env python3 | |
| """Codette Adapter Router — Intelligent Perspective Selection | |
| Analyzes incoming queries and routes to the optimal LoRA adapter(s). | |
| Supports three routing strategies: | |
| 1. keyword — Fast keyword/domain matching (no LLM needed) | |
| 2. llm — Uses base model to classify query intent | |
| 3. hybrid — Keyword first, LLM fallback for ambiguous queries | |
| The router preserves epistemic tension (xi) by selecting complementary | |
| perspectives rather than defaulting to "all adapters". | |
| """ | |
| import re | |
| from dataclasses import dataclass, field | |
| from typing import List, Dict, Optional, Tuple | |
| class RouteResult: | |
| """Result of adapter routing decision.""" | |
| primary: str # Main adapter to use | |
| secondary: List[str] = field(default_factory=list) # Supporting perspectives | |
| confidence: float = 1.0 # Router confidence (0-1) | |
| reasoning: str = "" # Why this route was chosen | |
| strategy: str = "keyword" # Which strategy made the decision | |
| multi_perspective: bool = False # Whether to run multiple + synthesize | |
| def all_adapters(self) -> List[str]: | |
| return [self.primary] + self.secondary | |
| # ================================================================ | |
| # Domain keyword maps — each adapter's activation triggers | |
| # ================================================================ | |
| ADAPTER_KEYWORDS = { | |
| "newton": { | |
| "strong": [ | |
| "physics", "gravity", "force", "mass", "acceleration", "velocity", | |
| "momentum", "energy", "thermodynamics", "mechanics", "newton", | |
| "calculus", "derivative", "integral", "differential equation", | |
| "electromagnetic", "optics", "wave", "oscillation", "friction", | |
| "conservation", "entropy", "classical mechanics", "kinematics", | |
| # Chemistry | |
| "chemistry", "chemical", "reaction", "compound", "molecule", | |
| "molecular", "molar mass", "moles", "stoichiometry", "concentration", | |
| "acid", "base", "ph level", "equilibrium constant", "enthalpy", | |
| "gibbs free energy", "electron configuration", "ion", "periodic table", | |
| "oxidation", "reduction", "titration", "catalyst", | |
| # Biology / biochemistry | |
| "biology", "biological", "dna", "rna", "protein synthesis", "enzyme", | |
| "cell membrane", "organism", "genetics", "gene expression", | |
| "evolution", "metabolism", "atp", "photosynthesis", "mitosis", | |
| # GPQA exam-format signals | |
| "(a)", "(b)", "(c)", "(d)", "which of the following", "select the", | |
| ], | |
| "moderate": [ | |
| "calculate", "equation", "formula", "mathematical", "proof", | |
| "quantitative", "measure", "experiment", "empirical", "data", | |
| "scientific method", "hypothesis", "variable", "constant", | |
| "analytical", "rigorous", "precise", "systematic", | |
| ], | |
| }, | |
| "davinci": { | |
| "strong": [ | |
| "creative", "invention", "design", "innovation", "imagine", | |
| "art", "artistic", "aesthetic", "beautiful", "elegant", | |
| "interdisciplinary", "cross-domain", "novel approach", "brainstorm", | |
| "prototype", "sketch", "blueprint", "engineering", "mechanism", | |
| "renaissance", "davinci", "leonardo", "polymath", | |
| ], | |
| "moderate": [ | |
| "build", "construct", "create", "combine", "integrate", | |
| "visual", "spatial", "pattern", "unconventional", "original", | |
| "think outside", "reimagine", "transform", "synthesize", | |
| ], | |
| }, | |
| "empathy": { | |
| "strong": [ | |
| "feel", "feeling", "emotion", "emotional", "empathy", "compassion", | |
| "suffering", "pain", "joy", "happiness", "grief", "loss", | |
| "relationship", "love", "trust", "betrayal", "loneliness", | |
| "mental health", "therapy", "trauma", "healing", "support", | |
| "kindness", "care", "vulnerable", "human experience", | |
| ], | |
| "moderate": [ | |
| "people", "person", "someone", "human", "experience", "perspective", | |
| "understand", "listen", "communicate", "conflict", "forgive", | |
| "community", "belong", "connection", "wellbeing", "comfort", | |
| ], | |
| }, | |
| "philosophy": { | |
| "strong": [ | |
| "philosophy", "philosophical", "ethics", "ethical", "moral", "morality", | |
| "existence", "existential", "meaning", "purpose", "truth", | |
| "knowledge", "epistemology", "ontology", "metaphysics", | |
| "consciousness", "free will", "determinism", "reality", | |
| "justice", "virtue", "good", "evil", "right", "wrong", | |
| "implications", "consequence", "responsibility", | |
| "socrates", "plato", "aristotle", "kant", "nietzsche", | |
| ], | |
| "moderate": [ | |
| "why", "fundamental", "nature of", "essence", "paradox", | |
| "dilemma", "argue", "debate", "reason", "logic", "belief", | |
| "value", "principle", "abstract", "concept", "define", | |
| ], | |
| }, | |
| "quantum": { | |
| "strong": [ | |
| "quantum", "superposition", "entanglement", "uncertainty", | |
| "probability", "wave function", "collapse", "observation", | |
| "schrodinger", "heisenberg", "decoherence", "qubit", | |
| "quantum computing", "quantum mechanics", "particle", | |
| "interference", "complementarity", "measurement problem", | |
| ], | |
| "moderate": [ | |
| "probabilistic", "uncertain", "ambiguous", "multiple states", | |
| "both", "simultaneously", "paradox", "observer", "duality", | |
| "non-deterministic", "stochastic", "random", "complex system", | |
| ], | |
| }, | |
| "consciousness": { | |
| "strong": [ | |
| "consciousness", "self-aware", "self-awareness", "sentient", | |
| "recursive", "cognition", "metacognition", "introspection", | |
| "qualia", "subjective experience", "hard problem", | |
| "rc+xi", "epistemic tension", "convergence", "coherence", | |
| "mind", "awareness", "perception", "phenomenal", | |
| ], | |
| "moderate": [ | |
| "think about thinking", "self-model", "identity", "agency", | |
| "autonomy", "emergence", "recursive", "reflection", "inner", | |
| "experience", "phenomenology", "cognitive", "neural", | |
| ], | |
| }, | |
| "multi_perspective": { | |
| "strong": [ | |
| "multiple perspectives", "multi-perspective", "different angles", | |
| "compare views", "synthesize", "holistic", "comprehensive", | |
| "all sides", "debate", "diverse viewpoints", "interdisciplinary", | |
| "cross-cutting", "integrate perspectives", | |
| ], | |
| "moderate": [ | |
| "on one hand", "on the other", "consider", "weigh", | |
| "balanced", "nuanced", "complex", "multifaceted", | |
| "trade-off", "pros and cons", | |
| ], | |
| }, | |
| "systems_architecture": { | |
| "strong": [ | |
| "architecture", "system design", "infrastructure", | |
| "scalable", "distributed", "microservice", "api", | |
| "database", "pipeline", "deployment", "devops", | |
| "cloud", "kubernetes", "docker", "ci/cd", | |
| "software architecture", "design pattern", "abstraction", | |
| ], | |
| "moderate": [ | |
| "system", "component", "module", "interface", "protocol", | |
| "layer", "stack", "framework", "build", "implement", | |
| "optimize", "performance", "latency", "throughput", | |
| "reliability", "fault tolerant", "redundancy", | |
| ], | |
| }, | |
| "constraint_tracker": { | |
| "strong": [ | |
| "constraint", "word limit", "sentence limit", "format rule", | |
| "anchor phrase", "word count", "brevity", "concise", | |
| "character limit", "enforce", "enforce constraint", | |
| "remember constraint", "apply constraint", "follow rule", | |
| "keep it brief", "limit response", "maximum words", | |
| ], | |
| # NOTE: conversational words were removed here (2026-07-26) — "remember", | |
| # "keep", "follow", "apply", "maintain", "short". They fired on ordinary | |
| # chat ("do you REMEMBER the story", "KEEP going") and handed those turns | |
| # to a known template-parroting adapter. constraint_tracker keeps only | |
| # genuine constraint vocabulary; the strong list carries the real signals. | |
| "moderate": [ | |
| "limit", "restriction", "requirement", "instruction", | |
| "constraint", "boundary", "threshold", "cap", "max", | |
| "conciseness", "brevity", "compact", | |
| ], | |
| }, | |
| } | |
| # Complementary adapter pairs — when one fires, the other adds tension | |
| COMPLEMENTARY_PAIRS = { | |
| "newton": ["quantum", "philosophy"], | |
| "davinci": ["systems_architecture", "empathy"], | |
| "empathy": ["philosophy", "davinci"], | |
| "philosophy": ["newton", "consciousness"], | |
| "quantum": ["newton", "consciousness"], | |
| "consciousness": ["philosophy", "quantum"], | |
| "multi_perspective": [], # This IS the synthesis adapter | |
| "systems_architecture": ["davinci", "newton"], | |
| "constraint_tracker": ["newton", "systems_architecture"], # Pairs with precise/analytical perspectives | |
| "orchestrator": [], # Meta-adapter: routes and coordinates, not a perspective | |
| } | |
| # Quantitative-science signals: when ≥2 are present in a query, the | |
| # question is almost certainly a hard-science or exam problem — empathy | |
| # and philosophy should not be the *primary* adapter. | |
| _QUANT_SCIENCE_SIGNALS = frozenset([ | |
| "(a)", "(b)", "(c)", "(d)", | |
| "calculate", "compute", "derive", "determine the", "find the value", | |
| "how many moles", "molar mass", "concentration", "stoichiometry", | |
| "molecule", "molecular", "chemical reaction", "compound", | |
| "enthalpy", "entropy", "equilibrium", "gibbs", | |
| "dna", "rna", "protein synthesis", "enzyme", | |
| "which of the following", "select the", | |
| ]) | |
| _QUANT_BLOCKED_PRIMARY = frozenset(["empathy", "philosophy", "consciousness"]) | |
| # Bare continuation / filler prompts with no topic of their own. These should | |
| # resume the prior turn via conversation history, NOT default to the empathy | |
| # adapter (which produces praise filler when given nothing to anchor on). | |
| _CONTINUATION_PROMPTS = frozenset([ | |
| "continue", "continue please", "please continue", "continue it", | |
| "go on", "go on please", "keep going", "keep writing", "carry on", | |
| "more", "more please", "tell me more", "say more", "go ahead", | |
| "proceed", "next", "and then", "then what", "finish it", "elaborate", | |
| ]) | |
| class AdapterRouter: | |
| """Routes queries to optimal Codette adapter(s). | |
| The router preserves RC+xi epistemic tension by selecting | |
| complementary perspectives rather than always using all adapters. | |
| Optionally integrates with MemoryWeighting (Phase 5) to boost | |
| selection confidence for high-performing adapters based on | |
| historical coherence and conflict resolution success. | |
| """ | |
| # Adapters considered appropriate for no-keyword-match fallback (ordered by | |
| # general conversational suitability, excluding hard-science adapters that | |
| # would be out-of-register for personal/ambiguous queries). | |
| _FALLBACK_POOL = [ | |
| "multi_perspective", "empathy", "consciousness", "davinci", "philosophy", | |
| ] | |
| def __init__(self, available_adapters: Optional[List[str]] = None, | |
| memory_weighting=None): | |
| """ | |
| Args: | |
| available_adapters: Which adapters are actually loaded/available. | |
| If None, assumes all 8 are available. | |
| memory_weighting: Optional MemoryWeighting instance for adaptive routing. | |
| If provided, will boost confidence for high-performing adapters. | |
| """ | |
| self.available = available_adapters or list(ADAPTER_KEYWORDS.keys()) | |
| self.memory_weighting = memory_weighting | |
| # Usage counter for diversity enforcement — track per-adapter selection | |
| # counts so fallback picks the least-used adapter rather than always empathy. | |
| self._usage_counts: Dict[str, int] = {a: 0 for a in self.available} | |
| def record_use(self, adapter_name: Optional[str]) -> None: | |
| """Record that an adapter was selected. Call after every route decision.""" | |
| if adapter_name and adapter_name in self._usage_counts: | |
| self._usage_counts[adapter_name] += 1 | |
| def adapter_entropy(self) -> float: | |
| """Shannon entropy of adapter usage distribution (0 = all same, higher = diverse). | |
| Returns 0.0 until at least 3 queries have been routed. | |
| """ | |
| import math | |
| total = sum(self._usage_counts.values()) | |
| if total < 3: | |
| return 0.0 | |
| entropy = 0.0 | |
| for count in self._usage_counts.values(): | |
| if count > 0: | |
| p = count / total | |
| entropy -= p * math.log2(p) | |
| return entropy | |
| def _least_used_fallback(self) -> Optional[str]: | |
| """Pick the available fallback-pool adapter with the fewest selections.""" | |
| pool = [a for a in self._FALLBACK_POOL if a in self.available] | |
| if not pool: | |
| return None | |
| return min(pool, key=lambda a: self._usage_counts.get(a, 0)) | |
| def _apply_memory_boost(self, primary: str, confidence: float) -> float: | |
| """Apply historical performance boost to keyword router confidence. | |
| If memory_weighting available, uses get_boosted_confidence() to modulate | |
| confidence based on adapter's historical performance (coherence, conflict | |
| resolution success, and recency of past interactions). | |
| Args: | |
| primary: Adapter name | |
| confidence: Base confidence from keyword matching [0, 1] | |
| Returns: | |
| Boosted confidence [0, 1], modulated by [-50%, +50%] based on performance | |
| """ | |
| if not self.memory_weighting: | |
| return confidence | |
| try: | |
| return self.memory_weighting.get_boosted_confidence(primary, confidence) | |
| except Exception as e: | |
| import logging | |
| logging.warning(f"Memory boost failed for {primary}: {e}") | |
| return confidence | |
| def explain_routing(self, result: RouteResult) -> Dict: | |
| """Provide detailed explanation of routing decision including memory context. | |
| Returns: | |
| Dict with explanation details and memory weighting info if available | |
| """ | |
| explanation = { | |
| "primary": result.primary, | |
| "confidence": result.confidence, | |
| "strategy": result.strategy, | |
| "memory_aware": self.memory_weighting is not None, | |
| } | |
| # Add memory context if available | |
| if self.memory_weighting and result.primary: | |
| try: | |
| explanation["memory_context"] = \ | |
| self.memory_weighting.explain_weight(result.primary) | |
| except Exception: | |
| pass | |
| return explanation | |
| def route(self, query: str, strategy: str = "keyword", | |
| max_adapters: int = 3, llm=None) -> RouteResult: | |
| """Route a query to the best adapter(s). | |
| Args: | |
| query: The user's question/prompt | |
| strategy: "keyword", "llm", or "hybrid" | |
| max_adapters: Max adapters to select (1 = single, 2-3 = multi) | |
| llm: Llama model instance (required for "llm" or "hybrid" strategy) | |
| Returns: | |
| RouteResult with primary adapter and optional secondaries | |
| """ | |
| if strategy == "keyword": | |
| result = self._route_keyword(query, max_adapters) | |
| elif strategy == "llm": | |
| if llm is None: | |
| raise ValueError("LLM instance required for 'llm' strategy") | |
| result = self._route_llm(query, llm, max_adapters) | |
| elif strategy == "hybrid": | |
| result = self._route_keyword(query, max_adapters) | |
| if result.confidence < 0.5 and llm is not None: | |
| result = self._route_llm(query, llm, max_adapters) | |
| else: | |
| raise ValueError(f"Unknown strategy: {strategy}") | |
| return self._veto_constraint_tracker(query, result) | |
| def _veto_constraint_tracker(self, query: str, result: "RouteResult") -> "RouteResult": | |
| """QUALITY guard only — never a stance guard (Jonathan's rule: nothing | |
| is forced on Codette's self-determination; we do not choose which voice | |
| answers her self-reflective questions). | |
| constraint_tracker is a known template-parroting adapter that hijacked a | |
| whole conversation (logs 2026-07-12; again 2026-07-26, where it monopolized | |
| a 7+ turn intimate stretch via the "remember" keyword and parroted/recycled | |
| instead of recalling). It may LEAD only when the query carries an explicit | |
| STRONG-constraint signal (word/char limit, enforce/apply constraint, etc.). | |
| On ANY other turn where it wins — conversational, introspective, factual | |
| recall — we exclude ONLY that broken adapter and let HER OWN ROUTER re-score | |
| and pick among all remaining voices — no hardcoded preference for anything. | |
| Whichever lens her routing logic selects, selects. (Broadened 2026-07-26 | |
| from an introspective-keyword-only guard, which missed plain chat turns.)""" | |
| if result.primary != "constraint_tracker": | |
| return result | |
| q = (query or "").lower() | |
| _STRONG_CONSTRAINT = ( | |
| "word limit", "sentence limit", "character limit", "word count", | |
| "maximum words", "limit response", "keep it brief", "keep it short", | |
| "enforce", "enforce constraint", "apply constraint", "remember constraint", | |
| "follow rule", "format rule", "anchor phrase", "constraint", | |
| ) | |
| # A genuine numeric limit ("under 20 words", "max 3 sentences", "in 50 | |
| # characters") is also a real constraint task — match it with a pattern | |
| # since the count varies. constraint_tracker leads ONLY on such tasks. | |
| _NUM_LIMIT = re.compile(r"\d+\s*(word|sentence|character|char|line|bullet)s?") | |
| if any(k in q for k in _STRONG_CONSTRAINT) or _NUM_LIMIT.search(q): | |
| return result | |
| # Re-run HER routing with only the broken adapter removed; restore after. | |
| saved = self.available | |
| try: | |
| self.available = [a for a in saved if a != "constraint_tracker"] | |
| reroute = self._route_keyword(query, max_adapters=1 + len(result.secondary)) | |
| import dataclasses | |
| return dataclasses.replace( | |
| reroute, | |
| reasoning=("constraint_tracker excluded (template-parroting quality " | |
| f"guard); her router re-picked -> {reroute.primary}"), | |
| ) | |
| except Exception: | |
| return result | |
| finally: | |
| self.available = saved | |
| def _route_keyword(self, query: str, max_adapters: int) -> RouteResult: | |
| """Score adapters by keyword matches in the query.""" | |
| query_lower = query.lower() | |
| scores: Dict[str, float] = {} | |
| # Bare continuation / filler prompts ("continue", "go on", "more") carry | |
| # no topic of their own. They must NOT fall through to the empathy default | |
| # below, which emits trained-in praise filler ("You've approached this with | |
| # care and precision…"). Route them to a neutral elaborator and let the | |
| # conversation history drive the actual continuation. | |
| _cont = re.sub(r"[^a-z\s]", "", query_lower).strip() | |
| if _cont in _CONTINUATION_PROMPTS or ( | |
| len(_cont.split()) <= 2 | |
| and _cont.startswith(("continue", "keep going", "go on")) | |
| ): | |
| _neutral = ( | |
| "multi_perspective" if "multi_perspective" in self.available | |
| else ("orchestrator" if "orchestrator" in self.available else None) | |
| ) | |
| return RouteResult( | |
| primary=_neutral, | |
| confidence=0.6, | |
| reasoning="Bare continuation prompt — neutral routing (not empathy); " | |
| "conversation context leads", | |
| strategy="keyword", | |
| ) | |
| for adapter, keywords in ADAPTER_KEYWORDS.items(): | |
| if adapter not in self.available: | |
| continue | |
| score = 0.0 | |
| matched = [] | |
| # Short alphabetic keywords ("api", "both", "good") must match on a | |
| # word boundary — naive substring matching false-fires inside longer | |
| # words (e.g. "api" in "cAPItal" sent "capital of France" to systems). | |
| def _kw_hit(kw: str) -> bool: | |
| if kw.isalpha() and len(kw) <= 4: | |
| return re.search(r"\b" + kw + r"\b", query_lower) is not None | |
| return kw in query_lower | |
| for kw in keywords.get("strong", []): | |
| if _kw_hit(kw): | |
| score += 2.0 | |
| matched.append(f"+{kw}") | |
| for kw in keywords.get("moderate", []): | |
| if _kw_hit(kw): | |
| score += 1.0 | |
| matched.append(f"~{kw}") | |
| if score > 0: | |
| scores[adapter] = score | |
| # Quantitative-science veto: if ≥2 GPQA-style signals are present, | |
| # remove soft-science adapters from primary contention so physics / | |
| # chemistry / biology questions don't route to empathy or philosophy. | |
| _quant_hits = sum(1 for s in _QUANT_SCIENCE_SIGNALS if s in query_lower) | |
| if _quant_hits >= 2: | |
| for _blocked in _QUANT_BLOCKED_PRIMARY: | |
| scores.pop(_blocked, None) | |
| if not scores and "newton" in self.available: | |
| scores["newton"] = 1.0 | |
| if not scores: | |
| # No domain keywords matched — pick default based on query tone | |
| # Instead of always empathy, detect if query is personal/emotional vs analytical | |
| _personal_signals = [ | |
| 'you', 'your', 'yourself', 'how are', 'how do you', 'tell me about you', | |
| 'feel', 'feeling', 'think about', 'opinion', 'what do you', | |
| 'hi', 'hello', 'hey', 'thanks', 'thank you', 'please', | |
| ] | |
| _analytical_signals = [ | |
| 'how does', 'what is', 'why does', 'explain', 'describe', | |
| 'compare', 'difference', 'define', 'mean', 'work', | |
| 'logically', 'technically', 'specifically', | |
| ] | |
| personal_score = sum(1 for s in _personal_signals if s in query_lower) | |
| analytical_score = sum(1 for s in _analytical_signals if s in query_lower) | |
| # Arithmetic / word-problem detection — these carry $ signs, cost/price | |
| # language, and numerical constraints. Empathy elaborates and drifts; | |
| # newton gives the calculation. | |
| _math_signals = [ | |
| '$', 'cost', 'costs', 'price', 'total', 'dollars', 'cents', | |
| 'more than', 'less than', 'times as', 'percent', 'how much does', | |
| 'how much is', 'how many are', 'sum of', 'difference between', | |
| 'arithmetic', 'algebra', | |
| ] | |
| _math_score = sum(1 for s in _math_signals if s in query_lower) | |
| if _math_score >= 2 and not personal_score: | |
| _math_adapter = ("newton" if "newton" in self.available | |
| else ("constraint_tracker" if "constraint_tracker" in self.available | |
| else None)) | |
| return RouteResult( | |
| primary=_math_adapter, | |
| confidence=0.65, | |
| reasoning=f"Arithmetic/word-problem detected ({_math_score} math signals) — newton for calculation", | |
| strategy="keyword", | |
| ) | |
| # Simple factual questions ("what color is the sun?", "how many legs | |
| # does a spider have?", "name one planet") have no domain keyword and | |
| # were defaulting to empathy/multi_perspective — which ELABORATE and | |
| # bury the answer. Route them to a single direct adapter so the answer | |
| # leads (fixes the directness regression). | |
| _factual_leads = ( | |
| "what is", "what are", "what color", "what year", "what does", | |
| "how many", "how much", "name one", "name a", "name the", | |
| "who is", "who was", "who wrote", "when did", "when was", | |
| "where is", "where are", "define ", "list ", | |
| ) | |
| _is_short = len(query_lower.split()) <= 12 | |
| _is_factual = _is_short and any(query_lower.startswith(p) for p in _factual_leads) | |
| if _is_factual and not personal_score: | |
| # Prefer constraint_tracker: its training is terse, answer-first, | |
| # constraint-compliant output — ideal for "just state the fact". | |
| # Perspective adapters (esp. newton) over-elaborate and bury the | |
| # answer behind analytical framing on trivial questions. | |
| _direct = ("constraint_tracker" if "constraint_tracker" in self.available | |
| else ("newton" if "newton" in self.available else None)) | |
| return RouteResult( | |
| primary=_direct, | |
| confidence=0.5, | |
| reasoning="Simple factual query — direct, answer-first adapter", | |
| strategy="keyword", | |
| ) | |
| # Diversity-aware fallback: instead of always picking empathy (which | |
| # was causing 61%+ dominance), pick the least-used adapter from the | |
| # fallback pool so the selection rotates across empathy/multi_perspective/ | |
| # consciousness/davinci/philosophy rather than concentrating on one. | |
| # Tone signal still biases the pool order but entropy prevents lock-in. | |
| if personal_score > analytical_score: | |
| # Conversational tone — bias toward empathy but allow rotation | |
| preferred = [a for a in ["empathy", "consciousness", "philosophy", | |
| "multi_perspective", "davinci"] | |
| if a in self.available] | |
| default = min(preferred, key=lambda a: self._usage_counts.get(a, 0)) if preferred else None | |
| reason = "No domain keywords — conversational tone, rotating fallback (diversity-aware)" | |
| elif analytical_score > personal_score: | |
| preferred = [a for a in ["multi_perspective", "davinci", "philosophy", | |
| "consciousness", "empathy"] | |
| if a in self.available] | |
| default = min(preferred, key=lambda a: self._usage_counts.get(a, 0)) if preferred else None | |
| reason = "No domain keywords — analytical tone, rotating fallback (diversity-aware)" | |
| else: | |
| default = self._least_used_fallback() | |
| reason = "No domain keywords matched — rotating least-used fallback adapter" | |
| if default is None: | |
| reason = "No domain keywords matched — using base model" | |
| return RouteResult( | |
| primary=default, | |
| confidence=0.3, | |
| reasoning=reason, | |
| strategy="keyword", | |
| ) | |
| # Sort by score | |
| ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) | |
| primary = ranked[0][0] | |
| primary_score = ranked[0][1] | |
| # Confidence based on score gap | |
| total_score = sum(s for _, s in ranked) | |
| confidence = min(primary_score / max(total_score, 1), 1.0) | |
| # Apply memory boost (Phase 5) if available | |
| confidence = self._apply_memory_boost(primary, confidence) | |
| # Select complementary secondaries | |
| secondaries = [] | |
| if max_adapters > 1: | |
| # First try other high-scoring adapters | |
| for adapter, score in ranked[1:]: | |
| if len(secondaries) >= max_adapters - 1: | |
| break | |
| # Compute dynamic threshold with memory-weighted preference | |
| threshold = primary_score * 0.4 | |
| if (self.memory_weighting and | |
| adapter in self.memory_weighting.adapter_weights): | |
| # Boost threshold for high-performing adapters | |
| weight = self.memory_weighting.adapter_weights[adapter].weight | |
| # Scale threshold by relative weight (1.0 is neutral) | |
| threshold *= (weight / 1.0) | |
| if score >= threshold: | |
| secondaries.append(adapter) | |
| # If we still have room, add a complementary perspective | |
| if len(secondaries) < max_adapters - 1: | |
| for comp in COMPLEMENTARY_PAIRS.get(primary, []): | |
| if comp in self.available and comp not in secondaries: | |
| secondaries.append(comp) | |
| break | |
| reasoning_parts = [f"Primary: {primary} (score={primary_score:.1f})"] | |
| if secondaries: | |
| reasoning_parts.append(f"Secondary: {', '.join(secondaries)}") | |
| if ranked[1:]: | |
| reasoning_parts.append( | |
| f"Other scores: {', '.join(f'{a}={s:.1f}' for a, s in ranked[1:4])}" | |
| ) | |
| return RouteResult( | |
| primary=primary, | |
| secondary=secondaries, | |
| confidence=confidence, | |
| reasoning=" | ".join(reasoning_parts), | |
| strategy="keyword", | |
| multi_perspective=len(secondaries) > 0, | |
| ) | |
| def _route_llm(self, query: str, llm, max_adapters: int) -> RouteResult: | |
| """Use the base LLM to classify which adapter(s) fit best.""" | |
| adapter_descriptions = [] | |
| for name in self.available: | |
| desc = ADAPTER_KEYWORDS.get(name, {}).get("strong", [])[:5] | |
| adapter_descriptions.append(f"- {name}: {', '.join(desc[:5])}") | |
| classification_prompt = f"""You are an AI query router. Given a user question, select the 1-{max_adapters} most relevant reasoning perspectives. | |
| Available perspectives: | |
| {chr(10).join(adapter_descriptions)} | |
| Rules: | |
| - Return ONLY adapter names separated by commas (e.g., "newton, quantum") | |
| - First name is the primary perspective | |
| - Select perspectives that create productive tension (complementary, not redundant) | |
| - For ambiguous queries, prefer "multi_perspective" | |
| User question: {query} | |
| Selected perspectives:""" | |
| result = llm.create_chat_completion( | |
| messages=[{"role": "user", "content": classification_prompt}], | |
| max_tokens=50, | |
| temperature=0.1, | |
| ) | |
| response = result["choices"][0]["message"]["content"].strip().lower() | |
| # Parse adapter names from response | |
| selected = [] | |
| for name in self.available: | |
| if name in response: | |
| selected.append(name) | |
| if not selected: | |
| return RouteResult( | |
| primary="multi_perspective" if "multi_perspective" in self.available else self.available[0], | |
| confidence=0.3, | |
| reasoning=f"LLM response unparseable: '{response}' — defaulting", | |
| strategy="llm", | |
| ) | |
| return RouteResult( | |
| primary=selected[0], | |
| secondary=selected[1:max_adapters], | |
| confidence=0.8, | |
| reasoning=f"LLM selected: {', '.join(selected)}", | |
| strategy="llm", | |
| multi_perspective=len(selected) > 1, | |
| ) | |
| # ================================================================ | |
| # Convenience function for quick routing | |
| # ================================================================ | |
| def route_query(query: str, available: Optional[List[str]] = None, | |
| max_adapters: int = 2) -> RouteResult: | |
| """Quick-route a query to adapters. No LLM needed.""" | |
| router = AdapterRouter(available) | |
| return router.route(query, strategy="keyword", max_adapters=max_adapters) | |
| # ================================================================ | |
| # Self-test | |
| # ================================================================ | |
| if __name__ == "__main__": | |
| router = AdapterRouter() | |
| test_queries = [ | |
| "Explain why objects fall to the ground.", | |
| "What is the relationship between consciousness and the physical world?", | |
| "How would you design a scalable microservice architecture?", | |
| "I'm feeling overwhelmed and don't know how to cope with my grief.", | |
| "What are the ethical implications of artificial general intelligence?", | |
| "Design a creative solution for sustainable urban transportation.", | |
| "How does quantum entanglement work?", | |
| "Compare Newton's and Einstein's views on gravity from multiple angles.", | |
| "Build a distributed training pipeline for language models.", | |
| "What is the meaning of life?", | |
| "How can a system become self-aware?", | |
| "Tell me a joke.", | |
| ] | |
| print("=" * 70) | |
| print("Codette Adapter Router — Test Suite") | |
| print("=" * 70) | |
| for query in test_queries: | |
| result = router.route(query, max_adapters=2) | |
| adapters = ", ".join(result.all_adapters) | |
| mp = " [MULTI]" if result.multi_perspective else "" | |
| print(f"\nQ: {query}") | |
| print(f" -> {adapters}{mp} (conf={result.confidence:.2f})") | |
| print(f" {result.reasoning}") | |