Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions spoon_ai/agents/spoon_react.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ async def run(
timeout: Optional[float] = None,
thinking: bool = False,
reasoning_effort: Optional[str] = None,
model: Optional[str] = None,
) -> str:
"""Ensure prompts reflect current tools before running."""
self._refresh_prompts()
Expand All @@ -207,4 +208,6 @@ async def run(
kwargs["thinking"] = True
if reasoning_effort is not None:
kwargs["reasoning_effort"] = reasoning_effort
if model is not None:
kwargs["model"] = model
return await super().run(**kwargs)
3 changes: 3 additions & 0 deletions spoon_ai/agents/spoon_react_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ async def run(
timeout: Optional[float] = None,
thinking: bool = False,
reasoning_effort: Optional[str] = None,
model: Optional[str] = None,
) -> str:
"""
Execute agent with per-turn auto skill activation.
Expand Down Expand Up @@ -125,6 +126,8 @@ async def _runner(req: Optional[str]) -> str:
kwargs["thinking"] = True
if reasoning_effort is not None:
kwargs["reasoning_effort"] = reasoning_effort
if model is not None:
kwargs["model"] = model
return await super(SpoonReactSkill, self).run(**kwargs)

return await self._run_with_auto_skills(request, _runner)
Expand Down
38 changes: 33 additions & 5 deletions spoon_ai/agents/toolcall.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def think(
self,
thinking: bool = False,
reasoning_effort: Optional[str] = None,
model: Optional[str] = None,
) -> bool:
self.last_reasoning_summary = None
last_role = getattr(self.memory.messages[-1], "role", None) if self.memory.messages else None
Expand Down Expand Up @@ -208,6 +209,8 @@ def convert_mcp_tool(tool: MCPTool) -> dict:
unique_tools_list,
llm_timeout,
thinking=thinking,
reasoning_effort=reasoning_effort,
model=model,
)
else:
# Fallback: direct LLM call without middleware
Expand All @@ -222,6 +225,8 @@ def convert_mcp_tool(tool: MCPTool) -> dict:
ask_tool_kwargs["thinking"] = True
if reasoning_effort:
ask_tool_kwargs["reasoning_effort"] = reasoning_effort
if model:
ask_tool_kwargs["model"] = model
response = await asyncio.wait_for(
self.llm.ask_tool(**ask_tool_kwargs),
timeout=llm_timeout,
Expand Down Expand Up @@ -423,6 +428,7 @@ async def run(
timeout: Optional[float] = None,
thinking: bool = False,
reasoning_effort: Optional[str] = None,
model: Optional[str] = None,
) -> str:
"""

Expand Down Expand Up @@ -527,7 +533,11 @@ async def run(
break

step_result = await asyncio.wait_for(
self.step(thinking=thinking, reasoning_effort=reasoning_effort),
self.step(
thinking=thinking,
reasoning_effort=reasoning_effort,
model=model,
),
timeout=step_timeout,
)
if await self.is_stuck():
Expand Down Expand Up @@ -560,7 +570,7 @@ async def run(
logger.info(f"Step {self.current_step}: {step_result}")

if self.current_step >= self.max_steps:
final_content = await self._maybe_finalize_after_tool_budget()
final_content = await self._maybe_finalize_after_tool_budget(model=model)
if final_content:
return final_content
results.append(f"Step {self.current_step}: Stuck in loop. Resetting state.")
Expand Down Expand Up @@ -599,11 +609,13 @@ async def step(
self,
thinking: bool = False,
reasoning_effort: Optional[str] = None,
model: Optional[str] = None,
) -> str:
"""Override the step method to handle finish_reason termination properly."""
should_act = await self.think(
thinking=thinking,
reasoning_effort=reasoning_effort,
model=model,
)
if not should_act:
if self.state == AgentState.FINISHED:
Expand Down Expand Up @@ -780,7 +792,11 @@ def consume_last_tool_error(self) -> Optional[str]:
self.last_tool_error = None
return err

async def _maybe_finalize_after_tool_budget(self) -> str:
async def _maybe_finalize_after_tool_budget(
self,
*,
model: Optional[str] = None,
) -> str:
"""Allow one final, tool-free summary turn after the last tool step."""
last_message = self.memory.messages[-1] if self.memory.messages else None
if getattr(last_message, "role", None) != "tool":
Expand All @@ -797,6 +813,7 @@ async def _maybe_finalize_after_tool_budget(self) -> str:
final_content = await self.llm.ask(
messages=self.memory.messages,
system_msg=self.system_prompt,
model=model,
)
final_content = (final_content or "").strip()
if not final_content:
Expand Down Expand Up @@ -840,6 +857,8 @@ async def _call_llm_with_middleware(
timeout: float,
*,
thinking: bool = False,
reasoning_effort: Optional[str] = None,
model: Optional[str] = None,
):
"""Call LLM through middleware pipeline for observability.

Expand All @@ -857,27 +876,36 @@ async def _call_llm_with_middleware(
)

# Create model request
extra_params: dict[str, Any] = {}
if thinking:
extra_params["thinking"] = thinking
if reasoning_effort:
extra_params["reasoning_effort"] = reasoning_effort
request = ModelRequest(
system_prompt=self.system_prompt,
messages=self.memory.messages,
tools=tools,
tool_choice=tool_choice,
runtime=runtime,
phase=AgentPhase.THINK,
extra_params={"thinking": thinking} if thinking else {},
model=model,
extra_params=extra_params,
)

# Define base handler that calls the actual LLM
async def base_handler(req: ModelRequest) -> ModelResponse:
# Call LLM directly
request_kwargs = dict(req.extra_params)
if req.model:
request_kwargs["model"] = req.model
llm_response = await asyncio.wait_for(
self.llm.ask_tool(
messages=req.messages,
system_msg=req.system_prompt,
tools=req.tools,
tool_choice=req.tool_choice,
output_queue=self.output_queue,
**req.extra_params,
**request_kwargs,
),
timeout=timeout,
)
Expand Down
65 changes: 46 additions & 19 deletions spoon_ai/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,22 +893,33 @@ def _normalize_tool_request_kwargs(self, kwargs: Dict[str, Any]) -> Dict[str, An

return normalized

async def ask(self, messages: List[Union[dict, Message]], system_msg: Optional[str] = None, output_queue: Optional[asyncio.Queue] = None) -> str:
async def ask(
self,
messages: List[Union[dict, Message]],
system_msg: Optional[str] = None,
output_queue: Optional[asyncio.Queue] = None,
*,
model: Optional[str] = None,
) -> str:
"""Ask method using the LLM manager architecture.

Automatically applies short-term memory strategy if enabled.
"""
effective_model = str(model or self.model_name or "").strip() or None
formatted_messages = self._format_messages(messages, system_msg)
messages_with_long_term, user_query = await self._inject_long_term_context(formatted_messages)
processed_messages = await self._apply_short_term_memory_strategy(
messages_with_long_term,
model=self.model_name,
model=effective_model,
)

response = await self.llm_manager.chat(
messages=processed_messages,
provider=self.llm_provider
)
request_kwargs: Dict[str, Any] = {
"messages": processed_messages,
"provider": self.llm_provider,
}
if effective_model:
request_kwargs["model"] = effective_model
response = await self.llm_manager.chat(**request_kwargs)

await self._store_long_term_memory(user_query, response.content)

Expand All @@ -921,11 +932,16 @@ async def ask_tool(self, messages: List[Union[dict, Message]], system_msg: Optio
"""
formatted_messages = self._format_messages(messages, system_msg)
messages_with_long_term, user_query = await self._inject_long_term_context(formatted_messages)
request_kwargs = self._normalize_tool_request_kwargs(kwargs)
effective_model = str(request_kwargs.get("model") or self.model_name or "").strip() or None
if effective_model:
request_kwargs["model"] = effective_model
else:
request_kwargs.pop("model", None)
processed_messages = await self._apply_short_term_memory_strategy(
messages_with_long_term,
model=self.model_name,
model=effective_model,
)
request_kwargs = self._normalize_tool_request_kwargs(kwargs)

response = await self.llm_manager.chat_with_tools(
messages=processed_messages,
Expand Down Expand Up @@ -1097,12 +1113,17 @@ async def astream(
**kwargs: Any,
) -> AsyncIterator[LLMResponseChunk]:
"""Stream LLM responses chunk by chunk."""
prepared_messages, all_callbacks = await self._prepare_run(
messages, system_msg, callbacks
)
stream_kwargs = sanitize_stream_kwargs(
self._normalize_tool_request_kwargs(kwargs)
)
effective_model = str(stream_kwargs.get("model") or self.model_name or "").strip() or None
if effective_model:
stream_kwargs["model"] = effective_model
else:
stream_kwargs.pop("model", None)
prepared_messages, all_callbacks = await self._prepare_run(
messages, system_msg, callbacks, model=effective_model
)

async for chunk in self._stream_chat(
prepared_messages, all_callbacks, stream_kwargs
Expand Down Expand Up @@ -1190,19 +1211,24 @@ async def astream_events(

raw_messages_dump = [message_to_dict(m) for m in messages]

processed_messages, all_callbacks = await self._prepare_run(
messages, system_msg, callbacks
)
stream_kwargs = sanitize_stream_kwargs(
self._normalize_tool_request_kwargs(kwargs)
)
effective_model = str(stream_kwargs.get("model") or self.model_name or "").strip() or None
if effective_model:
stream_kwargs["model"] = effective_model
else:
stream_kwargs.pop("model", None)
processed_messages, all_callbacks = await self._prepare_run(
messages, system_msg, callbacks, model=effective_model
)

# Chain start event
yield StreamEventBuilder.chain_start(
chain_run_id,
component_name,
inputs={"messages": [msg.model_dump() for msg in processed_messages]},
metadata={"provider": self.llm_provider, "model": self.model_name},
metadata={"provider": self.llm_provider, "model": effective_model},
)

prompt_run_id = uuid4()
Expand All @@ -1211,15 +1237,15 @@ async def astream_events(
f"{component_name}.prompt",
inputs={"messages": raw_messages_dump, "system": system_msg},
parent_ids=[str(chain_run_id)],
metadata={"model": self.model_name},
metadata={"model": effective_model},
)

yield StreamEventBuilder.prompt_end(
prompt_run_id,
f"{component_name}.prompt",
output={"messages": [msg.model_dump() for msg in processed_messages]},
parent_ids=[str(chain_run_id)],
metadata={"model": self.model_name},
metadata={"model": effective_model},
)

retriever_run_id = None
Expand All @@ -1243,7 +1269,7 @@ async def astream_events(
llm_run_id,
llm_name,
messages=[msg.model_dump() for msg in processed_messages],
model=self.model_name,
model=effective_model,
provider=self.llm_provider,
parent_ids=[str(chain_run_id)],
)
Expand Down Expand Up @@ -1360,6 +1386,7 @@ async def _prepare_run(
messages: List[Union[dict, Message]],
system_msg: Optional[str],
callbacks: Optional[List[BaseCallbackHandler]],
model: Optional[str] = None,
) -> Tuple[List[Message], List[BaseCallbackHandler]]:
"""Normalize messages and merge callbacks for streaming."""
formatted: List[Message] = []
Expand All @@ -1375,7 +1402,7 @@ async def _prepare_run(

processed = await self._apply_short_term_memory_strategy(
formatted,
model=self.model_name,
model=model or self.model_name,
)

merged_callbacks = list(callbacks) if callbacks else []
Expand Down
8 changes: 1 addition & 7 deletions spoon_ai/middleware/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,13 +657,7 @@ async def awrap_model_call(
new_prompt = self.system_prompt

# Create new request with updated system prompt
request = ModelRequest(
messages=request.messages,
system_prompt=new_prompt,
tools=request.tools,
phase=request.phase,
runtime=request.runtime,
)
request = request.override(system_prompt=new_prompt)

return await handler(request)

Expand Down
8 changes: 1 addition & 7 deletions spoon_ai/middleware/patch_tool_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,7 @@ async def awrap_model_call(

if len(patched) != len(messages):
# Create new request with patched messages
request = ModelRequest(
messages=patched,
system_prompt=request.system_prompt,
tools=request.tools,
phase=request.phase,
runtime=request.runtime,
)
request = request.override(messages=patched)

# Update agent memory if accessible
if request.runtime and hasattr(request.runtime, '_agent_instance'):
Expand Down
5 changes: 1 addition & 4 deletions spoon_ai/middleware/prompt_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,9 @@ async def awrap_model_call(

# Create new request if changes were made
if new_system_prompt != request.system_prompt or new_tools != request.tools:
request = ModelRequest(
messages=request.messages,
request = request.override(
system_prompt=new_system_prompt,
tools=new_tools,
phase=request.phase,
runtime=request.runtime,
)

return await handler(request)
Expand Down
Loading
Loading