universal-mcp-agents 0.1.9__py3-none-any.whl → 0.1.11__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 (49) hide show
  1. universal_mcp/agents/__init__.py +9 -9
  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 +1 -1
  17. universal_mcp/agents/codeact/__main__.py +15 -5
  18. universal_mcp/agents/codeact/agent.py +67 -100
  19. universal_mcp/agents/codeact/prompts.py +32 -42
  20. universal_mcp/agents/codeact/sandbox.py +30 -39
  21. universal_mcp/agents/codeact/state.py +3 -6
  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 +3 -3
  28. universal_mcp/agents/shared/tool_node.py +47 -47
  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.9.dist-info → universal_mcp_agents-0.1.11.dist-info}/METADATA +2 -1
  33. universal_mcp_agents-0.1.11.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-0.1.9.dist-info/RECORD +0 -54
  49. {universal_mcp_agents-0.1.9.dist-info → universal_mcp_agents-0.1.11.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,3 +1,3 @@
1
1
  from .agent import CodeActAgent
2
2
 
3
- __all__ = ["CodeActAgent"]
3
+ __all__ = ["CodeActAgent"]
@@ -1,23 +1,33 @@
1
1
  import asyncio
2
2
 
3
+ from langgraph.checkpoint.memory import MemorySaver
4
+ from rich import print
3
5
  from universal_mcp.agentr.registry import AgentrRegistry
6
+
4
7
  from universal_mcp.agents.codeact.agent import CodeActAgent
5
8
  from universal_mcp.agents.utils import messages_to_list
6
9
 
7
10
 
8
11
  async def main():
12
+ memory = MemorySaver()
9
13
  agent = CodeActAgent(
10
14
  "CodeAct Agent",
11
15
  instructions="Be very concise in your answers.",
12
- model="azure/gpt-4o",
13
- tools={"google_mail": ["send_email"]},
16
+ model="anthropic:claude-4-sonnet-20250514",
17
+ tools={"google_mail": ["list_messages"]},
14
18
  registry=AgentrRegistry(),
19
+ memory=memory,
15
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="")
16
28
  result = await agent.invoke(
17
- "Send an email to manoj@agentr.dev from my Gmail account with a subject 'testing codeact agent' and body 'This is a test of the codeact agent.'"
29
+ user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
18
30
  )
19
- from rich import print
20
-
21
31
  print(messages_to_list(result["messages"]))
22
32
 
23
33
 
@@ -1,25 +1,23 @@
1
- import inspect
2
- from typing import Callable, Union
1
+ from collections.abc import Callable
3
2
 
4
- from langchain_core.language_models import BaseChatModel
5
- from langchain_core.tools import StructuredTool, tool as create_tool
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
6
  from langgraph.checkpoint.base import BaseCheckpointSaver
7
7
  from langgraph.graph import END, StateGraph
8
- from loguru import logger
8
+ from universal_mcp.logger import logger
9
9
  from universal_mcp.tools.registry import ToolRegistry
10
10
  from universal_mcp.types import ToolConfig, ToolFormat
11
11
 
12
12
  from universal_mcp.agents.base import BaseAgent
13
- from universal_mcp.agents.llm import load_chat_model
14
13
  from universal_mcp.agents.codeact.prompts import (
15
14
  create_default_prompt,
16
15
  make_safe_function_name,
17
- REFLECTION_PROMPT,
18
- RETRY_PROMPT,
19
16
  )
20
17
  from universal_mcp.agents.codeact.sandbox import eval_unsafe
21
18
  from universal_mcp.agents.codeact.state import CodeActState
22
19
  from universal_mcp.agents.codeact.utils import extract_and_combine_codeblocks
20
+ from universal_mcp.agents.llm import load_chat_model
23
21
 
24
22
 
25
23
  class CodeActAgent(BaseAgent):
@@ -31,51 +29,29 @@ class CodeActAgent(BaseAgent):
31
29
  memory: BaseCheckpointSaver | None = None,
32
30
  tools: ToolConfig | None = None,
33
31
  registry: ToolRegistry | None = None,
34
- *,
35
- reflection_prompt: str = None,
36
- reflection_model: BaseChatModel = None,
37
- max_reflections: int = 3,
32
+ sandbox_timeout: int = 20,
38
33
  **kwargs,
39
34
  ):
40
35
  super().__init__(name, instructions, model, memory, **kwargs)
41
- self.model_instance = load_chat_model(model)
36
+ self.model_instance = load_chat_model(model, thinking=False)
42
37
  self.tools_config = tools or {}
43
38
  self.registry = registry
44
39
  self.eval_fn = eval_unsafe
45
- self.reflection_prompt = reflection_prompt
46
- self.reflection_model = reflection_model or self.model_instance
47
- self.max_reflections = max_reflections if reflection_prompt else 0
48
- self.tools_context = {}
49
- self.processed_tools: list[Union[StructuredTool, Callable]] = []
40
+ self.sandbox_timeout = sandbox_timeout
41
+ self.processed_tools: list[StructuredTool | Callable] = []
50
42
 
51
43
  async def _build_graph(self):
52
44
  if self.tools_config:
53
45
  if not self.registry:
54
46
  raise ValueError("Tools are configured but no registry is provided")
55
47
  # Langchain tools are fine
56
- exported_tools = await self.registry.export_tools(
57
- self.tools_config, ToolFormat.LANGCHAIN
58
- )
59
- self.processed_tools = [
60
- t if isinstance(t, StructuredTool) else create_tool(t)
61
- for t in exported_tools
62
- ]
48
+ exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
49
+ self.processed_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in exported_tools]
63
50
 
64
- self.instructions = create_default_prompt(
65
- self.processed_tools, self.instructions
66
- )
67
-
68
- for tool in self.processed_tools:
69
- safe_name = make_safe_function_name(tool.name)
70
- tool_callable = (
71
- tool.coroutine
72
- if hasattr(tool, "coroutine") and tool.coroutine is not None
73
- else tool.func
74
- )
75
- self.tools_context[safe_name] = tool_callable
51
+ self.instructions = create_default_prompt(self.processed_tools, self.instructions)
76
52
 
77
53
  agent = StateGraph(CodeActState)
78
- agent.add_node("call_model", lambda state, config: self.call_model(state, config))
54
+ agent.add_node("call_model", self.call_model)
79
55
  agent.add_node("sandbox", self.sandbox)
80
56
 
81
57
  agent.set_entry_point("call_model")
@@ -91,81 +67,72 @@ class CodeActAgent(BaseAgent):
91
67
  return agent.compile(checkpointer=self.memory)
92
68
 
93
69
  def should_run_sandbox(self, state: CodeActState) -> str:
70
+ last_message = state["messages"][-1]
71
+ if isinstance(last_message.content, str) and "TASK_COMPLETE" in last_message.content:
72
+ return END
73
+
94
74
  if state.get("script"):
95
75
  return "sandbox"
96
76
  return END
97
77
 
98
- def call_model(self, state: CodeActState, config: dict) -> dict:
99
- context = config.get("context", {})
100
- instructions = context.get("system_prompt", self.instructions)
101
- model = self.model_instance
102
- reflection_model = self.reflection_model
103
-
104
- messages = [{"role": "system", "content": instructions}] + state["messages"]
105
-
106
- response = model.invoke(messages)
107
-
108
- code = extract_and_combine_codeblocks(response.content)
109
-
110
- if self.max_reflections > 0 and code:
111
- reflection_count = 0
112
- while reflection_count < self.max_reflections:
113
- conversation_history = "\n".join(
114
- [
115
- f'<message role="{("user" if m.type == "human" else "assistant")}">\n{m.content}\n</message>'
116
- for m in state["messages"]
117
- ]
118
- )
119
- conversation_history += (
120
- f'\n<message role="assistant">\n{response.content}\n</message>'
121
- )
78
+ def _extract_content(self, response: AIMessageChunk) -> str:
79
+ if isinstance(response.content, list):
80
+ content = " ".join([c.get("text", "") for c in response.content])
81
+ else:
82
+ content = response.content
83
+ return content
122
84
 
123
- formatted_prompt = REFLECTION_PROMPT.format(
124
- conversation_history=conversation_history
125
- )
85
+ async def call_model(self, state: CodeActState) -> dict:
86
+ logger.debug(f"Calling model with state: {state}")
87
+ model = self.model_instance
126
88
 
127
- reflection_messages = [
128
- {"role": "system", "content": self.reflection_prompt},
129
- {"role": "user", "content": formatted_prompt},
130
- ]
131
- reflection_result = reflection_model.invoke(reflection_messages)
89
+ # Find the last script and its output in the message history
90
+ previous_script = state.get("script", "")
91
+ sandbox_output = state.get("sandbox_output", "")
132
92
 
133
- if "NONE" in reflection_result.content:
134
- break
93
+ logger.debug(f"Previous script: {previous_script}")
94
+ logger.debug(f"Sandbox output: {sandbox_output}")
135
95
 
136
- retry_prompt = RETRY_PROMPT.format(
137
- reflection_result=reflection_result.content
96
+ prompt_messages = [
97
+ {"role": "system", "content": self.instructions},
98
+ *state["messages"],
99
+ ]
100
+ if previous_script:
101
+ feedback_message = (
102
+ f"Here is the script you generated in the last turn:\n\n```python\n{previous_script}\n```\n\n"
103
+ )
104
+ if sandbox_output:
105
+ feedback_message += (
106
+ f"When executed, it produced the following output:\n\n```\n{sandbox_output}\n```\n\n"
138
107
  )
108
+ feedback_message += "Based on this, please generate a new, improved script to continue the task. Remember to replace the old script entirely."
109
+ prompt_messages.append({"role": "user", "content": feedback_message})
139
110
 
140
- regeneration_messages = [
141
- {"role": "system", "content": instructions},
142
- *state["messages"],
143
- {"role": "assistant", "content": response.content},
144
- {"role": "user", "content": retry_prompt},
145
- ]
146
- response = model.invoke(regeneration_messages)
111
+ logger.debug(f"Prompt messages: {prompt_messages}")
147
112
 
148
- code = extract_and_combine_codeblocks(response.content)
113
+ response = await model.ainvoke(prompt_messages)
114
+ logger.debug(f"Model response: {response}")
149
115
 
150
- if not code:
151
- break
116
+ text_content = self._extract_content(response)
117
+ if not isinstance(text_content, str):
118
+ raise ValueError(f"Content is not a string: {text_content}")
119
+ code = extract_and_combine_codeblocks(text_content)
120
+ logger.debug(f"Extracted code: {code}")
152
121
 
153
- reflection_count += 1
154
-
155
- if code:
156
- return {"messages": [response], "script": code}
157
- else:
158
- return {"messages": [response], "script": None}
122
+ return {"messages": [response], "script": code}
159
123
 
160
124
  async def sandbox(self, state: CodeActState) -> dict:
161
- existing_context = state.get("context", {})
162
- context = {**existing_context, **self.tools_context}
163
- if inspect.iscoroutinefunction(self.eval_fn):
164
- output, new_vars = await self.eval_fn(state["script"], context)
165
- else:
166
- output, new_vars = self.eval_fn(state["script"], context)
167
- new_context = {**existing_context, **new_vars}
125
+ logger.debug(f"Running sandbox with state: {state}")
126
+ tools_context = {}
127
+ for tool in self.processed_tools:
128
+ safe_name = make_safe_function_name(tool.name)
129
+ tool_callable = tool.coroutine if hasattr(tool, "coroutine") and tool.coroutine is not None else tool.func
130
+ tools_context[safe_name] = tool_callable
131
+
132
+ output, _ = await self.eval_fn(state["script"], tools_context, self.sandbox_timeout)
133
+ logger.debug(f"Sandbox output: {output}")
168
134
  return {
169
- "messages": [{"role": "user", "content": output}],
170
- "context": new_context,
171
- }
135
+ "messages": [AIMessageChunk(content=output.strip())],
136
+ "script": None,
137
+ "sandbox_output": output.strip(),
138
+ }
@@ -1,8 +1,8 @@
1
1
  import inspect
2
2
  import re
3
- from typing import Optional, Sequence
3
+ from collections.abc import Sequence
4
4
 
5
- from langchain_core.tools import StructuredTool, tool as create_tool
5
+ from langchain_core.tools import StructuredTool
6
6
 
7
7
 
8
8
  def make_safe_function_name(name: str) -> str:
@@ -20,23 +20,34 @@ def make_safe_function_name(name: str) -> str:
20
20
 
21
21
  def create_default_prompt(
22
22
  tools: Sequence[StructuredTool],
23
- base_prompt: Optional[str] = None,
23
+ base_prompt: str | None = None,
24
24
  ):
25
25
  """Create default prompt for the CodeAct agent."""
26
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.
27
+ prompt += """You are a Python programmer. You will be given a task to perform.
28
+ Your goal is to write a self-contained Python script to accomplish the task.
29
+
30
+ In each turn, you will generate a complete Python script. The script will be executed in a fresh, stateless environment.
31
+ You will be given the previous script you generated and the output it produced.
32
+ Your task is to analyze the output to find errors or opportunities for improvement, and then generate a new, improved script.
33
+ You must take the previous script as a starting point and replace it with a new one that moves closer to the final solution.
34
+ Your final script must be a single, complete piece of code that can be executed independently.
35
+
36
+ The script must follow this structure:
37
+ 1. All necessary imports at the top.
38
+ 2. An `async def main():` function containing the core logic.
39
+ 3. Do NOT include any code outside of the `async def main()` function, and do NOT call it. The execution environment handles this.
40
+
41
+ Any output you want to see from the code should be printed to the console from within the `main` function.
42
+ Code should be output in a fenced code block (e.g. ```python ... ```).
43
+
44
+ If you need to ask for more information or provide the final answer, you can output text to be shown directly to the user.
30
45
 
31
46
  In addition to the Python Standard Library, you can use the following functions:"""
32
47
 
33
48
  for tool in tools:
34
49
  # Use coroutine if it exists, otherwise use func
35
- tool_callable = (
36
- tool.coroutine
37
- if hasattr(tool, "coroutine") and tool.coroutine is not None
38
- else tool.func
39
- )
50
+ tool_callable = tool.coroutine if hasattr(tool, "coroutine") and tool.coroutine is not None else tool.func
40
51
  # Create a safe function name
41
52
  safe_name = make_safe_function_name(tool.name)
42
53
  # Determine if it's an async function
@@ -44,20 +55,19 @@ In addition to the Python Standard Library, you can use the following functions:
44
55
  # Add appropriate function definition
45
56
  prompt += f'''\n{"async " if is_async else ""}def {safe_name}{str(inspect.signature(tool_callable))}:
46
57
  """{tool.description}"""
47
- ...
58
+ ...
48
59
  '''
49
60
 
50
- prompt += """
51
-
52
- Variables defined at the top level of previous code snippets can be referenced in your code.
53
-
54
- 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'.
61
+ prompt += """\n\n\nAlways 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'.
55
62
 
56
63
  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.
57
64
 
58
65
  IMPORTANT CODING STRATEGY:
59
- 1. Only write code up to the point where you make an API call/tool usage with an output
60
- 2. Print the type/shape and a sample entry of this output, and using that knowledge proceed to write the further code
66
+ 1. All your code must be inside an `async def main()` function.
67
+ 2. Do NOT import `asyncio` or call `main()`. The execution environment handles this.
68
+ 3. Since many of the provided tools are async, you must use `await` to call them from within `main()`.
69
+ 4. Write code up to the point where you make an API call/tool usage with an output.
70
+ 5. Print the type/shape and a sample entry of this output, and using that knowledge proceed to write the further code.
61
71
 
62
72
  This means:
63
73
  - Write code that makes the API call or tool usage
@@ -66,27 +76,7 @@ This means:
66
76
  - Print a sample entry: print(f"Sample: {result[0] if isinstance(result, (list, tuple)) and len(result) > 0 else result}")
67
77
  - Then, based on this knowledge, write the code to process/use this data
68
78
 
69
- Reminder: use Python code snippets to call tools"""
70
- return prompt
71
-
79
+ Reminder: use Python code snippets to call tools
72
80
 
73
- REFLECTION_PROMPT = """
74
- Review the assistant's latest code for as per the quality rules:
75
-
76
- <conversation_history>
77
- {conversation_history}
78
- </conversation_history>
79
-
80
- If you find ANY of these issues, describe the problem briefly and clearly.
81
- If NO issues are found, respond with EXACTLY: "NONE"
82
- """
83
-
84
- RETRY_PROMPT = """
85
- I need you to completely regenerate your previous response based on this feedback:
86
-
87
- '''
88
- {reflection_result}
89
- '''
90
-
91
- DO NOT reference the feedback directly. Instead, provide a completely new response that addresses the issues.
92
- """
81
+ When you have completely finished the task, present the final result from your script to the user in a clean and readable Markdown format. Do not just summarize what you did; provide the actual output. For example, if you were asked to find unsubscribe links and your script found them, your final response should be a Markdown-formatted list of those links. After you have provided the final output, you MUST end your response with the exact phrase "TASK_COMPLETE"."""
82
+ return prompt
@@ -4,45 +4,36 @@ import contextlib
4
4
  import io
5
5
  from typing import Any
6
6
 
7
+ from loguru import logger
7
8
 
8
- async def eval_unsafe(code: str, _locals: dict[str, Any]) -> tuple[str, dict[str, Any]]:
9
- """
10
- Execute code in a non-blocking way and return the output and changed variables.
11
- """
9
+
10
+ async def eval_unsafe(code: str, _locals: dict[str, Any], timeout: int = 10) -> tuple[str, dict[str, Any]]:
11
+ """Executes a string of Python code in a sandboxed environment."""
12
+ # Store original keys before execution
13
+ original_keys = set(_locals.keys())
12
14
  result = f"Executing code...\n{code}\n\nOutput:\n"
13
15
  result += "=" * 50 + "\n"
14
-
15
- # Create a combined globals/locals environment that includes builtins
16
- # and the provided context. This allows nested functions to access tools.
17
- execution_env = {**builtins.__dict__, **_locals}
18
-
19
- def sync_eval_in_thread():
20
- """Synchronously execute code and capture output."""
21
- try:
22
- with contextlib.redirect_stdout(io.StringIO()) as f:
23
- exec(code, execution_env)
24
- output = f.getvalue()
25
- if not output:
26
- output = "<code ran, no output printed to stdout>"
27
- return output
28
- except Exception as e:
29
- return f"Error during execution: {repr(e)}"
30
-
31
- # Run the synchronous exec in a separate thread to avoid blocking the event loop.
32
- output = await asyncio.to_thread(sync_eval_in_thread)
33
- result += output
34
-
35
- # Identify all variables that are not part of the original builtins
36
- # and were not in the initial _locals, or were changed.
37
- changed_vars = {}
38
- builtin_keys = set(builtins.__dict__.keys())
39
-
40
- for key, value in execution_env.items():
41
- if key in builtin_keys:
42
- continue # Skip builtins
43
-
44
- # Check if the key is new or if the value has changed
45
- if key not in _locals or _locals[key] is not value:
46
- changed_vars[key] = value
47
-
48
- return result, changed_vars
16
+ try:
17
+ logger.debug(f"Executing code with timeout {timeout}")
18
+ with contextlib.redirect_stdout(io.StringIO()) as f:
19
+ # Execute the code in the provided locals context
20
+ # This should define an async function `main`
21
+ exec(code, builtins.__dict__, _locals)
22
+
23
+ if "main" in _locals and asyncio.iscoroutinefunction(_locals["main"]):
24
+ # Run the main async function
25
+ await asyncio.wait_for(_locals["main"](), timeout=timeout)
26
+ else:
27
+ result += "\nError: No `async def main()` function found in the script."
28
+
29
+ output = f.getvalue()
30
+ result += output
31
+ if not output:
32
+ result += "<code ran, no output printed to stdout>"
33
+ except Exception as e:
34
+ result += f"Error during execution: {repr(e)}"
35
+
36
+ # Determine new variables created during execution
37
+ new_keys = set(_locals.keys()) - original_keys
38
+ new_vars = {key: _locals[key] for key in new_keys}
39
+ return result, new_vars
@@ -1,12 +1,9 @@
1
- from typing import Any, Optional
2
-
3
1
  from langgraph.graph import MessagesState
2
+ from pydantic import Field
4
3
 
5
4
 
6
5
  class CodeActState(MessagesState):
7
6
  """State for CodeAct agent."""
8
7
 
9
- script: Optional[str]
10
- """The Python code script to be executed."""
11
- context: dict[str, Any]
12
- """Dictionary containing the execution context with available tools and variables."""
8
+ script: str | None = Field(default=None, description="The Python code script to be executed.")
9
+ sandbox_output: str | None = Field(default=None, description="The output of the Python code script execution.")
@@ -1,5 +1,7 @@
1
1
  import re
2
2
 
3
+ from universal_mcp.logger import logger
4
+
3
5
  BACKTICK_PATTERN = r"(?:^|\n)```(.*?)(?:```(?:\n|$))"
4
6
 
5
7
 
@@ -37,7 +39,12 @@ def extract_and_combine_codeblocks(text: str) -> str:
37
39
  """
38
40
  # Find all code blocks in the text using regex
39
41
  # Pattern matches anything between triple backticks, with or without a language identifier
40
- code_blocks = re.findall(BACKTICK_PATTERN, text, re.DOTALL)
42
+ try:
43
+ code_blocks = re.findall(BACKTICK_PATTERN, text, re.DOTALL)
44
+ except Exception as e:
45
+ logger.error(f"Error extracting code blocks: {e}")
46
+ logger.error(f"Text: {text}")
47
+ return ""
41
48
 
42
49
  if not code_blocks:
43
50
  return ""
@@ -46,15 +53,15 @@ def extract_and_combine_codeblocks(text: str) -> str:
46
53
  processed_blocks = []
47
54
  for block in code_blocks:
48
55
  # Strip leading and trailing whitespace
49
- block = block.strip()
56
+ cleaned_block = block.strip()
50
57
 
51
58
  # If the first line looks like a language identifier, remove it
52
- lines = block.split("\n")
59
+ lines = cleaned_block.split("\n")
53
60
  if lines and (not lines[0].strip() or " " not in lines[0].strip()):
54
61
  # First line is empty or likely a language identifier (no spaces)
55
- block = "\n".join(lines[1:])
62
+ cleaned_block = "\n".join(lines[1:])
56
63
 
57
- processed_blocks.append(block)
64
+ processed_blocks.append(cleaned_block)
58
65
 
59
66
  # Combine all codeblocks with newlines between them
60
67
  combined_code = "\n\n".join(processed_blocks)
@@ -63,12 +63,7 @@ def handle_interrupt(interrupt: Interrupt) -> str | bool:
63
63
  value = input("Do you accept this? (y/n): " + interrupt.value["question"])
64
64
  return value.lower() in ["y", "yes"]
65
65
  elif interrupt_type == "choice":
66
- value = input(
67
- "Enter your choice: "
68
- + interrupt.value["question"]
69
- + " "
70
- + ", ".join(interrupt.value["choices"])
71
- )
66
+ value = input("Enter your choice: " + interrupt.value["question"] + " " + ", ".join(interrupt.value["choices"]))
72
67
  if value in interrupt.value["choices"]:
73
68
  return value
74
69
  else: