mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
Merge pull request #109 from open-jarvis/fix/agent-config-defaults-103
fix: agent constructor defaults resolve from config.toml
This commit is contained in:
@@ -13,6 +13,7 @@ from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from openjarvis.core.config import load_config
|
||||
from openjarvis.core.events import EventBus, EventType
|
||||
from openjarvis.core.types import Conversation, Message, Role, ToolResult
|
||||
from openjarvis.engine._stubs import InferenceEngine
|
||||
@@ -63,17 +64,44 @@ class BaseAgent(ABC):
|
||||
model: str,
|
||||
*,
|
||||
bus: Optional[EventBus] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
prompt_builder: Optional[Any] = None,
|
||||
) -> None:
|
||||
self._engine = engine
|
||||
self._model = model
|
||||
self._bus = bus
|
||||
self._temperature = temperature
|
||||
self._max_tokens = max_tokens
|
||||
self._prompt_builder = prompt_builder
|
||||
|
||||
# Three-tier resolution: explicit arg > config > class default > hardcoded
|
||||
if temperature is not None and max_tokens is not None:
|
||||
self._temperature = temperature
|
||||
self._max_tokens = max_tokens
|
||||
else:
|
||||
try:
|
||||
cfg = load_config()
|
||||
self._temperature = (
|
||||
temperature
|
||||
if temperature is not None
|
||||
else cfg.intelligence.temperature
|
||||
)
|
||||
self._max_tokens = (
|
||||
max_tokens
|
||||
if max_tokens is not None
|
||||
else cfg.intelligence.max_tokens
|
||||
)
|
||||
except Exception:
|
||||
self._temperature = (
|
||||
temperature
|
||||
if temperature is not None
|
||||
else getattr(self, "_default_temperature", 0.7)
|
||||
)
|
||||
self._max_tokens = (
|
||||
max_tokens
|
||||
if max_tokens is not None
|
||||
else getattr(self, "_default_max_tokens", 1024)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Concrete helpers
|
||||
# ------------------------------------------------------------------
|
||||
@@ -197,7 +225,9 @@ class BaseAgent(ABC):
|
||||
"""
|
||||
# Full <think>...</think> blocks
|
||||
text = re.sub(
|
||||
r"<think>.*?</think>\s*", "", text,
|
||||
r"<think>.*?</think>\s*",
|
||||
"",
|
||||
text,
|
||||
flags=re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
# Leading content before a bare </think> (no opening tag)
|
||||
@@ -230,9 +260,9 @@ class ToolUsingAgent(BaseAgent):
|
||||
*,
|
||||
tools: Optional[List["BaseTool"]] = None, # noqa: F821
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 10,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
loop_guard_config: Optional[Any] = None,
|
||||
capability_policy: Optional[Any] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
@@ -240,21 +270,33 @@ class ToolUsingAgent(BaseAgent):
|
||||
confirm_callback: Optional[Any] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, bus=bus,
|
||||
temperature=temperature, max_tokens=max_tokens,
|
||||
engine,
|
||||
model,
|
||||
bus=bus,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
from openjarvis.tools._stubs import ToolExecutor
|
||||
|
||||
self._tools = tools or []
|
||||
_aid = agent_id or getattr(self, "agent_id", "")
|
||||
self._executor = ToolExecutor(
|
||||
self._tools, bus=bus,
|
||||
self._tools,
|
||||
bus=bus,
|
||||
capability_policy=capability_policy,
|
||||
agent_id=_aid,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
self._max_turns = max_turns
|
||||
# Resolve max_turns: explicit arg > config > class default > 10
|
||||
if max_turns is not None:
|
||||
self._max_turns = max_turns
|
||||
else:
|
||||
try:
|
||||
cfg = load_config()
|
||||
self._max_turns = cfg.agent.max_turns
|
||||
except Exception:
|
||||
self._max_turns = getattr(self, "_default_max_turns", 10)
|
||||
|
||||
# Loop guard
|
||||
self._loop_guard = None
|
||||
|
||||
@@ -55,6 +55,8 @@ class ClaudeCodeAgent(BaseAgent):
|
||||
|
||||
agent_id = "claude_code"
|
||||
accepts_tools = False
|
||||
_default_temperature = 0.7
|
||||
_default_max_tokens = 1024
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -62,8 +64,8 @@ class ClaudeCodeAgent(BaseAgent):
|
||||
model: str,
|
||||
*,
|
||||
bus: Optional[EventBus] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
api_key: str = "",
|
||||
workspace: str = "",
|
||||
session_id: str = "",
|
||||
@@ -72,8 +74,11 @@ class ClaudeCodeAgent(BaseAgent):
|
||||
timeout: int = 300,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, bus=bus,
|
||||
temperature=temperature, max_tokens=max_tokens,
|
||||
engine,
|
||||
model,
|
||||
bus=bus,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
self._api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
|
||||
self._workspace = workspace or os.getcwd()
|
||||
@@ -178,7 +183,8 @@ class ClaudeCodeAgent(BaseAgent):
|
||||
stderr = proc.stderr.strip() if proc.stderr else "Unknown error"
|
||||
logger.error(
|
||||
"claude_code_runner exited with code %d: %s",
|
||||
proc.returncode, stderr,
|
||||
proc.returncode,
|
||||
stderr,
|
||||
)
|
||||
self._emit_turn_end(turns=1, error=True)
|
||||
return AgentResult(
|
||||
@@ -217,7 +223,7 @@ class ClaudeCodeAgent(BaseAgent):
|
||||
# No sentinels -- treat entire stdout as plain content
|
||||
return stdout.strip(), [], {}
|
||||
|
||||
json_str = stdout[start + len(_OUTPUT_START):end].strip()
|
||||
json_str = stdout[start + len(_OUTPUT_START) : end].strip()
|
||||
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
|
||||
@@ -84,6 +84,9 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
|
||||
agent_id = "monitor_operative"
|
||||
accepts_tools = True
|
||||
_default_temperature = 0.3
|
||||
_default_max_tokens = 4096
|
||||
_default_max_turns = 25
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,9 +95,9 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
*,
|
||||
tools: Optional[List[BaseTool]] = None,
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 25,
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 4096,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
# Strategy parameters
|
||||
memory_extraction: str = "causality_graph",
|
||||
@@ -110,10 +113,15 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, tools=tools, bus=bus,
|
||||
max_turns=max_turns, temperature=temperature,
|
||||
engine,
|
||||
model,
|
||||
tools=tools,
|
||||
bus=bus,
|
||||
max_turns=max_turns,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
interactive=interactive, confirm_callback=confirm_callback,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
# Validate strategies
|
||||
if memory_extraction not in VALID_MEMORY_EXTRACTION:
|
||||
@@ -191,7 +199,8 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
|
||||
# 4. Build messages
|
||||
messages = self._build_operative_messages(
|
||||
input, context,
|
||||
input,
|
||||
context,
|
||||
system_prompt=system_prompt,
|
||||
session_messages=session_messages,
|
||||
)
|
||||
@@ -238,18 +247,21 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
]
|
||||
|
||||
# Append assistant message with tool calls
|
||||
messages.append(Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
)
|
||||
|
||||
# Execute each tool
|
||||
for tc in tool_calls:
|
||||
# Loop guard check
|
||||
if self._loop_guard:
|
||||
verdict = self._loop_guard.check_call(
|
||||
tc.name, tc.arguments,
|
||||
tc.name,
|
||||
tc.arguments,
|
||||
)
|
||||
if verdict.blocked:
|
||||
tool_result = ToolResult(
|
||||
@@ -258,12 +270,14 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
success=False,
|
||||
)
|
||||
all_tool_results.append(tool_result)
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
tool_result = self._executor.execute(tc)
|
||||
@@ -279,18 +293,21 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
except (json.JSONDecodeError, TypeError) as exc:
|
||||
logger.debug(
|
||||
"Failed to parse tool call arguments"
|
||||
" for state tracking: %s", exc,
|
||||
" for state tracking: %s",
|
||||
exc,
|
||||
)
|
||||
|
||||
# Compress observation if strategy requires it
|
||||
observation_content = self._compress_observation(tool_result.content)
|
||||
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=observation_content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=observation_content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
|
||||
# Extract and store findings based on memory strategy
|
||||
self._extract_and_store(tc.name, tool_result.content)
|
||||
@@ -298,7 +315,9 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
# Max turns exceeded
|
||||
self._save_session(input, content)
|
||||
return self._max_turns_result(
|
||||
all_tool_results, turns, content=content,
|
||||
all_tool_results,
|
||||
turns,
|
||||
content=content,
|
||||
metadata=total_usage,
|
||||
)
|
||||
|
||||
@@ -345,6 +364,7 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
if not self._tools:
|
||||
return ""
|
||||
from openjarvis.tools._stubs import build_tool_descriptions
|
||||
|
||||
return build_tool_descriptions(self._tools)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -458,11 +478,13 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
self._memory_backend.store(key, value)
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
"Failed to store causality relation in memory: %s", exc,
|
||||
"Failed to store causality relation in memory: %s",
|
||||
exc,
|
||||
)
|
||||
except (json.JSONDecodeError, Exception):
|
||||
logger.debug(
|
||||
"Causality extraction failed for tool %s output", tool_name,
|
||||
"Causality extraction failed for tool %s output",
|
||||
tool_name,
|
||||
)
|
||||
|
||||
def _store_scratchpad(self, tool_name: str, content: str) -> None:
|
||||
@@ -503,7 +525,8 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
"Failed to store structured data for tool %s: %s",
|
||||
tool_name, exc,
|
||||
tool_name,
|
||||
exc,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -557,10 +580,12 @@ class MonitorOperativeAgent(ToolUsingAgent):
|
||||
session_id = f"monitor_operative:{self._operator_id}"
|
||||
try:
|
||||
self._session_store.save_message(
|
||||
session_id, {"role": "user", "content": input_text},
|
||||
session_id,
|
||||
{"role": "user", "content": input_text},
|
||||
)
|
||||
self._session_store.save_message(
|
||||
session_id, {"role": "assistant", "content": response},
|
||||
session_id,
|
||||
{"role": "assistant", "content": response},
|
||||
)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
|
||||
@@ -54,6 +54,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
"""Native CodeAct agent -- generates and executes Python code."""
|
||||
|
||||
agent_id = "native_openhands"
|
||||
_default_temperature = 0.7
|
||||
_default_max_tokens = 2048
|
||||
_default_max_turns = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -62,17 +65,22 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
*,
|
||||
tools: Optional[List[BaseTool]] = None,
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 3,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2048,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
interactive: bool = False,
|
||||
confirm_callback=None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, tools=tools, bus=bus,
|
||||
max_turns=max_turns, temperature=temperature,
|
||||
engine,
|
||||
model,
|
||||
tools=tools,
|
||||
bus=bus,
|
||||
max_turns=max_turns,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
interactive=interactive, confirm_callback=confirm_callback,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -93,9 +101,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
content = WebSearchTool._fetch_url(url, max_chars=4000)
|
||||
header = f"\n\n--- Content from {url} ---\n"
|
||||
footer = "\n--- End of content ---\n"
|
||||
expanded = text.replace(
|
||||
url, f"{header}{content}{footer}"
|
||||
)
|
||||
expanded = text.replace(url, f"{header}{content}{footer}")
|
||||
return expanded, True
|
||||
except Exception:
|
||||
return text, False
|
||||
@@ -121,9 +127,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
messages[i] = Message(
|
||||
role=Role.USER,
|
||||
content=(
|
||||
truncated
|
||||
+ "\n\n[Input truncated"
|
||||
" to fit context window]"
|
||||
truncated + "\n\n[Input truncated to fit context window]"
|
||||
),
|
||||
)
|
||||
break
|
||||
@@ -135,7 +139,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
# Remove Action: ... Action Input: ... blocks
|
||||
text = re.sub(
|
||||
r"Action:\s*.+?(?:Action Input:\s*.+?)?(?=\n\n|\Z)",
|
||||
"", text, flags=re.DOTALL | re.IGNORECASE,
|
||||
"",
|
||||
text,
|
||||
flags=re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
# Remove <tool_call>...</tool_call> or </tool_name> blocks
|
||||
text = re.sub(r"<tool_call>.*?</\w+>", "", text, flags=re.DOTALL)
|
||||
@@ -242,7 +248,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
usage = result.get("usage", {})
|
||||
self._emit_turn_end(turns=1)
|
||||
return AgentResult(
|
||||
content=content, tool_results=[], turns=1,
|
||||
content=content,
|
||||
tool_results=[],
|
||||
turns=1,
|
||||
metadata={
|
||||
"prompt_tokens": usage.get("prompt_tokens", 0),
|
||||
"completion_tokens": usage.get("completion_tokens", 0),
|
||||
@@ -258,10 +266,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
"Please try a shorter message."
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
"The model returned an error: "
|
||||
+ error_str
|
||||
)
|
||||
error_msg = "The model returned an error: " + error_str
|
||||
self._emit_turn_end(turns=1, error=True)
|
||||
return AgentResult(
|
||||
content=error_msg,
|
||||
@@ -358,7 +363,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
|
||||
content = self._strip_tool_call_text(content)
|
||||
self._emit_turn_end(turns=turns)
|
||||
return AgentResult(
|
||||
content=content, tool_results=all_tool_results, turns=turns,
|
||||
content=content,
|
||||
tool_results=all_tool_results,
|
||||
turns=turns,
|
||||
metadata=total_usage,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ class NativeReActAgent(ToolUsingAgent):
|
||||
"""ReAct agent: Thought -> Action -> Observation loop."""
|
||||
|
||||
agent_id = "native_react"
|
||||
_default_temperature = 0.7
|
||||
_default_max_tokens = 1024
|
||||
_default_max_turns = 10
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -44,17 +47,22 @@ class NativeReActAgent(ToolUsingAgent):
|
||||
*,
|
||||
tools: Optional[List[BaseTool]] = None,
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 10,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
interactive: bool = False,
|
||||
confirm_callback=None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, tools=tools, bus=bus,
|
||||
max_turns=max_turns, temperature=temperature,
|
||||
engine,
|
||||
model,
|
||||
tools=tools,
|
||||
bus=bus,
|
||||
max_turns=max_turns,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
interactive=interactive, confirm_callback=confirm_callback,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
|
||||
def _parse_response(self, text: str) -> dict:
|
||||
@@ -161,7 +169,8 @@ class NativeReActAgent(ToolUsingAgent):
|
||||
# Loop guard check before execution
|
||||
if self._loop_guard:
|
||||
verdict = self._loop_guard.check_call(
|
||||
tool_call.name, tool_call.arguments,
|
||||
tool_call.name,
|
||||
tool_call.arguments,
|
||||
)
|
||||
if verdict.blocked:
|
||||
tool_result = ToolResult(
|
||||
|
||||
@@ -25,6 +25,8 @@ class OpenHandsAgent(BaseAgent):
|
||||
"""
|
||||
|
||||
agent_id = "openhands"
|
||||
_default_temperature = 0.7
|
||||
_default_max_tokens = 1024
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -32,14 +34,17 @@ class OpenHandsAgent(BaseAgent):
|
||||
model: str,
|
||||
*,
|
||||
bus: Optional[EventBus] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
workspace: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, bus=bus,
|
||||
temperature=temperature, max_tokens=max_tokens,
|
||||
engine,
|
||||
model,
|
||||
bus=bus,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
self._workspace = workspace or os.getcwd()
|
||||
self._api_key = api_key or os.environ.get("LLM_API_KEY", "")
|
||||
|
||||
@@ -38,6 +38,9 @@ class OperativeAgent(ToolUsingAgent):
|
||||
|
||||
agent_id = "operative"
|
||||
accepts_tools = True
|
||||
_default_temperature = 0.3
|
||||
_default_max_tokens = 2048
|
||||
_default_max_turns = 20
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -46,9 +49,9 @@ class OperativeAgent(ToolUsingAgent):
|
||||
*,
|
||||
tools: Optional[List[BaseTool]] = None,
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 20,
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 2048,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
operator_id: Optional[str] = None,
|
||||
session_store: Optional[Any] = None,
|
||||
@@ -58,10 +61,15 @@ class OperativeAgent(ToolUsingAgent):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, tools=tools, bus=bus,
|
||||
max_turns=max_turns, temperature=temperature,
|
||||
engine,
|
||||
model,
|
||||
tools=tools,
|
||||
bus=bus,
|
||||
max_turns=max_turns,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
interactive=interactive, confirm_callback=confirm_callback,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
self._system_prompt = system_prompt or ""
|
||||
self._operator_id = operator_id
|
||||
@@ -94,7 +102,9 @@ class OperativeAgent(ToolUsingAgent):
|
||||
|
||||
# 4. Build messages
|
||||
messages = self._build_operative_messages(
|
||||
input, context, system_prompt=system_prompt,
|
||||
input,
|
||||
context,
|
||||
system_prompt=system_prompt,
|
||||
session_messages=session_messages,
|
||||
)
|
||||
|
||||
@@ -140,11 +150,13 @@ class OperativeAgent(ToolUsingAgent):
|
||||
for i, tc in enumerate(raw_tool_calls)
|
||||
]
|
||||
|
||||
messages.append(Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
)
|
||||
|
||||
for tc in tool_calls:
|
||||
# Loop guard check
|
||||
@@ -157,12 +169,14 @@ class OperativeAgent(ToolUsingAgent):
|
||||
success=False,
|
||||
)
|
||||
all_tool_results.append(tool_result)
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
tool_result = self._executor.execute(tc)
|
||||
@@ -178,12 +192,14 @@ class OperativeAgent(ToolUsingAgent):
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Max turns exceeded
|
||||
self._save_session(input, content)
|
||||
@@ -274,10 +290,12 @@ class OperativeAgent(ToolUsingAgent):
|
||||
session_id = f"operator:{self._operator_id}"
|
||||
try:
|
||||
self._session_store.save_message(
|
||||
session_id, {"role": "user", "content": input_text},
|
||||
session_id,
|
||||
{"role": "user", "content": input_text},
|
||||
)
|
||||
self._session_store.save_message(
|
||||
session_id, {"role": "assistant", "content": response},
|
||||
session_id,
|
||||
{"role": "assistant", "content": response},
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Could not save session for operator %s", self._operator_id)
|
||||
|
||||
@@ -41,6 +41,9 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
"""
|
||||
|
||||
agent_id = "orchestrator"
|
||||
_default_temperature = 0.7
|
||||
_default_max_tokens = 1024
|
||||
_default_max_turns = 10
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -49,9 +52,9 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
*,
|
||||
tools: Optional[List[BaseTool]] = None,
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 10,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
mode: str = "function_calling",
|
||||
system_prompt: Optional[str] = None,
|
||||
parallel_tools: bool = True,
|
||||
@@ -59,10 +62,15 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
confirm_callback=None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, tools=tools, bus=bus,
|
||||
max_turns=max_turns, temperature=temperature,
|
||||
engine,
|
||||
model,
|
||||
tools=tools,
|
||||
bus=bus,
|
||||
max_turns=max_turns,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
interactive=interactive, confirm_callback=confirm_callback,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
self._mode = mode
|
||||
self._system_prompt = system_prompt
|
||||
@@ -97,6 +105,7 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
from openjarvis.learning.intelligence.orchestrator.prompt_registry import (
|
||||
build_system_prompt,
|
||||
)
|
||||
|
||||
sys_prompt = build_system_prompt(tools=self._tools)
|
||||
|
||||
messages = self._build_messages(input, context, system_prompt=sys_prompt)
|
||||
@@ -126,9 +135,7 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
|
||||
# TOOL -> execute
|
||||
if parsed["tool"]:
|
||||
messages.append(
|
||||
Message(role=Role.ASSISTANT, content=content)
|
||||
)
|
||||
messages.append(Message(role=Role.ASSISTANT, content=content))
|
||||
|
||||
tool_call = ToolCall(
|
||||
id=f"orch_{turns}",
|
||||
@@ -138,12 +145,8 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
tool_result = self._executor.execute(tool_call)
|
||||
all_tool_results.append(tool_result)
|
||||
|
||||
observation = (
|
||||
f"Observation: {tool_result.content}"
|
||||
)
|
||||
messages.append(
|
||||
Message(role=Role.USER, content=observation)
|
||||
)
|
||||
observation = f"Observation: {tool_result.content}"
|
||||
messages.append(Message(role=Role.USER, content=observation))
|
||||
continue
|
||||
|
||||
# Neither -> treat content as final answer
|
||||
@@ -184,9 +187,7 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
result["final_answer"] = final_match.group(1).strip()
|
||||
return result
|
||||
|
||||
tool_match = re.search(
|
||||
r"TOOL:\s*(.+)", text, re.IGNORECASE
|
||||
)
|
||||
tool_match = re.search(r"TOOL:\s*(.+)", text, re.IGNORECASE)
|
||||
if tool_match:
|
||||
result["tool"] = tool_match.group(1).strip()
|
||||
|
||||
@@ -271,11 +272,13 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
]
|
||||
|
||||
# Append assistant message with tool calls
|
||||
messages.append(Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
)
|
||||
|
||||
# Execute each tool (with loop guard check) and append results
|
||||
if self._parallel_tools and len(tool_calls) > 1:
|
||||
@@ -283,7 +286,8 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
def _exec_tool(tc: ToolCall) -> tuple:
|
||||
if self._loop_guard:
|
||||
verdict = self._loop_guard.check_call(
|
||||
tc.name, tc.arguments,
|
||||
tc.name,
|
||||
tc.arguments,
|
||||
)
|
||||
if verdict.blocked:
|
||||
return tc, ToolResult(
|
||||
@@ -296,10 +300,7 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=len(tool_calls),
|
||||
) as pool:
|
||||
futures = {
|
||||
pool.submit(_exec_tool, tc): tc
|
||||
for tc in tool_calls
|
||||
}
|
||||
futures = {pool.submit(_exec_tool, tc): tc for tc in tool_calls}
|
||||
results_map: dict[int, tuple] = {}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
tc_orig = futures[future]
|
||||
@@ -309,19 +310,22 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
for tc in tool_calls:
|
||||
_, tool_result = results_map[id(tc)]
|
||||
all_tool_results.append(tool_result)
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Sequential execution
|
||||
for tc in tool_calls:
|
||||
# Loop guard check before execution
|
||||
if self._loop_guard:
|
||||
verdict = self._loop_guard.check_call(
|
||||
tc.name, tc.arguments,
|
||||
tc.name,
|
||||
tc.arguments,
|
||||
)
|
||||
if verdict.blocked:
|
||||
tool_result = ToolResult(
|
||||
@@ -330,24 +334,28 @@ class OrchestratorAgent(ToolUsingAgent):
|
||||
success=False,
|
||||
)
|
||||
all_tool_results.append(tool_result)
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
tool_result = self._executor.execute(tc)
|
||||
all_tool_results.append(tool_result)
|
||||
|
||||
# Append tool response message
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
|
||||
# Max turns exceeded
|
||||
final_content = self._strip_think_tags(content) if content else ""
|
||||
|
||||
@@ -35,8 +35,8 @@ RLM_SYSTEM_PROMPT = (
|
||||
"final answer.\n"
|
||||
"- `FINAL_VAR(var_name: str)` — Terminate and return the "
|
||||
"value of variable `var_name`.\n"
|
||||
"- `answer` dict — Set `answer[\"value\"] = ...` and "
|
||||
"`answer[\"ready\"] = True` to terminate.\n\n"
|
||||
'- `answer` dict — Set `answer["value"] = ...` and '
|
||||
'`answer["ready"] = True` to terminate.\n\n'
|
||||
"{tool_section}"
|
||||
"## Available Modules\n\n"
|
||||
"json, re, math, collections, itertools, functools, "
|
||||
@@ -52,7 +52,7 @@ RLM_SYSTEM_PROMPT = (
|
||||
"and use `llm_query()` on each chunk.\n"
|
||||
"3. Combine sub-results programmatically.\n"
|
||||
"4. When you have the final answer, call "
|
||||
"`FINAL(answer_value)` or `FINAL_VAR(\"var_name\")`.\n"
|
||||
'`FINAL(answer_value)` or `FINAL_VAR("var_name")`.\n'
|
||||
"5. If you can answer directly without code, just respond "
|
||||
"with text (no code block).\n\n"
|
||||
"## Strategy Tips\n\n"
|
||||
@@ -84,6 +84,9 @@ class RLMAgent(ToolUsingAgent):
|
||||
"""
|
||||
|
||||
agent_id = "rlm"
|
||||
_default_temperature = 0.7
|
||||
_default_max_tokens = 2048
|
||||
_default_max_turns = 10
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,9 +95,9 @@ class RLMAgent(ToolUsingAgent):
|
||||
*,
|
||||
tools: Optional[List[BaseTool]] = None,
|
||||
bus: Optional[EventBus] = None,
|
||||
max_turns: int = 10,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2048,
|
||||
max_turns: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
sub_model: Optional[str] = None,
|
||||
sub_temperature: float = 0.3,
|
||||
sub_max_tokens: int = 1024,
|
||||
@@ -104,10 +107,15 @@ class RLMAgent(ToolUsingAgent):
|
||||
confirm_callback=None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
engine, model, tools=tools, bus=bus,
|
||||
max_turns=max_turns, temperature=temperature,
|
||||
engine,
|
||||
model,
|
||||
tools=tools,
|
||||
bus=bus,
|
||||
max_turns=max_turns,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
interactive=interactive, confirm_callback=confirm_callback,
|
||||
interactive=interactive,
|
||||
confirm_callback=confirm_callback,
|
||||
)
|
||||
# Override executor: RLM only creates one if tools are provided
|
||||
if not self._tools:
|
||||
@@ -168,7 +176,9 @@ class RLMAgent(ToolUsingAgent):
|
||||
|
||||
# Build conversation
|
||||
messages = self._build_messages(
|
||||
input, context, system_prompt=system_prompt,
|
||||
input,
|
||||
context,
|
||||
system_prompt=system_prompt,
|
||||
)
|
||||
|
||||
all_tool_results: list[ToolResult] = []
|
||||
@@ -233,9 +243,7 @@ class RLMAgent(ToolUsingAgent):
|
||||
# Feed output back as user message
|
||||
messages.append(Message(role=Role.ASSISTANT, content=content))
|
||||
feedback = (
|
||||
f"REPL Output: {output}"
|
||||
if output
|
||||
else "REPL Output: (no output)"
|
||||
f"REPL Output: {output}" if output else "REPL Output: (no output)"
|
||||
)
|
||||
messages.append(Message(role=Role.USER, content=feedback))
|
||||
|
||||
@@ -281,19 +289,23 @@ class RLMAgent(ToolUsingAgent):
|
||||
)
|
||||
for i, tc in enumerate(raw_tool_calls)
|
||||
]
|
||||
messages.append(Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
)
|
||||
for tc in tool_calls:
|
||||
tr = self._executor.execute(tc)
|
||||
messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tr.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
))
|
||||
messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tr.content,
|
||||
tool_call_id=tc.id,
|
||||
name=tc.name,
|
||||
)
|
||||
)
|
||||
followup = self._engine.generate(
|
||||
messages,
|
||||
model=self._sub_model,
|
||||
|
||||
@@ -7,6 +7,7 @@ found in the TOML file.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
@@ -59,7 +60,10 @@ def _run_cmd(cmd: list[str]) -> str:
|
||||
"""Run a command and return stripped stdout, or empty string on failure."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=10, # noqa: S603
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10, # noqa: S603
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
@@ -69,11 +73,13 @@ def _run_cmd(cmd: list[str]) -> str:
|
||||
def _detect_nvidia_gpu() -> Optional[GpuInfo]:
|
||||
if not shutil.which("nvidia-smi"):
|
||||
return None
|
||||
raw = _run_cmd([
|
||||
"nvidia-smi",
|
||||
"--query-gpu=name,memory.total,count",
|
||||
"--format=csv,noheader,nounits",
|
||||
])
|
||||
raw = _run_cmd(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=name,memory.total,count",
|
||||
"--format=csv,noheader,nounits",
|
||||
]
|
||||
)
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
@@ -117,6 +123,7 @@ def _detect_amd_gpu() -> Optional[GpuInfo]:
|
||||
try:
|
||||
allinfo_raw = _run_cmd(["rocm-smi", "--showallinfo"])
|
||||
import re
|
||||
|
||||
gpu_ids = set(re.findall(r"GPU\[(\d+)\]", allinfo_raw))
|
||||
if gpu_ids:
|
||||
count = len(gpu_ids)
|
||||
@@ -448,26 +455,26 @@ class IntelligenceConfig:
|
||||
|
||||
default_model: str = ""
|
||||
fallback_model: str = ""
|
||||
model_path: str = "" # Local weights (HF repo, GGUF file, etc.)
|
||||
checkpoint_path: str = "" # Checkpoint/adapter path
|
||||
quantization: str = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
|
||||
preferred_engine: str = "" # Override engine for this model (e.g., "vllm")
|
||||
provider: str = "" # local, openai, anthropic, google
|
||||
model_path: str = "" # Local weights (HF repo, GGUF file, etc.)
|
||||
checkpoint_path: str = "" # Checkpoint/adapter path
|
||||
quantization: str = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
|
||||
preferred_engine: str = "" # Override engine for this model (e.g., "vllm")
|
||||
provider: str = "" # local, openai, anthropic, google
|
||||
# Generation defaults (overridable per-call)
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 1024
|
||||
top_p: float = 0.9
|
||||
top_k: int = 40
|
||||
repetition_penalty: float = 1.0
|
||||
stop_sequences: str = "" # Comma-separated stop strings
|
||||
stop_sequences: str = "" # Comma-separated stop strings
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RoutingLearningConfig:
|
||||
"""Routing sub-policy config within Learning."""
|
||||
|
||||
policy: str = "heuristic" # heuristic | learned
|
||||
min_samples: int = 5 # Min traces before trusting learned routing
|
||||
policy: str = "heuristic" # heuristic | learned
|
||||
min_samples: int = 5 # Min traces before trusting learned routing
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -716,9 +723,9 @@ class AgentConfig:
|
||||
|
||||
default_agent: str = "simple"
|
||||
max_turns: int = 10
|
||||
tools: str = "" # comma-separated tool names
|
||||
objective: str = "" # concise purpose for routing/learning/docs
|
||||
system_prompt: str = "" # inline system prompt (takes precedence if set)
|
||||
tools: str = "" # comma-separated tool names
|
||||
objective: str = "" # concise purpose for routing/learning/docs
|
||||
system_prompt: str = "" # inline system prompt (takes precedence if set)
|
||||
system_prompt_path: str = "" # path to system prompt file (.txt, .md)
|
||||
context_from_memory: bool = True # inject relevant memory context into prompts
|
||||
|
||||
@@ -898,7 +905,7 @@ class BlueBubblesChannelConfig:
|
||||
class WhatsAppBaileysChannelConfig:
|
||||
"""Per-channel config for WhatsApp via Baileys protocol."""
|
||||
|
||||
auth_dir: str = "" # Defaults to ~/.openjarvis/whatsapp_auth
|
||||
auth_dir: str = "" # Defaults to ~/.openjarvis/whatsapp_auth
|
||||
assistant_name: str = "Jarvis"
|
||||
assistant_has_own_number: bool = False
|
||||
|
||||
@@ -1176,7 +1183,8 @@ def _migrate_toml_data(data: Dict[str, Any], cfg: "JarvisConfig") -> None:
|
||||
src = data.get(src_section, {})
|
||||
if isinstance(src, dict) and "context_injection" in src:
|
||||
data.setdefault("agent", {}).setdefault(
|
||||
"context_from_memory", src.pop("context_injection"),
|
||||
"context_from_memory",
|
||||
src.pop("context_injection"),
|
||||
)
|
||||
|
||||
if "tools" in data:
|
||||
@@ -1185,10 +1193,12 @@ def _migrate_toml_data(data: Dict[str, Any], cfg: "JarvisConfig") -> None:
|
||||
storage_sub = tools_data.get("storage", {})
|
||||
if isinstance(storage_sub, dict) and "context_injection" in storage_sub:
|
||||
data.setdefault("agent", {}).setdefault(
|
||||
"context_from_memory", storage_sub.pop("context_injection"),
|
||||
"context_from_memory",
|
||||
storage_sub.pop("context_injection"),
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def load_config(path: Optional[Path] = None) -> JarvisConfig:
|
||||
"""Detect hardware, build defaults, overlay TOML overrides.
|
||||
|
||||
@@ -1218,16 +1228,31 @@ def load_config(path: Optional[Path] = None) -> JarvisConfig:
|
||||
# All top-level sections — recursive _apply_toml_section handles
|
||||
# nested sub-configs (engine.ollama, learning.routing, channel.*, etc.)
|
||||
top_sections = (
|
||||
"engine", "intelligence", "learning", "agent",
|
||||
"server", "telemetry", "traces", "security",
|
||||
"channel", "tools", "sandbox", "scheduler",
|
||||
"workflow", "sessions", "a2a", "operators",
|
||||
"speech", "optimize", "agent_manager",
|
||||
"engine",
|
||||
"intelligence",
|
||||
"learning",
|
||||
"agent",
|
||||
"server",
|
||||
"telemetry",
|
||||
"traces",
|
||||
"security",
|
||||
"channel",
|
||||
"tools",
|
||||
"sandbox",
|
||||
"scheduler",
|
||||
"workflow",
|
||||
"sessions",
|
||||
"a2a",
|
||||
"operators",
|
||||
"speech",
|
||||
"optimize",
|
||||
"agent_manager",
|
||||
)
|
||||
for section_name in top_sections:
|
||||
if section_name in data:
|
||||
_apply_toml_section(
|
||||
getattr(cfg, section_name), data[section_name],
|
||||
getattr(cfg, section_name),
|
||||
data[section_name],
|
||||
)
|
||||
|
||||
# Memory: accept [memory] (old) → maps to tools.storage
|
||||
@@ -1248,13 +1273,8 @@ def generate_minimal_toml(hw: HardwareInfo, engine: str | None = None) -> str:
|
||||
model = recommend_model(hw, engine)
|
||||
gpu_comment = ""
|
||||
if hw.gpu:
|
||||
mem_label = (
|
||||
"unified memory" if hw.gpu.vendor == "apple" else "VRAM"
|
||||
)
|
||||
gpu_comment = (
|
||||
f"\n# GPU: {hw.gpu.name}"
|
||||
f" ({hw.gpu.vram_gb} GB {mem_label})"
|
||||
)
|
||||
mem_label = "unified memory" if hw.gpu.vendor == "apple" else "VRAM"
|
||||
gpu_comment = f"\n# GPU: {hw.gpu.name} ({hw.gpu.vram_gb} GB {mem_label})"
|
||||
return f"""\
|
||||
# OpenJarvis configuration
|
||||
# Hardware: {hw.cpu_brand} ({hw.cpu_count} cores, {hw.ram_gb} GB RAM){gpu_comment}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Tests for agent constructor config-based default resolution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openjarvis.agents._stubs import AgentResult, BaseAgent, ToolUsingAgent
|
||||
|
||||
|
||||
class _TestAgent(BaseAgent):
|
||||
agent_id = "test_cfg"
|
||||
|
||||
def run(self, input, context=None, **kwargs):
|
||||
return AgentResult(content="ok", turns=1)
|
||||
|
||||
|
||||
class _TestToolAgent(ToolUsingAgent):
|
||||
agent_id = "test_cfg_tool"
|
||||
|
||||
def run(self, input, context=None, **kwargs):
|
||||
return AgentResult(content="ok", turns=1)
|
||||
|
||||
|
||||
class _TestToolAgentWithDefaults(ToolUsingAgent):
|
||||
"""Agent with class-level defaults (like MonitorOperativeAgent)."""
|
||||
|
||||
agent_id = "test_cfg_tool_defaults"
|
||||
_default_temperature = 0.3
|
||||
_default_max_tokens = 4096
|
||||
_default_max_turns = 25
|
||||
|
||||
def run(self, input, context=None, **kwargs):
|
||||
return AgentResult(content="ok", turns=1)
|
||||
|
||||
|
||||
class TestBaseAgentConfigResolution:
|
||||
"""BaseAgent resolves None params from config.intelligence."""
|
||||
|
||||
def test_none_temperature_reads_config(self):
|
||||
"""When temperature is not passed, it should come from config."""
|
||||
engine = MagicMock()
|
||||
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
|
||||
mock_cfg.return_value.intelligence.temperature = 0.2
|
||||
mock_cfg.return_value.intelligence.max_tokens = 512
|
||||
agent = _TestAgent(engine, "m")
|
||||
assert agent._temperature == 0.2
|
||||
|
||||
def test_none_max_tokens_reads_config(self):
|
||||
"""When max_tokens is not passed, it should come from config."""
|
||||
engine = MagicMock()
|
||||
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
|
||||
mock_cfg.return_value.intelligence.temperature = 0.7
|
||||
mock_cfg.return_value.intelligence.max_tokens = 512
|
||||
agent = _TestAgent(engine, "m")
|
||||
assert agent._max_tokens == 512
|
||||
|
||||
def test_explicit_temperature_overrides_config(self):
|
||||
"""Caller-provided temperature takes precedence over config."""
|
||||
engine = MagicMock()
|
||||
agent = _TestAgent(engine, "m", temperature=0.9)
|
||||
assert agent._temperature == 0.9
|
||||
|
||||
def test_explicit_max_tokens_overrides_config(self):
|
||||
"""Caller-provided max_tokens takes precedence over config."""
|
||||
engine = MagicMock()
|
||||
agent = _TestAgent(engine, "m", max_tokens=2048)
|
||||
assert agent._max_tokens == 2048
|
||||
|
||||
def test_partial_override_temperature_only(self):
|
||||
"""Providing only temperature still reads max_tokens from config."""
|
||||
engine = MagicMock()
|
||||
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
|
||||
mock_cfg.return_value.intelligence.temperature = 0.2
|
||||
mock_cfg.return_value.intelligence.max_tokens = 512
|
||||
agent = _TestAgent(engine, "m", temperature=0.9)
|
||||
assert agent._temperature == 0.9
|
||||
assert agent._max_tokens == 512
|
||||
|
||||
def test_config_load_failure_uses_hardcoded_fallback(self):
|
||||
"""When config loading fails, fall back to class defaults then 0.7/1024."""
|
||||
engine = MagicMock()
|
||||
with patch(
|
||||
"openjarvis.agents._stubs.load_config",
|
||||
side_effect=Exception("boom"),
|
||||
):
|
||||
agent = _TestAgent(engine, "m")
|
||||
assert agent._temperature == 0.7
|
||||
assert agent._max_tokens == 1024
|
||||
|
||||
|
||||
class TestToolUsingAgentConfigResolution:
|
||||
"""ToolUsingAgent resolves None max_turns from config.agent."""
|
||||
|
||||
def test_none_max_turns_reads_config(self):
|
||||
"""When max_turns is not passed, it should come from config."""
|
||||
engine = MagicMock()
|
||||
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
|
||||
mock_cfg.return_value.intelligence.temperature = 0.7
|
||||
mock_cfg.return_value.intelligence.max_tokens = 1024
|
||||
mock_cfg.return_value.agent.max_turns = 15
|
||||
agent = _TestToolAgent(engine, "m")
|
||||
assert agent._max_turns == 15
|
||||
|
||||
def test_explicit_max_turns_overrides_config(self):
|
||||
"""Caller-provided max_turns takes precedence over config."""
|
||||
engine = MagicMock()
|
||||
agent = _TestToolAgent(engine, "m", max_turns=5)
|
||||
assert agent._max_turns == 5
|
||||
|
||||
def test_temperature_and_max_tokens_forwarded_to_base(self):
|
||||
"""ToolUsingAgent also resolves temperature/max_tokens from config."""
|
||||
engine = MagicMock()
|
||||
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
|
||||
mock_cfg.return_value.intelligence.temperature = 0.1
|
||||
mock_cfg.return_value.intelligence.max_tokens = 256
|
||||
mock_cfg.return_value.agent.max_turns = 10
|
||||
agent = _TestToolAgent(engine, "m")
|
||||
assert agent._temperature == 0.1
|
||||
assert agent._max_tokens == 256
|
||||
|
||||
def test_config_load_failure_max_turns_fallback(self):
|
||||
"""When config loading fails, max_turns falls back to class default then 10."""
|
||||
engine = MagicMock()
|
||||
with patch(
|
||||
"openjarvis.agents._stubs.load_config",
|
||||
side_effect=Exception("boom"),
|
||||
):
|
||||
agent = _TestToolAgent(engine, "m")
|
||||
assert agent._max_turns == 10
|
||||
|
||||
|
||||
class TestClassLevelDefaults:
|
||||
"""Agents with class-level _default_* attributes use them as fallback."""
|
||||
|
||||
def test_class_default_used_when_config_fails(self):
|
||||
"""Agent class defaults are used when config is unavailable."""
|
||||
engine = MagicMock()
|
||||
with patch(
|
||||
"openjarvis.agents._stubs.load_config",
|
||||
side_effect=Exception("boom"),
|
||||
):
|
||||
agent = _TestToolAgentWithDefaults(engine, "m")
|
||||
assert agent._temperature == 0.3
|
||||
assert agent._max_tokens == 4096
|
||||
assert agent._max_turns == 25
|
||||
|
||||
def test_config_overrides_class_default(self):
|
||||
"""User config takes precedence over class-level defaults."""
|
||||
engine = MagicMock()
|
||||
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
|
||||
mock_cfg.return_value.intelligence.temperature = 0.5
|
||||
mock_cfg.return_value.intelligence.max_tokens = 2048
|
||||
mock_cfg.return_value.agent.max_turns = 12
|
||||
agent = _TestToolAgentWithDefaults(engine, "m")
|
||||
assert agent._temperature == 0.5
|
||||
assert agent._max_tokens == 2048
|
||||
assert agent._max_turns == 12
|
||||
|
||||
def test_explicit_overrides_everything(self):
|
||||
"""Caller-provided values override both config and class defaults."""
|
||||
engine = MagicMock()
|
||||
agent = _TestToolAgentWithDefaults(
|
||||
engine,
|
||||
"m",
|
||||
temperature=0.9,
|
||||
max_tokens=100,
|
||||
max_turns=2,
|
||||
)
|
||||
assert agent._temperature == 0.9
|
||||
assert agent._max_tokens == 100
|
||||
assert agent._max_turns == 2
|
||||
Reference in New Issue
Block a user