diff --git a/src/openjarvis/agents/_stubs.py b/src/openjarvis/agents/_stubs.py index 9a91c90b..66b8755e 100644 --- a/src/openjarvis/agents/_stubs.py +++ b/src/openjarvis/agents/_stubs.py @@ -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 ... blocks text = re.sub( - r".*?\s*", "", text, + r".*?\s*", + "", + text, flags=re.DOTALL | re.IGNORECASE, ) # Leading content before a bare (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 diff --git a/src/openjarvis/agents/claude_code.py b/src/openjarvis/agents/claude_code.py index 59aaefc1..8fb77473 100644 --- a/src/openjarvis/agents/claude_code.py +++ b/src/openjarvis/agents/claude_code.py @@ -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) diff --git a/src/openjarvis/agents/monitor_operative.py b/src/openjarvis/agents/monitor_operative.py index a764ca35..3965e3d4 100644 --- a/src/openjarvis/agents/monitor_operative.py +++ b/src/openjarvis/agents/monitor_operative.py @@ -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( diff --git a/src/openjarvis/agents/native_openhands.py b/src/openjarvis/agents/native_openhands.py index 46570d53..cf5579cd 100644 --- a/src/openjarvis/agents/native_openhands.py +++ b/src/openjarvis/agents/native_openhands.py @@ -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 ... or blocks text = re.sub(r".*?", "", 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, ) diff --git a/src/openjarvis/agents/native_react.py b/src/openjarvis/agents/native_react.py index cfcc5023..75cd8efc 100644 --- a/src/openjarvis/agents/native_react.py +++ b/src/openjarvis/agents/native_react.py @@ -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( diff --git a/src/openjarvis/agents/openhands.py b/src/openjarvis/agents/openhands.py index 87dad601..4432b655 100644 --- a/src/openjarvis/agents/openhands.py +++ b/src/openjarvis/agents/openhands.py @@ -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", "") diff --git a/src/openjarvis/agents/operative.py b/src/openjarvis/agents/operative.py index 7f88f840..02c1d725 100644 --- a/src/openjarvis/agents/operative.py +++ b/src/openjarvis/agents/operative.py @@ -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) diff --git a/src/openjarvis/agents/orchestrator.py b/src/openjarvis/agents/orchestrator.py index ffbff90e..cca03d2c 100644 --- a/src/openjarvis/agents/orchestrator.py +++ b/src/openjarvis/agents/orchestrator.py @@ -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 "" diff --git a/src/openjarvis/agents/rlm.py b/src/openjarvis/agents/rlm.py index 0cc6e3a4..cd8c0c37 100644 --- a/src/openjarvis/agents/rlm.py +++ b/src/openjarvis/agents/rlm.py @@ -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, diff --git a/src/openjarvis/core/config.py b/src/openjarvis/core/config.py index 5ca8fa77..6c6672f2 100644 --- a/src/openjarvis/core/config.py +++ b/src/openjarvis/core/config.py @@ -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} diff --git a/tests/agents/test_config_defaults.py b/tests/agents/test_config_defaults.py new file mode 100644 index 00000000..d3f2b117 --- /dev/null +++ b/tests/agents/test_config_defaults.py @@ -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