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
86 changes: 44 additions & 42 deletions mesa_llm/reasoning/cot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class CoTReasoning(Reasoning):
- **agent** (LLMAgent reference)

Methods:
- **plan(obs, ttl=1, prompt=None, selected_tools=None)** → *Plan* - Generate synchronous plan with CoT reasoning
- **async aplan(obs, ttl=1, prompt=None, selected_tools=None)** → *Plan* - Generate asynchronous plan with CoT reasoning
- **plan(obs, ttl=1, prompt=None, selected_tools=None, tool_calls="auto")** → *Plan* - Generate synchronous plan with CoT reasoning
- **async aplan(obs, ttl=1, prompt=None, selected_tools=None, tool_calls="auto")** → *Plan* - Generate asynchronous plan with CoT reasoning

Reasoning Format:
Thought 1: [Initial reasoning based on observation]
Expand Down Expand Up @@ -91,14 +91,29 @@ def plan(
obs: Observation | None = None,
ttl: int = 1,
selected_tools: list[str] | None = None,
tool_calls: str | None = "auto",
) -> Plan:
"""
Plan the next (CoT) action based on the current observation and memory.
Plan the next (CoT) action based on the current observation and the
agent's memory.

``selected_tools`` is forwarded to ``ToolManager.get_all_tools_schema()``.
Omitting it or passing ``None`` uses the default behavior of exposing
all tools, ``[]`` exposes no tools, and a non-empty list restricts
planning/execution to the named tools.

``tool_calls`` controls the execution-phase LiteLLM ``tool_choice``.
The reasoning pass still keeps tool use disabled with ``"none"``.

Supported values in Mesa-LLM are:
- ``None``: defer to LiteLLM/provider default behavior. In practice,
this usually means no tool calls when no tools are provided and
behavior similar to ``"auto"`` when tools are available.
- ``"none"``: never return tool calls; return a normal assistant
message instead.
- ``"auto"``: allow the model to either return a normal assistant
message or call one or more tools.
- ``"required"``: require the model to call one or more tools.
"""
# If no prompt is provided, use the agent's default step prompt
if prompt is None:
Expand All @@ -110,14 +125,7 @@ def plan(
if obs is None:
obs = self.agent.generate_obs()

step = obs.step + 1
llm = self.agent.llm
obs_str = str(obs)

# Add current observation to memory (for record)
self.agent.memory.add_to_memory(
type="Observation", content={"content": obs_str}
)
system_prompt = self.get_cot_system_prompt(obs)

llm.system_prompt = system_prompt
Expand All @@ -129,24 +137,17 @@ def plan(

chaining_message = rsp.choices[0].message.content
self.agent.memory.add_to_memory(
type="Plan", content={"content": chaining_message}
type="plan", content={"content": chaining_message}
)

# Pass plan content to agent for display
if hasattr(self.agent, "_step_display_data"):
self.agent._step_display_data["plan_content"] = chaining_message
system_prompt = "You are an executor that executes the plan given to you in the prompt through tool calls."
llm.system_prompt = system_prompt
rsp = llm.generate(
prompt=chaining_message,
tool_schema=self.agent.tool_manager.get_all_tools_schema(selected_tools),
tool_choice="required",
)
response_message = rsp.choices[0].message
cot_plan = Plan(step=step, llm_plan=response_message, ttl=ttl)

self.agent.memory.add_to_memory(
type="Plan-Execution", content={"content": str(cot_plan)}
cot_plan = self.execute_tool_call(
chaining_message,
selected_tools=selected_tools,
ttl=ttl,
tool_calls=tool_calls,
)

return cot_plan
Expand All @@ -157,6 +158,7 @@ async def aplan(
obs: Observation | None = None,
ttl: int = 1,
selected_tools: list[str] | None = None,
tool_calls: str | None = "auto",
) -> Plan:
"""
Asynchronous version of plan() method for parallel planning.
Expand All @@ -165,6 +167,19 @@ async def aplan(
it or passing ``None`` uses the default behavior of exposing all
tools, ``[]`` exposes no tools, and a non-empty list restricts
planning/execution to the named tools.

``tool_calls`` controls the execution-phase LiteLLM ``tool_choice``.
The reasoning pass still keeps tool use disabled with ``"none"``.

Supported values in Mesa-LLM are:
- ``None``: defer to LiteLLM/provider default behavior. In practice,
this usually means no tool calls when no tools are provided and
behavior similar to ``"auto"`` when tools are available.
- ``"none"``: never return tool calls; return a normal assistant
message instead.
- ``"auto"``: allow the model to either return a normal assistant
message or call one or more tools.
- ``"required"``: require the model to call one or more tools.
"""
# If no prompt is provided, use the agent's default step prompt
if prompt is None:
Expand All @@ -176,13 +191,7 @@ async def aplan(
if obs is None:
obs = await self.agent.agenerate_obs()

step = obs.step + 1
llm = self.agent.llm

obs_str = str(obs)
await self.agent.memory.aadd_to_memory(
type="Observation", content={"content": obs_str}
)
system_prompt = self.get_cot_system_prompt(obs)
llm.system_prompt = system_prompt

Expand All @@ -194,24 +203,17 @@ async def aplan(

chaining_message = rsp.choices[0].message.content
await self.agent.memory.aadd_to_memory(
type="Plan", content={"content": chaining_message}
type="plan", content={"content": chaining_message}
)

# Pass plan content to agent for display
if hasattr(self.agent, "_step_display_data"):
self.agent._step_display_data["plan_content"] = chaining_message
system_prompt = "You are an executor that executes the plan given to you in the prompt through tool calls."
llm.system_prompt = system_prompt
rsp = await llm.agenerate(
prompt=chaining_message,
tool_schema=self.agent.tool_manager.get_all_tools_schema(selected_tools),
tool_choice="required",
)
response_message = rsp.choices[0].message
cot_plan = Plan(step=step, llm_plan=response_message, ttl=ttl)

await self.agent.memory.aadd_to_memory(
type="Plan-Execution", content={"content": str(cot_plan)}
cot_plan = await self.aexecute_tool_call(
chaining_message,
selected_tools=selected_tools,
ttl=ttl,
tool_calls=tool_calls,
)

return cot_plan
37 changes: 34 additions & 3 deletions mesa_llm/reasoning/react.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class ReActReasoning(Reasoning):
- **agent** (LLMAgent reference)

Methods:
- **plan(prompt, obs=None, ttl=1, selected_tools=None)** → *Plan* - Generate synchronous plan with ReAct reasoning
- **async aplan(prompt, obs=None, ttl=1, selected_tools=None)** → *Plan* - Generate asynchronous plan with ReAct reasoning
- **plan(prompt, obs=None, ttl=1, selected_tools=None, tool_calls="auto")** → *Plan* - Generate synchronous plan with ReAct reasoning
- **async aplan(prompt, obs=None, ttl=1, selected_tools=None, tool_calls="auto")** → *Plan* - Generate asynchronous plan with ReAct reasoning
"""

def __init__(self, agent: "LLMAgent"):
Expand Down Expand Up @@ -62,14 +62,29 @@ def plan(
obs: Observation | None = None,
ttl: int = 1,
selected_tools: list[str] | None = None,
tool_calls: str | None = "auto",
) -> Plan:
"""
Plan the next (ReAct) action based on the current observation and memory.
Plan the next (ReAct) action based on the current observation and the
agent's memory.

``selected_tools`` is forwarded to ``ToolManager.get_all_tools_schema()``.
Omitting it or passing ``None`` uses the default behavior of exposing
all tools, ``[]`` exposes no tools, and a non-empty list restricts
planning/execution to the named tools.

``tool_calls`` controls the execution-phase LiteLLM ``tool_choice``.
The reasoning pass still keeps tool use disabled with ``"none"``.

Supported values in Mesa-LLM are:
- ``None``: defer to LiteLLM/provider default behavior. In practice,
this usually means no tool calls when no tools are provided and
behavior similar to ``"auto"`` when tools are available.
- ``"none"``: never return tool calls; return a normal assistant
message instead.
- ``"auto"``: allow the model to either return a normal assistant
message or call one or more tools.
- ``"required"``: require the model to call one or more tools.
"""

if obs is None:
Expand Down Expand Up @@ -108,6 +123,7 @@ def plan(
formatted_response["action"],
selected_tools=selected_tools,
ttl=ttl,
tool_calls=tool_calls,
)

return react_plan
Expand All @@ -118,6 +134,7 @@ async def aplan(
obs: Observation | None = None,
ttl: int = 1,
selected_tools: list[str] | None = None,
tool_calls: str | None = "auto",
) -> Plan:
"""
Asynchronous version of plan() method for parallel planning.
Expand All @@ -126,6 +143,19 @@ async def aplan(
it or passing ``None`` uses the default behavior of exposing all
tools, ``[]`` exposes no tools, and a non-empty list restricts
planning/execution to the named tools.

``tool_calls`` controls the execution-phase LiteLLM ``tool_choice``.
The reasoning pass still keeps tool use disabled with ``"none"``.

Supported values in Mesa-LLM are:
- ``None``: defer to LiteLLM/provider default behavior. In practice,
this usually means no tool calls when no tools are provided and
behavior similar to ``"auto"`` when tools are available.
- ``"none"``: never return tool calls; return a normal assistant
message instead.
- ``"auto"``: allow the model to either return a normal assistant
message or call one or more tools.
- ``"required"``: require the model to call one or more tools.
"""
if obs is None:
obs = await self.agent.agenerate_obs()
Expand Down Expand Up @@ -164,6 +194,7 @@ async def aplan(
formatted_response["action"],
selected_tools=selected_tools,
ttl=ttl,
tool_calls=tool_calls,
)

return react_plan
74 changes: 64 additions & 10 deletions mesa_llm/reasoning/reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ class Reasoning(ABC):
- **agent** (LLMAgent reference)

Methods:
- **abstract plan(prompt, obs=None, ttl=1, selected_tools=None)** → *Plan* - Generate synchronous plan
- **async aplan(prompt, obs=None, ttl=1, selected_tools=None)** → *Plan* - Generate asynchronous plan
- **abstract plan(prompt, obs=None, ttl=1, selected_tools=None, tool_calls="auto")** → *Plan* - Generate synchronous plan
- **async aplan(prompt, obs=None, ttl=1, selected_tools=None, tool_calls="auto")** → *Plan* - Generate asynchronous plan


Reasoning Flow:
Expand All @@ -83,6 +83,7 @@ def plan(
obs: Observation | None = None,
ttl: int = 1,
selected_tools: list[str] | None = None,
tool_calls: str | None = "auto",
) -> Plan:
"""Generate a plan for the next action.

Expand All @@ -95,6 +96,24 @@ def plan(
the default behavior exposes all tools. ``[]`` exposes no
tools, and a non-empty list restricts planning/execution to
the named tools.
tool_calls: Execution-phase LiteLLM ``tool_choice`` override used
when converting the natural-language plan into tool calls.
Planning still keeps tool use disabled.

Supported values in Mesa-LLM are:
- ``None``: defer to LiteLLM/provider default behavior. In
practice, this usually means no tool calls when no tools are
provided and behavior similar to ``"auto"`` when tools are
available.
- ``"none"``: never return tool calls; return a normal
assistant message instead.
- ``"auto"``: allow the model to either return a normal
assistant message or call one or more tools.
- ``"required"``: require the model to call one or more tools.

Mesa-LLM currently exposes only these string choices, not
provider-specific object forms. See LiteLLM docs:
https://docs.litellm.ai/
"""

async def aplan(
Expand All @@ -103,6 +122,7 @@ async def aplan(
obs: Observation | None = None,
ttl: int = 1,
selected_tools: list[str] | None = None,
tool_calls: str | None = "auto",
) -> Plan:
"""
Asynchronous version of plan() method for parallel planning.
Expand All @@ -118,32 +138,59 @@ async def aplan(
obs=obs,
ttl=ttl,
selected_tools=selected_tools,
tool_calls=tool_calls,
)

def execute_tool_call(
self,
chaining_message,
selected_tools: list[str] | None = None,
ttl: int = 1,
tool_calls: str | None = "auto",
):
"""Turn a natural-language plan into tool calls.

``selected_tools`` is forwarded to ``ToolManager.get_all_tools_schema()``.
Omitting it or passing ``None`` uses the default behavior of exposing
all tools, ``[]`` exposes no tools, and a non-empty list restricts
execution to the named tools.
Args:
chaining_message: Natural-language plan or action text to execute.
selected_tools: Optional explicit tool allowlist forwarded to
``ToolManager.get_all_tools_schema()``. Omitting it or passing
``None`` uses the default behavior of exposing all tools,
``[]`` exposes no tools, and a non-empty list restricts
execution to the named tools.
ttl: Time-to-live for the returned plan.
tool_calls: LiteLLM ``tool_choice`` passed to the execution call.
Supported values in Mesa-LLM are:
- ``None``: defer to LiteLLM/provider default behavior. In
practice, this usually means no tool calls when no tools are
provided and behavior similar to ``"auto"`` when tools are
available.
- ``"none"``: never return tool calls; return a normal
assistant message instead.
- ``"auto"``: allow the model to either return a normal
assistant message or call one or more tools.
- ``"required"``: require the model to call one or more tools.

Mesa-LLM currently exposes only these string choices, not
provider-specific object forms. See LiteLLM docs:
https://docs.litellm.ai/
"""
system_prompt = "You are an executor that executes the plan given to you in the prompt through tool calls."
system_prompt = (
"You are an executor that executes the plan given to you in the prompt through tool calls. "
"If the plan concludes that no action should be taken, do not call any tool."
)
self.agent.llm.system_prompt = system_prompt
rsp = self.agent.llm.generate(
prompt=chaining_message,
tool_schema=self.agent.tool_manager.get_all_tools_schema(
selected_tools=selected_tools
),
tool_choice="required",
tool_choice=tool_calls,
)
response_message = rsp.choices[0].message
plan = Plan(step=self.agent.model.steps, llm_plan=response_message, ttl=ttl)
self.agent.memory.add_to_memory(
type="plan_execution", content={"content": str(plan)}
)

return plan

Expand All @@ -152,6 +199,7 @@ async def aexecute_tool_call(
chaining_message,
selected_tools: list[str] | None = None,
ttl: int = 1,
tool_calls: str | None = "auto",
):
"""
Asynchronous version of execute_tool_call() method.
Expand All @@ -161,16 +209,22 @@ async def aexecute_tool_call(
default behavior of exposing all tools, ``[]`` exposes no tools, and
a non-empty list restricts execution to the named tools.
"""
system_prompt = "You are an executor that executes the plan given to you in the prompt through tool calls."
system_prompt = (
"You are an executor that executes the plan given to you in the prompt through tool calls. "
"If the plan concludes that no action should be taken, do not call any tool."
)
self.agent.llm.system_prompt = system_prompt
rsp = await self.agent.llm.agenerate(
prompt=chaining_message,
tool_schema=self.agent.tool_manager.get_all_tools_schema(
selected_tools=selected_tools
),
tool_choice="required",
tool_choice=tool_calls,
)
response_message = rsp.choices[0].message
plan = Plan(step=self.agent.model.steps, llm_plan=response_message, ttl=ttl)
await self.agent.memory.aadd_to_memory(
type="plan_execution", content={"content": str(plan)}
)

return plan
Loading
Loading