Codette-Reasoning-Demo / inference /adapter_router.py
Raiff1982's picture
Upgrade to REAL orchestrated Codette on ZeroGPU (transformers backend for the llama.cpp pipeline)
c8fbdf1 verified
Raw
History Blame Contribute Delete
34.3 kB
#!/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
@dataclass
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
@property
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}")