mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
Major changes across parallel sessions: - Add orchestrator SFT & GRPO training subpackage (learning/orchestrator/) with episode types, multi-objective reward, prompt registry, policy model, RL environment, and registered learning policies - Add structured THOUGHT/TOOL/INPUT/FINAL_ANSWER mode to OrchestratorAgent - Add 15 channel backends (Discord, Slack, Telegram, Email, Webhook, IRC, Matrix, Teams, WhatsApp, Signal, Mattermost, BlueBubbles, Feishu, Google Chat, Webchat) with channel tools and config - Add LiteLLM engine backend for unified LLM provider access - Add RLM agent and REPL tool - Remove ToolLearningPolicy — learning taxonomy now only targets Intelligence (LM weights/routing) and Agents (logic/ICL/tool strategies) - Rename SFTPolicy to SFTRouterPolicy (backward-compat alias kept) - Remove OpenClaw agent infrastructure (openclaw*.py, openclaw_bridge.py) - Fix async streaming tests (asyncio.run vs deprecated get_event_loop) - Fix server channel route tests (pytest.importorskip for optional fastapi) - Track uv.lock for reproducibility - Update CLAUDE.md and docs to reflect all changes 1676 tests pass, 37 skipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
129 lines
4.1 KiB
Python
129 lines
4.1 KiB
Python
"""Tests for orchestrator RL environment."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from openjarvis.core.types import ToolResult
|
|
from openjarvis.learning.orchestrator.environment import (
|
|
OrchestratorEnvironment,
|
|
)
|
|
from openjarvis.learning.orchestrator.types import OrchestratorAction
|
|
from openjarvis.tools._stubs import BaseTool, ToolSpec
|
|
|
|
# -- Mock tool ---------------------------------------------------------------
|
|
|
|
|
|
class _MockCalculator(BaseTool):
|
|
tool_id = "calculator"
|
|
|
|
@property
|
|
def spec(self) -> ToolSpec:
|
|
return ToolSpec(
|
|
name="calculator",
|
|
description="mock calculator",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"expression": {
|
|
"type": "string",
|
|
"description": "math expression",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
def execute(self, **params) -> ToolResult:
|
|
expr = params.get("expression", "")
|
|
try:
|
|
result = str(eval(expr)) # noqa: S307
|
|
except Exception as e:
|
|
return ToolResult(
|
|
tool_name="calculator",
|
|
content=f"Error: {e}",
|
|
success=False,
|
|
)
|
|
return ToolResult(
|
|
tool_name="calculator",
|
|
content=result,
|
|
success=True,
|
|
)
|
|
|
|
|
|
# -- Tests -------------------------------------------------------------------
|
|
|
|
|
|
class TestOrchestratorEnvironment:
|
|
def test_reset_creates_clean_state(self):
|
|
env = OrchestratorEnvironment(tools=[_MockCalculator()])
|
|
state = env.reset("What is 2+2?")
|
|
assert state.initial_prompt == "What is 2+2?"
|
|
assert state.num_turns() == 0
|
|
assert state.final_answer is None
|
|
|
|
def test_step_executes_tool(self):
|
|
env = OrchestratorEnvironment(tools=[_MockCalculator()])
|
|
state = env.reset("q")
|
|
action = OrchestratorAction(
|
|
thought="calc",
|
|
tool_name="calculator",
|
|
tool_input="2+2",
|
|
)
|
|
state, obs = env.step(state, action)
|
|
assert state.num_turns() == 1
|
|
assert obs.latency_seconds >= 0
|
|
|
|
def test_is_done_on_final_answer(self):
|
|
env = OrchestratorEnvironment(tools=[_MockCalculator()])
|
|
state = env.reset("q")
|
|
action = OrchestratorAction(
|
|
thought="done",
|
|
tool_name="calculator",
|
|
tool_input="2+2",
|
|
is_final_answer=True,
|
|
)
|
|
state, obs = env.step(state, action)
|
|
assert env.is_done(state) is True
|
|
|
|
def test_is_done_on_max_turns(self):
|
|
env = OrchestratorEnvironment(
|
|
tools=[_MockCalculator()], max_turns=2
|
|
)
|
|
state = env.reset("q")
|
|
for _ in range(2):
|
|
action = OrchestratorAction(
|
|
thought="go", tool_name="calculator", tool_input="1+1"
|
|
)
|
|
state, obs = env.step(state, action)
|
|
assert env.is_done(state) is True
|
|
|
|
def test_invalid_tool_raises(self):
|
|
env = OrchestratorEnvironment(tools=[_MockCalculator()])
|
|
state = env.reset("q")
|
|
action = OrchestratorAction(
|
|
thought="t", tool_name="nonexistent", tool_input="x"
|
|
)
|
|
with pytest.raises(ValueError, match="not available"):
|
|
env.step(state, action)
|
|
|
|
def test_max_turns_exceeded_raises(self):
|
|
env = OrchestratorEnvironment(
|
|
tools=[_MockCalculator()], max_turns=1
|
|
)
|
|
state = env.reset("q")
|
|
action = OrchestratorAction(
|
|
thought="go", tool_name="calculator", tool_input="1+1"
|
|
)
|
|
state, _ = env.step(state, action)
|
|
with pytest.raises(ValueError, match="exceeded"):
|
|
env.step(state, action)
|
|
|
|
def test_get_available_tools(self):
|
|
env = OrchestratorEnvironment(tools=[_MockCalculator()])
|
|
assert env.get_available_tools() == ["calculator"]
|
|
|
|
def test_not_done_initially(self):
|
|
env = OrchestratorEnvironment(tools=[_MockCalculator()])
|
|
state = env.reset("q")
|
|
assert env.is_done(state) is False
|