mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
* 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>
144 lines
5.0 KiB
Python
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
|