Files
OpenJarvis/tests/test_orchestrator_learning/test_reward.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

144 lines
5.0 KiB
Python

"""Tests for orchestrator multi-objective reward."""
from __future__ import annotations
import pytest
from openjarvis.learning.intelligence.orchestrator.reward import (
AdaptiveRewardWeights,
MultiObjectiveReward,
Normalizers,
RewardWeights,
)
from openjarvis.learning.intelligence.orchestrator.types import (
Episode,
)
class TestRewardWeights:
def test_default_sum(self):
w = RewardWeights()
total = w.alpha + w.beta_cost + w.beta_energy + w.gamma_latency + w.gamma_power
assert abs(total - 1.0) < 0.01
def test_invalid_sum_raises(self):
with pytest.raises(ValueError, match="sum to 1.0"):
RewardWeights(alpha=0.9, beta_cost=0.5)
def test_custom_weights(self):
w = RewardWeights(
alpha=0.5,
beta_cost=0.1,
beta_energy=0.1,
gamma_latency=0.2,
gamma_power=0.1,
)
assert w.alpha == 0.5
class TestMultiObjectiveReward:
def _make_episode(self, correct: bool = True) -> Episode:
ep = Episode(
task_id="t",
initial_prompt="q",
ground_truth="4",
final_answer="4" if correct else "5",
correct=correct,
total_energy_joules=50.0,
total_cost_usd=0.05,
total_latency_seconds=15.0,
max_power_watts=100.0,
)
return ep
def test_correct_episode_positive(self):
reward_fn = MultiObjectiveReward(RewardWeights(), Normalizers())
ep = self._make_episode(correct=True)
r = reward_fn.compute(ep)
assert r > 0, "Correct episode should have positive reward"
def test_incorrect_episode_negative(self):
reward_fn = MultiObjectiveReward(RewardWeights(), Normalizers())
ep = self._make_episode(correct=False)
r = reward_fn.compute(ep)
assert r < 0, "Incorrect episode should have negative reward"
def test_correct_better_than_incorrect(self):
reward_fn = MultiObjectiveReward(RewardWeights(), Normalizers())
correct = reward_fn.compute(self._make_episode(correct=True))
incorrect = reward_fn.compute(self._make_episode(correct=False))
assert correct > incorrect
def test_compute_with_breakdown(self):
reward_fn = MultiObjectiveReward(RewardWeights(), Normalizers())
ep = self._make_episode(correct=True)
bd = reward_fn.compute_with_breakdown(ep)
assert "total_reward" in bd
assert "accuracy_reward" in bd
assert bd["accuracy_reward"] == 1.0
assert bd["cost_penalty"] > 0
assert bd["energy_penalty"] > 0
assert "ipj" in bd
def test_compute_batch(self):
reward_fn = MultiObjectiveReward(RewardWeights(), Normalizers())
episodes = [
self._make_episode(correct=True),
self._make_episode(correct=False),
]
rewards = reward_fn.compute_batch(episodes)
assert len(rewards) == 2
assert rewards[0] > rewards[1]
def test_zero_cost_episode(self):
reward_fn = MultiObjectiveReward(RewardWeights(), Normalizers())
ep = Episode(
task_id="t",
initial_prompt="q",
correct=True,
total_energy_joules=0.0,
total_cost_usd=0.0,
total_latency_seconds=0.0,
max_power_watts=0.0,
)
r = reward_fn.compute(ep)
# Only accuracy component, no penalties
assert r == pytest.approx(RewardWeights().alpha)
class TestAdaptiveRewardWeights:
def test_at_zero_progress(self):
adaptive = AdaptiveRewardWeights(total_steps=10000)
w = adaptive.get_weights(0)
total = w.alpha + w.beta_cost + w.beta_energy + w.gamma_latency + w.gamma_power
assert abs(total - 1.0) < 0.01
# At step 0, alpha should be close to initial (highest)
assert w.alpha > 0.5
def test_at_fifty_percent(self):
adaptive = AdaptiveRewardWeights(total_steps=10000)
w = adaptive.get_weights(5000)
total = w.alpha + w.beta_cost + w.beta_energy + w.gamma_latency + w.gamma_power
assert abs(total - 1.0) < 0.01
def test_at_hundred_percent(self):
adaptive = AdaptiveRewardWeights(total_steps=10000)
w = adaptive.get_weights(10000)
total = w.alpha + w.beta_cost + w.beta_energy + w.gamma_latency + w.gamma_power
assert abs(total - 1.0) < 0.01
# At step 10000, alpha should be lower
w0 = adaptive.get_weights(0)
assert w.alpha < w0.alpha
def test_alpha_decreases(self):
adaptive = AdaptiveRewardWeights(total_steps=1000)
w_start = adaptive.get_weights(0)
w_end = adaptive.get_weights(1000)
assert w_start.alpha > w_end.alpha
def test_beyond_total_steps_clamped(self):
adaptive = AdaptiveRewardWeights(total_steps=100)
w = adaptive.get_weights(200)
total = w.alpha + w.beta_cost + w.beta_energy + w.gamma_latency + w.gamma_power
assert abs(total - 1.0) < 0.01