Files
OpenJarvis/tests/learning/routing/test_learned_router.py
T
05f2c02131 feat: Algolia DocSearch + learning subsystem reorganization (#43)
* chore: create learning subdirectory structure (routing, agents, intelligence)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: extract classify_query to routing/_utils.py

Move the classify_query() function and its regex patterns into a shared
utility module so multiple routing policies can import it without
depending on the full trace_policy module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move routing files to learning/routing/ subdirectory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: create LearnedRouterPolicy merging trace-driven + SFT routing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add conditional Algolia DocSearch integration

Add Algolia DocSearch as an optional search upgrade — native lunr.js
search remains the default until credentials are configured. Includes
CDN assets, Jinja2 conditional config injection, init script with
graceful fallback, light/dark theme CSS, improved search tokenization
for snake_case/dotted identifiers, and search boosts for key pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move agent_evolver and skill_discovery to learning/agents/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move learning/orchestrator to learning/intelligence/orchestrator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: delete removed learning policies, rewrite __init__.py, clean up api_routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add SFT/GRPO/DSPy/GEPA config dataclasses, update LearningConfig

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add general-purpose SFT trainer (intelligence/sft_trainer.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update stale imports in multi_model_router example

Update imports to use new learning/routing/ paths after the
subdirectory reorganization. Replace BanditRouterPolicy with
LearnedRouterPolicy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add general-purpose GRPO trainer (intelligence/grpo_trainer.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add DSPy agent optimizer (agents/dspy_optimizer.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add GEPA agent optimizer (agents/gepa_optimizer.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add learning-dspy and learning-gepa optional dependency extras

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update integration test to check for learned policy instead of grpo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clean up stale APIs and unused params in examples

- deep_research: remove system_prompt and max_turns params not accepted
  by Jarvis.ask(), inline system prompt into the query instead
- doc_qa: remove unused --top-k CLI arg that was never passed to the API
- multi_model_router: fix select_model() call to match single-arg signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: import SFT/GRPO trainers in intelligence/__init__.py for registry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove .md file changes from PR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: restore search boost frontmatter for key docs pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:34:31 -07:00

152 lines
4.9 KiB
Python

"""Tests for LearnedRouterPolicy (merged trace-driven + SFT routing)."""
from __future__ import annotations
import time
from pathlib import Path
from openjarvis.core.types import StepType, Trace, TraceStep
from openjarvis.learning._stubs import RoutingContext
from openjarvis.learning.routing.learned_router import LearnedRouterPolicy
from openjarvis.traces.analyzer import TraceAnalyzer
from openjarvis.traces.store import TraceStore
def _make_trace(
query: str = "test",
model: str = "qwen3:8b",
outcome: str | None = "success",
feedback: float | None = 0.8,
) -> Trace:
now = time.time()
return Trace(
query=query,
agent="orchestrator",
model=model,
engine="ollama",
result="result",
outcome=outcome,
feedback=feedback,
started_at=now,
ended_at=now + 0.5,
total_tokens=100,
total_latency_seconds=0.5,
steps=[
TraceStep(
step_type=StepType.GENERATE,
timestamp=now,
duration_seconds=0.5,
output={"tokens": 100},
),
],
)
class TestLearnedRouterPolicy:
def test_registered_as_learned(self) -> None:
from openjarvis.core.registry import RouterPolicyRegistry
from openjarvis.learning.routing.learned_router import ensure_registered
ensure_registered()
assert RouterPolicyRegistry.contains("learned")
def test_fallback_no_traces(self) -> None:
policy = LearnedRouterPolicy(default_model="qwen3:8b")
ctx = RoutingContext(query="hello")
assert policy.select_model(ctx) == "qwen3:8b"
def test_fallback_chain(self) -> None:
policy = LearnedRouterPolicy(
default_model="missing",
fallback_model="llama3:8b",
available_models=["llama3:8b"],
)
ctx = RoutingContext(query="hello")
assert policy.select_model(ctx) == "llama3:8b"
def test_update_from_traces(self, tmp_path: Path) -> None:
store = TraceStore(tmp_path / "test.db")
for _ in range(6):
store.save(_make_trace(
query="def foo(): pass",
model="codestral",
outcome="success",
feedback=0.9,
))
for _ in range(6):
store.save(_make_trace(
query="def bar(): return 1",
model="qwen3:8b",
outcome="failure",
feedback=0.3,
))
analyzer = TraceAnalyzer(store)
policy = LearnedRouterPolicy(
analyzer=analyzer,
default_model="qwen3:8b",
)
policy.min_samples = 3
result = policy.update_from_traces()
assert result["updated"] is True
ctx = RoutingContext(query="import os; def main(): pass")
assert policy.select_model(ctx) == "codestral"
store.close()
def test_policy_map_readable(self, tmp_path: Path) -> None:
store = TraceStore(tmp_path / "test.db")
for _ in range(5):
store.save(_make_trace(
query="hello", model="small-model",
outcome="success",
))
analyzer = TraceAnalyzer(store)
policy = LearnedRouterPolicy(analyzer=analyzer, default_model="default")
policy.min_samples = 3
policy.update_from_traces()
pmap = policy.policy_map
assert isinstance(pmap, dict)
assert "short" in pmap
assert pmap["short"] == "small-model"
store.close()
def test_observe_online(self) -> None:
policy = LearnedRouterPolicy(default_model="default")
policy.min_samples = 3
policy.observe("hello", "fast-model", "success", 0.9)
assert policy.policy_map.get("short") == "fast-model"
def test_batch_update(self) -> None:
"""Test the batch update() method inherited from SFT routing logic."""
from unittest.mock import MagicMock
policy = LearnedRouterPolicy(default_model="default")
mock_store = MagicMock()
mock_store.list_traces.return_value = [
_make_trace(
query="def foo(): pass", model="code-model",
outcome="success", feedback=0.9,
),
_make_trace(
query="def bar(): pass", model="code-model",
outcome="success", feedback=0.85,
),
_make_trace(
query="def baz(): pass", model="code-model",
outcome="success", feedback=0.88,
),
_make_trace(
query="def qux(): pass", model="code-model",
outcome="success", feedback=0.92,
),
_make_trace(
query="def quux(): pass", model="code-model",
outcome="success", feedback=0.87,
),
]
result = policy.update(mock_store)
assert isinstance(result, dict)
assert "policy_map" in result