universal-mcp-agents 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of universal-mcp-agents might be problematic. Click here for more details.

Files changed (50) hide show
  1. universal_mcp/agents/__init__.py +11 -8
  2. universal_mcp/agents/base.py +13 -18
  3. universal_mcp/agents/bigtool2/__init__.py +6 -7
  4. universal_mcp/agents/bigtool2/__main__.py +2 -4
  5. universal_mcp/agents/bigtool2/agent.py +1 -0
  6. universal_mcp/agents/bigtool2/graph.py +48 -184
  7. universal_mcp/agents/bigtool2/meta_tools.py +120 -0
  8. universal_mcp/agents/bigtoolcache/__init__.py +31 -22
  9. universal_mcp/agents/bigtoolcache/__main__.py +1 -4
  10. universal_mcp/agents/bigtoolcache/agent.py +1 -3
  11. universal_mcp/agents/bigtoolcache/graph.py +101 -191
  12. universal_mcp/agents/bigtoolcache/prompts.py +7 -31
  13. universal_mcp/agents/bigtoolcache/tools.py +141 -0
  14. universal_mcp/agents/builder.py +10 -20
  15. universal_mcp/agents/cli.py +1 -2
  16. universal_mcp/agents/codeact/__init__.py +2 -254
  17. universal_mcp/agents/codeact/__main__.py +35 -0
  18. universal_mcp/agents/codeact/agent.py +160 -0
  19. universal_mcp/agents/codeact/prompts.py +91 -0
  20. universal_mcp/agents/codeact/sandbox.py +42 -18
  21. universal_mcp/agents/codeact/state.py +10 -0
  22. universal_mcp/agents/codeact/utils.py +12 -5
  23. universal_mcp/agents/hil.py +1 -6
  24. universal_mcp/agents/planner/__init__.py +1 -3
  25. universal_mcp/agents/planner/graph.py +1 -3
  26. universal_mcp/agents/react.py +14 -6
  27. universal_mcp/agents/shared/prompts.py +31 -17
  28. universal_mcp/agents/shared/tool_node.py +68 -53
  29. universal_mcp/agents/simple.py +2 -1
  30. universal_mcp/agents/utils.py +4 -15
  31. universal_mcp/applications/ui/app.py +5 -15
  32. {universal_mcp_agents-0.1.8.dist-info → universal_mcp_agents-0.1.10.dist-info}/METADATA +2 -1
  33. universal_mcp_agents-0.1.10.dist-info/RECORD +42 -0
  34. universal_mcp/agents/autoagent/__init__.py +0 -30
  35. universal_mcp/agents/autoagent/__main__.py +0 -25
  36. universal_mcp/agents/autoagent/context.py +0 -26
  37. universal_mcp/agents/autoagent/graph.py +0 -170
  38. universal_mcp/agents/autoagent/prompts.py +0 -9
  39. universal_mcp/agents/autoagent/state.py +0 -27
  40. universal_mcp/agents/autoagent/utils.py +0 -13
  41. universal_mcp/agents/bigtool/__init__.py +0 -58
  42. universal_mcp/agents/bigtool/__main__.py +0 -23
  43. universal_mcp/agents/bigtool/graph.py +0 -210
  44. universal_mcp/agents/bigtool/prompts.py +0 -31
  45. universal_mcp/agents/bigtool/state.py +0 -27
  46. universal_mcp/agents/bigtoolcache/tools_all.txt +0 -956
  47. universal_mcp/agents/bigtoolcache/tools_important.txt +0 -474
  48. universal_mcp/agents/codeact/test.py +0 -16
  49. universal_mcp_agents-0.1.8.dist-info/RECORD +0 -51
  50. {universal_mcp_agents-0.1.8.dist-info → universal_mcp_agents-0.1.10.dist-info}/WHEEL +0 -0
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from collections import defaultdict
2
3
  from collections.abc import Sequence
3
4
  from typing import Annotated, TypedDict
4
5
 
@@ -15,7 +16,7 @@ from universal_mcp.agents.base import BaseAgent
15
16
  from universal_mcp.agents.llm import load_chat_model
16
17
  from universal_mcp.agents.shared.tool_node import build_tool_node_graph
17
18
  from universal_mcp.agents.utils import messages_to_list
18
- from collections import defaultdict
19
+
19
20
 
20
21
  class Agent(BaseModel):
21
22
  """Agent that can be created by the builder."""
@@ -24,9 +25,7 @@ class Agent(BaseModel):
24
25
  description: str = Field(description="A small description of the agent.")
25
26
  expertise: str = Field(description="The expertise of the agent.")
26
27
  instructions: str = Field(description="The instructions for the agent to follow.")
27
- schedule: str | None = Field(
28
- description="The cron expression for the agent to run on.", default=None
29
- )
28
+ schedule: str | None = Field(description="The cron expression for the agent to run on.", default=None)
30
29
 
31
30
 
32
31
  class BuilderState(TypedDict):
@@ -79,9 +78,7 @@ Create an agent that feels thoughtfully designed, intelligent, and professionall
79
78
  """
80
79
 
81
80
 
82
- async def generate_agent(
83
- llm: BaseChatModel, task: str, old_agent: Agent | None = None
84
- ) -> Agent:
81
+ async def generate_agent(llm: BaseChatModel, task: str, old_agent: Agent | None = None) -> Agent:
85
82
  """Generates an agent from a task, optionally modifying an existing one."""
86
83
  prompt_parts = [AGENT_BUILDER_INSTRUCTIONS]
87
84
  if old_agent:
@@ -111,7 +108,7 @@ class BuilderAgent(BaseAgent):
111
108
  ):
112
109
  super().__init__(name, instructions, model, memory, **kwargs)
113
110
  self.registry = registry
114
- self.llm: BaseChatModel = load_chat_model(model)
111
+ self.llm: BaseChatModel = load_chat_model(model, thinking=False)
115
112
 
116
113
  async def _create_agent(self, state: BuilderState):
117
114
  last_message = state["messages"][-1]
@@ -129,11 +126,7 @@ class BuilderAgent(BaseAgent):
129
126
  yield {
130
127
  "user_task": task,
131
128
  "generated_agent": generated_agent,
132
- "messages": [
133
- AIMessage(
134
- content=("I've designed an agent to help you with your task.")
135
- )
136
- ],
129
+ "messages": [AIMessage(content=("I've designed an agent to help you with your task."))],
137
130
  }
138
131
 
139
132
  async def _create_tool_config(self, state: BuilderState):
@@ -146,7 +139,7 @@ class BuilderAgent(BaseAgent):
146
139
  ]
147
140
  }
148
141
  tool_finder_graph = build_tool_node_graph(self.llm, self.registry)
149
-
142
+
150
143
  initial_state = {
151
144
  "original_task": task,
152
145
  "messages": [HumanMessage(content=task)],
@@ -165,9 +158,7 @@ class BuilderAgent(BaseAgent):
165
158
  apps_with_tools[app_id].extend(tool_ids)
166
159
 
167
160
  # Convert to a regular dict and remove any duplicate tool_ids for the same app
168
- tool_config = {
169
- app_id: list(set(tools)) for app_id, tools in apps_with_tools.items()
170
- }
161
+ tool_config = {app_id: list(set(tools)) for app_id, tools in apps_with_tools.items()}
171
162
  final_message = "I have selected the necessary tools for the agent. The agent is ready!"
172
163
  else:
173
164
  # Handle the case where the graph failed to create a plan
@@ -175,9 +166,7 @@ class BuilderAgent(BaseAgent):
175
166
 
176
167
  yield {
177
168
  "tool_config": tool_config,
178
- "messages": [
179
- AIMessage(content=final_message)
180
- ],
169
+ "messages": [AIMessage(content=final_message)],
181
170
  }
182
171
 
183
172
  async def _build_graph(self):
@@ -205,6 +194,7 @@ async def main():
205
194
  "Send a daily email to manoj@agentr.dev with daily agenda of the day",
206
195
  )
207
196
  from rich import print
197
+
208
198
  print(messages_to_list(result["messages"]))
209
199
  print(result["generated_agent"])
210
200
  print(result["tool_config"])
@@ -20,7 +20,6 @@ def run(name: str = "react"):
20
20
  """Run the agent CLI"""
21
21
  import asyncio
22
22
 
23
-
24
23
  setup_logger(log_file=None, level="ERROR")
25
24
  client = AgentrClient()
26
25
  params = {
@@ -30,7 +29,7 @@ def run(name: str = "react"):
30
29
  "memory": MemorySaver(),
31
30
  "tools": {
32
31
  "google_mail": ["send_email"],
33
- }
32
+ },
34
33
  }
35
34
  agent_cls = get_agent(name)
36
35
  agent = agent_cls(name, **params)
@@ -1,255 +1,3 @@
1
- import inspect
2
- import re
3
- from collections.abc import Awaitable, Callable, Sequence
4
- from typing import Any, TypeVar
1
+ from .agent import CodeActAgent
5
2
 
6
- from langchain_core.language_models import BaseChatModel
7
- from langchain_core.tools import StructuredTool
8
- from langchain_core.tools import tool as create_tool
9
- from langgraph.graph import END, START, MessagesState, StateGraph
10
- from langgraph.types import Command
11
-
12
- from .utils import extract_and_combine_codeblocks
13
-
14
- EvalFunction = Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]
15
- EvalCoroutine = Callable[[str, dict[str, Any]], Awaitable[tuple[str, dict[str, Any]]]]
16
-
17
-
18
- class CodeActState(MessagesState):
19
- """State for CodeAct agent."""
20
-
21
- script: str | None
22
- """The Python code script to be executed."""
23
- context: dict[str, Any]
24
- """Dictionary containing the execution context with available tools and variables."""
25
-
26
-
27
- StateSchema = TypeVar("StateSchema", bound=CodeActState)
28
- StateSchemaType = type[StateSchema]
29
-
30
-
31
- def make_safe_function_name(name: str) -> str:
32
- """Convert a tool name to a valid Python function name."""
33
- # Replace non-alphanumeric characters with underscores
34
- safe_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
35
- # Ensure the name doesn't start with a digit
36
- if safe_name and safe_name[0].isdigit():
37
- safe_name = f"tool_{safe_name}"
38
- # Handle empty name edge case
39
- if not safe_name:
40
- safe_name = "unnamed_tool"
41
- return safe_name
42
-
43
-
44
- def create_default_prompt(tools: list[StructuredTool], base_prompt: str | None = None):
45
- """Create default prompt for the CodeAct agent."""
46
- tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in tools]
47
- prompt = f"{base_prompt}\n\n" if base_prompt else ""
48
- prompt += """You will be given a task to perform. You should output either
49
- - a Python code snippet that provides the solution to the task, or a step towards the solution. Any output you want to extract from the code should be printed to the console. Code should be output in a fenced code block.
50
- - text to be shown directly to the user, if you want to ask for more information or provide the final answer.
51
-
52
- In addition to the Python Standard Library, you can use the following functions:
53
- """
54
-
55
- for tool in tools:
56
- # Use coroutine if it exists, otherwise use func
57
- tool_callable = (
58
- tool.coroutine
59
- if hasattr(tool, "coroutine") and tool.coroutine is not None
60
- else tool.func
61
- )
62
- # Create a safe function name
63
- safe_name = make_safe_function_name(tool.name)
64
- # Determine if it's an async function
65
- is_async = inspect.iscoroutinefunction(tool_callable)
66
- # Add appropriate function definition
67
- prompt += f'''
68
- {"async " if is_async else ""}def {safe_name}{str(inspect.signature(tool_callable))}:
69
- """{tool.description}"""
70
- ...
71
- '''
72
-
73
- prompt += """
74
-
75
- Variables defined at the top level of previous code snippets can be referenced in your code.
76
-
77
- Reminder: use Python code snippets to call tools"""
78
- return prompt
79
-
80
-
81
- def create_codeact(
82
- model: BaseChatModel,
83
- tools: Sequence[StructuredTool | Callable],
84
- eval_fn: EvalFunction | EvalCoroutine,
85
- *,
86
- prompt: str | None = None,
87
- reflection_prompt: str | None = None,
88
- reflection_model: BaseChatModel | None = None,
89
- max_reflections: int = 3,
90
- state_schema: StateSchemaType = CodeActState,
91
- ) -> StateGraph:
92
- """Create a CodeAct agent.
93
-
94
- Args:
95
- model: The language model to use for generating code
96
- tools: List of tools available to the agent. Can be passed as python functions or StructuredTool instances.
97
- eval_fn: Function or coroutine that executes code in a sandbox. Takes code string and locals dict,
98
- returns a tuple of (stdout output, new variables dict)
99
- prompt: Optional custom system prompt. If None, uses default prompt.
100
- To customize default prompt you can use `create_default_prompt` helper:
101
- `create_default_prompt(tools, "You are a helpful assistant.")`
102
- reflection_prompt: Optional prompt for reflection. If provided, will be used to evaluate responses.
103
- If the reflection output contains "NONE", the response is considered valid, otherwise the
104
- reflection output is passed back to the model for regeneration.
105
- reflection_model: Optional model to use for reflection. If None, uses the same model as for generation.
106
- max_reflections: Maximum number of reflection iterations (default: 3).
107
- state_schema: The state schema to use for the agent.
108
-
109
- Returns:
110
- A StateGraph implementing the CodeAct architecture
111
- """
112
- tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in tools]
113
-
114
- if prompt is None:
115
- prompt = create_default_prompt(tools)
116
-
117
- # If no reflection model is provided, use the main model
118
- if reflection_model is None:
119
- reflection_model = model
120
-
121
- # Make tools available to the code sandbox - use safe names for keys
122
- tools_context = {}
123
- for tool in tools:
124
- safe_name = make_safe_function_name(tool.name)
125
- # Use coroutine if it exists, otherwise use func (same as in create_default_prompt)
126
- tool_callable = (
127
- tool.coroutine
128
- if hasattr(tool, "coroutine") and tool.coroutine is not None
129
- else tool.func
130
- )
131
- # Only use the safe name for consistency with the prompt
132
- tools_context[safe_name] = tool_callable
133
-
134
- def call_model(state: StateSchema) -> Command:
135
- messages = [{"role": "system", "content": prompt}] + state["messages"]
136
-
137
- # Run the model and potentially loop for reflection
138
- response = model.invoke(messages)
139
-
140
- # Extract and combine all code blocks
141
- code = extract_and_combine_codeblocks(response.content)
142
-
143
- # Loop for reflection if needed and if code is present
144
- if reflection_prompt and code:
145
- reflection_count = 0
146
- while reflection_count < max_reflections:
147
- # Format conversation history with XML-style tags
148
- conversation_history = "\n".join(
149
- [
150
- f'<message role="{("user" if m.type == "human" else "assistant")}">\n{m.content}\n</message>'
151
- for m in state["messages"]
152
- ]
153
- )
154
-
155
- # Add the current response
156
- conversation_history += (
157
- f'\n<message role="assistant">\n{response.content}\n</message>'
158
- )
159
-
160
- # Create the reflection prompt with the tagged conversation history
161
- formatted_prompt = f"""
162
- Review the assistant's latest code for as per the quality rules:
163
-
164
- <conversation_history>
165
- {conversation_history}
166
- </conversation_history>
167
-
168
- If you find ANY of these issues, describe the problem briefly and clearly.
169
- If NO issues are found, respond with EXACTLY: "NONE"
170
- """
171
-
172
- # Create messages for reflection with correct ordering
173
- reflection_messages = [
174
- {"role": "system", "content": reflection_prompt},
175
- # Include the formatted reflection prompt as the final user message
176
- {"role": "user", "content": formatted_prompt},
177
- ]
178
- reflection_result = reflection_model.invoke(reflection_messages)
179
-
180
- # Check if reflection passed
181
- if "NONE" in reflection_result.content:
182
- # Reflection passed, exit loop
183
- break
184
-
185
- # Reflection didn't pass, regenerate response
186
- reflection_messages = [
187
- {"role": "system", "content": prompt},
188
- *state["messages"],
189
- {"role": "assistant", "content": response.content},
190
- {
191
- "role": "user",
192
- "content": f"""
193
- I need you to completely regenerate your previous response based on this feedback:
194
-
195
- '''
196
- {reflection_result.content}
197
- '''
198
-
199
- DO NOT reference the feedback directly. Instead, provide a completely new response that addresses the issues.
200
- """,
201
- },
202
- ]
203
- response = model.invoke(reflection_messages)
204
-
205
- # Extract code from the new response
206
- code = extract_and_combine_codeblocks(response.content)
207
-
208
- # If no code in the new response, exit the reflection loop
209
- if not code:
210
- break
211
-
212
- # Increment reflection count
213
- reflection_count += 1
214
-
215
- # Return appropriate command with only the latest response
216
- if code:
217
- return Command(
218
- goto="sandbox", update={"messages": [response], "script": code}
219
- )
220
- else:
221
- # no code block, end the loop and respond to the user
222
- return Command(update={"messages": [response], "script": None})
223
-
224
- # If eval_fn is a async, we define async node function.
225
- if inspect.iscoroutinefunction(eval_fn):
226
-
227
- async def sandbox(state: StateSchema):
228
- existing_context = state.get("context", {})
229
- context = {**existing_context, **tools_context}
230
- # Execute the script in the sandbox
231
- output, new_vars = await eval_fn(state["script"], context)
232
- new_context = {**existing_context, **new_vars}
233
- return {
234
- "messages": [{"role": "user", "content": output}],
235
- "context": new_context,
236
- }
237
- else:
238
-
239
- def sandbox(state: StateSchema):
240
- existing_context = state.get("context", {})
241
- context = {**existing_context, **tools_context}
242
- # Execute the script in the sandbox
243
- output, new_vars = eval_fn(state["script"], context)
244
- new_context = {**existing_context, **new_vars}
245
- return {
246
- "messages": [{"role": "user", "content": output}],
247
- "context": new_context,
248
- }
249
-
250
- agent = StateGraph(state_schema)
251
- agent.add_node(call_model, destinations=(END, "sandbox"))
252
- agent.add_node(sandbox)
253
- agent.add_edge(START, "call_model")
254
- agent.add_edge("sandbox", "call_model")
255
- return agent
3
+ __all__ = ["CodeActAgent"]
@@ -0,0 +1,35 @@
1
+ import asyncio
2
+
3
+ from langgraph.checkpoint.memory import MemorySaver
4
+ from rich import print
5
+ from universal_mcp.agentr.registry import AgentrRegistry
6
+
7
+ from universal_mcp.agents.codeact.agent import CodeActAgent
8
+ from universal_mcp.agents.utils import messages_to_list
9
+
10
+
11
+ async def main():
12
+ memory = MemorySaver()
13
+ agent = CodeActAgent(
14
+ "CodeAct Agent",
15
+ instructions="Be very concise in your answers.",
16
+ model="anthropic:claude-4-sonnet-20250514",
17
+ tools={"google_mail": ["list_messages"]},
18
+ registry=AgentrRegistry(),
19
+ memory=memory,
20
+ )
21
+ print("Starting agent...")
22
+ # await agent.ainit()
23
+ # await agent.run_interactive()
24
+ # async for event in agent.stream(
25
+ # user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
26
+ # ):
27
+ # print(event.content, end="")
28
+ result = await agent.invoke(
29
+ user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
30
+ )
31
+ print(messages_to_list(result["messages"]))
32
+
33
+
34
+ if __name__ == "__main__":
35
+ asyncio.run(main())
@@ -0,0 +1,160 @@
1
+ from collections.abc import Callable
2
+
3
+ from langchain_core.messages import AIMessageChunk
4
+ from langchain_core.tools import StructuredTool
5
+ from langchain_core.tools import tool as create_tool
6
+ from langgraph.checkpoint.base import BaseCheckpointSaver
7
+ from langgraph.graph import END, StateGraph
8
+ from universal_mcp.logger import logger
9
+ from universal_mcp.tools.registry import ToolRegistry
10
+ from universal_mcp.types import ToolConfig, ToolFormat
11
+
12
+ from universal_mcp.agents.base import BaseAgent
13
+ from universal_mcp.agents.codeact.prompts import (
14
+ REFLECTION_PROMPT,
15
+ RETRY_PROMPT,
16
+ create_default_prompt,
17
+ make_safe_function_name,
18
+ )
19
+ from universal_mcp.agents.codeact.sandbox import eval_unsafe
20
+ from universal_mcp.agents.codeact.state import CodeActState
21
+ from universal_mcp.agents.codeact.utils import extract_and_combine_codeblocks
22
+ from universal_mcp.agents.llm import load_chat_model
23
+
24
+
25
+ class CodeActAgent(BaseAgent):
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ instructions: str,
30
+ model: str,
31
+ memory: BaseCheckpointSaver | None = None,
32
+ tools: ToolConfig | None = None,
33
+ registry: ToolRegistry | None = None,
34
+ sandbox_timeout: int = 20,
35
+ **kwargs,
36
+ ):
37
+ super().__init__(name, instructions, model, memory, **kwargs)
38
+ self.model_instance = load_chat_model(model, thinking=False)
39
+ self.tools_config = tools or {}
40
+ self.registry = registry
41
+ self.eval_fn = eval_unsafe
42
+ self.reflection_prompt = REFLECTION_PROMPT
43
+ self.reflection_model = self.model_instance
44
+ self.max_reflections = 3
45
+ self.tools_context = {}
46
+ self.context = {}
47
+ self.sandbox_timeout = sandbox_timeout
48
+ self.processed_tools: list[StructuredTool | Callable] = []
49
+
50
+ async def _build_graph(self):
51
+ if self.tools_config:
52
+ if not self.registry:
53
+ raise ValueError("Tools are configured but no registry is provided")
54
+ # Langchain tools are fine
55
+ exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
56
+ self.processed_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in exported_tools]
57
+
58
+ self.instructions = create_default_prompt(self.processed_tools, self.instructions)
59
+
60
+ for tool in self.processed_tools:
61
+ safe_name = make_safe_function_name(tool.name)
62
+ tool_callable = tool.coroutine if hasattr(tool, "coroutine") and tool.coroutine is not None else tool.func
63
+ self.tools_context[safe_name] = tool_callable
64
+
65
+ self.context = {**self.context, **self.tools_context}
66
+
67
+ agent = StateGraph(CodeActState)
68
+ agent.add_node("call_model", self.call_model)
69
+ agent.add_node("sandbox", self.sandbox)
70
+
71
+ agent.set_entry_point("call_model")
72
+ agent.add_conditional_edges(
73
+ "call_model",
74
+ self.should_run_sandbox,
75
+ {
76
+ "sandbox": "sandbox",
77
+ END: END,
78
+ },
79
+ )
80
+ agent.add_edge("sandbox", "call_model")
81
+ return agent.compile(checkpointer=self.memory)
82
+
83
+ def should_run_sandbox(self, state: CodeActState) -> str:
84
+ last_message = state["messages"][-1]
85
+ if isinstance(last_message.content, str) and "TASK_COMPLETE" in last_message.content:
86
+ return END
87
+
88
+ if state.get("script"):
89
+ return "sandbox"
90
+ return END
91
+
92
+ def _extract_content(self, response: AIMessageChunk) -> str:
93
+ if isinstance(response.content, list):
94
+ content = " ".join([c.get("text", "") for c in response.content])
95
+ else:
96
+ content = response.content
97
+ return content
98
+
99
+ async def call_model(self, state: CodeActState) -> dict:
100
+ model = self.model_instance
101
+ reflection_model = self.reflection_model
102
+
103
+ messages = [{"role": "system", "content": self.instructions}] + state["messages"]
104
+
105
+ response = await model.ainvoke(messages)
106
+
107
+ text_content = self._extract_content(response)
108
+ if not isinstance(text_content, str):
109
+ raise ValueError(f"Content is not a string: {text_content}")
110
+ code = extract_and_combine_codeblocks(text_content)
111
+ logger.debug(f"Code: {code}")
112
+
113
+ if self.max_reflections > 0 and code:
114
+ reflection_count = 0
115
+ while reflection_count < self.max_reflections:
116
+ conversation_history = "\n".join(
117
+ [
118
+ f'<message role="{("user" if m.type == "human" else "assistant")}">\n{m.content}\n</message>'
119
+ for m in state["messages"]
120
+ ]
121
+ )
122
+ conversation_history += f'\n<message role="assistant">\n{response.content}\n</message>'
123
+
124
+ formatted_prompt = REFLECTION_PROMPT.format(conversation_history=conversation_history)
125
+
126
+ reflection_messages = [
127
+ {"role": "system", "content": self.reflection_prompt},
128
+ {"role": "user", "content": formatted_prompt},
129
+ ]
130
+ reflection_result = await reflection_model.ainvoke(reflection_messages)
131
+
132
+ if "NONE" in reflection_result.content:
133
+ break
134
+
135
+ retry_prompt = RETRY_PROMPT.format(reflection_result=reflection_result.content)
136
+
137
+ regeneration_messages = [
138
+ {"role": "system", "content": self.instructions},
139
+ *state["messages"],
140
+ {"role": "assistant", "content": response.content},
141
+ {"role": "user", "content": retry_prompt},
142
+ ]
143
+ response = await model.ainvoke(regeneration_messages)
144
+
145
+ code = extract_and_combine_codeblocks(response.content)
146
+
147
+ if not code:
148
+ break
149
+
150
+ reflection_count += 1
151
+
152
+ return {"messages": [response], "script": code}
153
+
154
+ async def sandbox(self, state: CodeActState) -> dict:
155
+ output, new_vars = await self.eval_fn(state["script"], self.context, timeout=self.sandbox_timeout)
156
+ self.context = {**self.context, **new_vars}
157
+ return {
158
+ "messages": [AIMessageChunk(content=output.strip())],
159
+ "script": None,
160
+ }
@@ -0,0 +1,91 @@
1
+ import inspect
2
+ import re
3
+ from collections.abc import Sequence
4
+
5
+ from langchain_core.tools import StructuredTool
6
+
7
+
8
+ def make_safe_function_name(name: str) -> str:
9
+ """Convert a tool name to a valid Python function name."""
10
+ # Replace non-alphanumeric characters with underscores
11
+ safe_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
12
+ # Ensure the name doesn't start with a digit
13
+ if safe_name and safe_name[0].isdigit():
14
+ safe_name = f"tool_{safe_name}"
15
+ # Handle empty name edge case
16
+ if not safe_name:
17
+ safe_name = "unnamed_tool"
18
+ return safe_name
19
+
20
+
21
+ def create_default_prompt(
22
+ tools: Sequence[StructuredTool],
23
+ base_prompt: str | None = None,
24
+ ):
25
+ """Create default prompt for the CodeAct agent."""
26
+ prompt = f"{base_prompt}\n\n" if base_prompt else ""
27
+ prompt += """You will be given a task to perform. You should output either
28
+ - a Python code snippet that provides the solution to the task, or a step towards the solution. Any output you want to extract from the code should be printed to the console. Code should be output in a fenced code block.
29
+ - text to be shown directly to the user, if you want to ask for more information or provide the final answer.
30
+
31
+ In addition to the Python Standard Library, you can use the following functions:"""
32
+
33
+ for tool in tools:
34
+ # Use coroutine if it exists, otherwise use func
35
+ tool_callable = tool.coroutine if hasattr(tool, "coroutine") and tool.coroutine is not None else tool.func
36
+ # Create a safe function name
37
+ safe_name = make_safe_function_name(tool.name)
38
+ # Determine if it's an async function
39
+ is_async = inspect.iscoroutinefunction(tool_callable)
40
+ # Add appropriate function definition
41
+ prompt += f'''\n{"async " if is_async else ""}def {safe_name}{str(inspect.signature(tool_callable))}:
42
+ """{tool.description}"""
43
+ ...
44
+ '''
45
+
46
+ prompt += """
47
+
48
+ Variables defined at the top level of previous code snippets can be referenced in your code.
49
+
50
+ Always use print() statements to explore data structures and function outputs. Simply returning values will not display them back to you for inspection. For example, use print(result) instead of just 'result'.
51
+
52
+ As you don't know the output schema of the additional Python functions you have access to, start from exploring their contents before building a final solution.
53
+
54
+ IMPORTANT CODING STRATEGY:
55
+ 1. Only write code up to the point where you make an API call/tool usage with an output
56
+ 2. Print the type/shape and a sample entry of this output, and using that knowledge proceed to write the further code
57
+
58
+ This means:
59
+ - Write code that makes the API call or tool usage
60
+ - Print the result with type information: print(f"Type: {type(result)}")
61
+ - Print the shape/structure: print(f"Shape/Keys: {result.keys() if isinstance(result, dict) else len(result) if isinstance(result, (list, tuple)) else 'N/A'}")
62
+ - Print a sample entry: print(f"Sample: {result[0] if isinstance(result, (list, tuple)) and len(result) > 0 else result}")
63
+ - Then, based on this knowledge, write the code to process/use this data
64
+
65
+ Reminder: use Python code snippets to call tools
66
+
67
+ When you have completely finished the task and provided the final answer, you MUST end your response with the exact phrase "TASK_COMPLETE".
68
+ """
69
+ return prompt
70
+
71
+
72
+ REFLECTION_PROMPT = """
73
+ Review the assistant's latest code for as per the quality rules:
74
+
75
+ <conversation_history>
76
+ {conversation_history}
77
+ </conversation_history>
78
+
79
+ If you find ANY of these issues, describe the problem briefly and clearly.
80
+ If NO issues are found, respond with EXACTLY: "NONE"
81
+ """
82
+
83
+ RETRY_PROMPT = """
84
+ I need you to completely regenerate your previous response based on this feedback:
85
+
86
+ '''
87
+ {reflection_result}
88
+ '''
89
+
90
+ DO NOT reference the feedback directly. Instead, provide a completely new response that addresses the issues.
91
+ """