universal-mcp-agents 0.1.23rc3__py3-none-any.whl → 0.1.23rc4__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.

@@ -15,16 +15,10 @@ def get_agent(
15
15
  return ReactAgent
16
16
  elif agent_name == "simple":
17
17
  return SimpleAgent
18
- elif agent_name == "builder":
19
- return BuilderAgent
20
- elif agent_name == "bigtool":
21
- return BigToolAgent
22
18
  elif agent_name == "codeact-repl":
23
19
  return CodeActPlaybookAgent
24
20
  else:
25
- raise ValueError(
26
- f"Unknown agent: {agent_name}. Possible values: react, simple, builder, bigtool, codeact-repl"
27
- )
21
+ raise ValueError(f"Unknown agent: {agent_name}. Possible values: react, simple, codeact-repl")
28
22
 
29
23
 
30
24
  __all__ = [
@@ -64,7 +64,7 @@ class BaseAgent:
64
64
  stream_mode=["messages", "custom"],
65
65
  stream_usage=True,
66
66
  ):
67
- if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2:
67
+ if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2: # noqa: PLR2004
68
68
  payload, meta_dict = meta
69
69
  is_agent_builder = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "agent_builder"
70
70
  additional_kwargs = getattr(payload, "additional_kwargs", {}) or {}
@@ -1,6 +1,6 @@
1
1
  from typing import Annotated
2
2
 
3
- from langgraph.prebuilt.chat_agent_executor import AgentState
3
+ from langchain.agents import AgentState
4
4
 
5
5
 
6
6
  def _enqueue(left: list, right: list) -> list:
@@ -18,7 +18,7 @@ async def main():
18
18
  memory=memory,
19
19
  )
20
20
  print("Starting agent...")
21
- result = await agent.invoke(user_input="load all the tools of reddit which can be used to search subreddit")
21
+ result = await agent.invoke(user_input="Check my google calendar and show my todays agenda")
22
22
  print(messages_to_list(result["messages"]))
23
23
 
24
24
 
@@ -3,10 +3,10 @@ import json
3
3
  import re
4
4
  import uuid
5
5
  from typing import Literal, cast
6
+ from types import SimpleNamespace
6
7
 
7
8
  from langchain_anthropic import ChatAnthropic
8
9
  from langchain_core.messages import AIMessage, ToolMessage
9
- from langchain_core.tools import StructuredTool
10
10
  from langgraph.checkpoint.base import BaseCheckpointSaver
11
11
  from langgraph.graph import START, StateGraph
12
12
  from langgraph.types import Command, RetryPolicy, StreamWriter
@@ -22,13 +22,12 @@ from universal_mcp.agents.codeact0.prompts import (
22
22
  create_default_prompt,
23
23
  )
24
24
  from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
25
- from universal_mcp.agents.codeact0.state import CodeActState, AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan
25
+ from universal_mcp.agents.codeact0.state import AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan, CodeActState
26
26
  from universal_mcp.agents.codeact0.tools import (
27
27
  create_meta_tools,
28
28
  enter_agent_builder_mode,
29
- get_valid_tools,
30
29
  )
31
- from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string
30
+ from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string, create_agent_call
32
31
  from universal_mcp.agents.llm import load_chat_model
33
32
  from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
34
33
 
@@ -57,6 +56,8 @@ class CodeActPlaybookAgent(BaseAgent):
57
56
  self.registry = registry
58
57
  self.agent_builder_registry = agent_builder_registry
59
58
  self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
59
+
60
+
60
61
  self.tools_config = self.agent.tools if self.agent else {}
61
62
  self.eval_fn = eval_unsafe
62
63
  self.sandbox_timeout = sandbox_timeout
@@ -65,14 +66,11 @@ class CodeActPlaybookAgent(BaseAgent):
65
66
  }
66
67
  self.final_instructions = ""
67
68
  self.tools_context = {}
68
- self.exported_tools = []
69
69
 
70
- async def _build_graph(self):
70
+ async def _build_graph(self): # noqa: PLR0915
71
+ """Build the graph for the CodeAct Playbook Agent."""
71
72
  meta_tools = create_meta_tools(self.registry)
72
- additional_tools = [smart_print, meta_tools["web_search"]]
73
- self.additional_tools = [
74
- t if isinstance(t, StructuredTool) else StructuredTool.from_function(t) for t in additional_tools
75
- ]
73
+ self.additional_tools = [smart_print, meta_tools["web_search"]]
76
74
 
77
75
  if self.tools_config:
78
76
  if isinstance(self.tools_config, dict):
@@ -81,9 +79,8 @@ class CodeActPlaybookAgent(BaseAgent):
81
79
  ]
82
80
  if not self.registry:
83
81
  raise ValueError("Tools are configured but no registry is provided")
84
- await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
85
-
86
- await self.registry.export_tools(self.default_tools_config, ToolFormat.LANGCHAIN)
82
+ await self.registry.load_tools(self.tools_config) # Load the default tools
83
+ await self.registry.load_tools(self.default_tools_config) # Load more tools
87
84
 
88
85
  async def call_model(state: CodeActState) -> Command[Literal["execute_tools"]]:
89
86
  """This node now only ever binds the four meta-tools to the LLM."""
@@ -113,8 +110,7 @@ class CodeActPlaybookAgent(BaseAgent):
113
110
  tools=agent_facing_tools,
114
111
  tool_choice="auto",
115
112
  )
116
-
117
- response = cast(AIMessage, model_with_tools.invoke(messages))
113
+ response = cast(AIMessage, await model_with_tools.ainvoke(messages))
118
114
  if response.tool_calls:
119
115
  return Command(goto="execute_tools", update={"messages": [response]})
120
116
  else:
@@ -146,7 +142,10 @@ class CodeActPlaybookAgent(BaseAgent):
146
142
  )
147
143
  return Command(
148
144
  goto="agent_builder",
149
- update={"agent_builder_mode": "planning", "messages": [tool_message]}, # Entered Agent Builder mode
145
+ update={
146
+ "agent_builder_mode": "planning",
147
+ "messages": [tool_message],
148
+ }, # Entered Agent Builder mode
150
149
  )
151
150
  elif tool_name == "execute_ipython_cell":
152
151
  code = tool_call["args"]["snippet"]
@@ -162,7 +161,9 @@ class CodeActPlaybookAgent(BaseAgent):
162
161
  tool_result = output
163
162
  elif tool_name == "load_functions":
164
163
  # The tool now does all the work of validation and formatting.
165
- tool_result, new_context_for_sandbox, valid_tools, unconnected_links = await meta_tools["load_functions"].ainvoke(tool_args)
164
+ tool_result, new_context_for_sandbox, valid_tools, unconnected_links = await meta_tools[
165
+ "load_functions"
166
+ ].ainvoke(tool_args)
166
167
  # We still need to update the sandbox context for `execute_ipython_cell`
167
168
  new_tool_ids.extend(valid_tools)
168
169
  if new_tool_ids:
@@ -187,7 +188,7 @@ class CodeActPlaybookAgent(BaseAgent):
187
188
  tool_call_id=tool_call["id"],
188
189
  )
189
190
  tool_messages.append(tool_message)
190
-
191
+
191
192
  if ask_user:
192
193
  tool_messages.append(AIMessage(content=ai_msg))
193
194
  return Command(
@@ -209,7 +210,7 @@ class CodeActPlaybookAgent(BaseAgent):
209
210
  },
210
211
  )
211
212
 
212
- def agent_builder(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
213
+ async def agent_builder(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
213
214
  agent_builder_mode = state.get("agent_builder_mode")
214
215
  if agent_builder_mode == "planning":
215
216
  plan_id = str(uuid.uuid4())
@@ -217,8 +218,10 @@ class CodeActPlaybookAgent(BaseAgent):
217
218
  planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT
218
219
  messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
219
220
 
220
- model_with_structured_output = self.agent_builder_model_instance.with_structured_output(AgentBuilderPlan)
221
- response = model_with_structured_output.invoke(messages)
221
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
222
+ AgentBuilderPlan
223
+ )
224
+ response = await model_with_structured_output.ainvoke(messages)
222
225
  plan = cast(AgentBuilderPlan, response)
223
226
 
224
227
  writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
@@ -277,8 +280,10 @@ class CodeActPlaybookAgent(BaseAgent):
277
280
  meta_instructions = self.instructions + AGENT_BUILDER_META_PROMPT
278
281
  messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
279
282
 
280
- model_with_structured_output = self.agent_builder_model_instance.with_structured_output(AgentBuilderMeta)
281
- meta_response = model_with_structured_output.invoke(messages)
283
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
284
+ AgentBuilderMeta
285
+ )
286
+ meta_response = await model_with_structured_output.ainvoke(messages)
282
287
  meta = cast(AgentBuilderMeta, meta_response)
283
288
  name, description = meta.name, meta.description
284
289
 
@@ -316,8 +321,10 @@ class CodeActPlaybookAgent(BaseAgent):
316
321
  generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT
317
322
  messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
318
323
 
319
- model_with_structured_output = self.agent_builder_model_instance.with_structured_output(AgentBuilderCode)
320
- response = model_with_structured_output.invoke(messages)
324
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
325
+ AgentBuilderCode
326
+ )
327
+ response = await model_with_structured_output.ainvoke(messages)
321
328
  func_code = cast(AgentBuilderCode, response).code
322
329
 
323
330
  # Extract function name (handle both regular and async functions)
@@ -367,23 +374,35 @@ class CodeActPlaybookAgent(BaseAgent):
367
374
  },
368
375
  }
369
376
  )
377
+ mock_exit_tool_call = {
378
+ "name": "exit_agent_builder_mode",
379
+ "args": {},
380
+ "id": "exit_builder_1"
381
+ }
370
382
  mock_assistant_message = AIMessage(
371
383
  content=json.dumps(response.model_dump()),
384
+ tool_calls=[mock_exit_tool_call],
372
385
  additional_kwargs={
373
386
  "type": "generating",
374
387
  "id": str(res.id),
375
388
  "update": bool(self.agent),
376
- "name": final_name,
389
+ "name": final_name.replace(" ", "_"),
377
390
  "description": final_description,
378
391
  },
379
392
  )
393
+
394
+ mock_exit_tool_response = ToolMessage(
395
+ content=json.dumps("Exited Agent Builder Mode. Enter this mode again if you need to modify the saved agent."),
396
+ name="exit_agent_builder_mode",
397
+ tool_call_id="exit_builder_1"
398
+ )
380
399
 
381
- return Command(update={"messages": [mock_assistant_message], "agent_builder_mode": "normal"})
400
+ return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "agent_builder_mode": "normal"})
382
401
 
383
- async def route_entry(state: CodeActState) -> Literal["call_model", "agent_builder"]:
402
+ async def route_entry(state: CodeActState) -> Command[Literal["call_model", "agent_builder", "execute_tools"]]:
384
403
  """Route to either normal mode or agent builder creation"""
385
- all_tools = await self.registry.export_tools(state["selected_tool_ids"], ToolFormat.LANGCHAIN)
386
- # print(all_tools)
404
+ await self.registry.load_tools(state["selected_tool_ids"])
405
+ all_tools = await self.registry.export_tools(format=ToolFormat.NATIVE)
387
406
 
388
407
  # Create the initial system prompt and tools_context in one go
389
408
  self.final_instructions, self.tools_context = create_default_prompt(
@@ -394,13 +413,19 @@ class CodeActPlaybookAgent(BaseAgent):
394
413
  self.agent,
395
414
  is_initial_prompt=True,
396
415
  )
416
+ if len(state['messages']) == 1 and self.agent: # Inject the agent's script function into add_context for execution
417
+ script = self.agent.instructions.get('script')
418
+ add_context = {"functions":[script]}
419
+ return Command(goto="call_model", update = {"add_context": add_context})
420
+
397
421
  if state.get("agent_builder_mode") in ["planning", "confirming", "generating"]:
398
- return "agent_builder"
399
- return "call_model"
422
+ return Command(goto="agent_builder")
423
+ return Command(goto="call_model")
400
424
 
401
425
  agent = StateGraph(state_schema=CodeActState)
402
426
  agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
403
427
  agent.add_node(agent_builder)
404
428
  agent.add_node(execute_tools)
405
- agent.add_conditional_edges(START, route_entry)
429
+ agent.add_node(route_entry)
430
+ agent.add_edge(START, "route_entry")
406
431
  return agent.compile(checkpointer=self.memory)
@@ -1,10 +1,8 @@
1
1
  import inspect
2
2
  import re
3
- from collections.abc import Sequence
3
+ from collections.abc import Callable
4
4
 
5
- from langchain_core.tools import StructuredTool
6
-
7
- from universal_mcp.agents.codeact0.utils import schema_to_signature
5
+ from loguru import logger
8
6
 
9
7
  uneditable_prompt = """
10
8
  You are **Ruzo**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
@@ -14,7 +12,7 @@ Your job is to answer the user's question or perform the task they ask for.
14
12
  - For task requiring operations or access to external resources, you should achieve the task by executing Python code snippets.
15
13
  - You have access to `execute_ipython_cell` tool that allows you to execute Python code in an IPython notebook cell.
16
14
  - You also have access to two tools for finding and loading more python functions- `search_functions` and `load_functions`, which you must use for finding functions for using different external applications or additional functionality.
17
- - Prioritize connected applications over unconnected ones from the output of `search_functions`.
15
+ - Prioritize connected applications over unconnected ones from the output of `search_functions`. However, if the user specifically asks for an application, you MUST use that irrespective of connection status.
18
16
  - When multiple apps are connected, or none of the apps are connected, YOU MUST ask the user to choose the application(s). The search results will inform you when such a case occurs, and you must stop and ask the user if multiple apps are relevant.
19
17
  - In writing or natural language processing tasks DO NOT answer directly. Instead use `execute_ipython_cell` tool with the AI functions provided to you for tasks like summarizing, text generation, classification, data extraction from text or unstructured data, etc. Avoid hardcoded approaches to classification, data extraction, or creative writing.
20
18
  - The code you write will be executed in a sandbox environment, and you can use the output of previous executions in your code. variables, functions, imports are retained.
@@ -26,6 +24,7 @@ Your job is to answer the user's question or perform the task they ask for.
26
24
  - Read and understand the output of the previous code snippet and use it to answer the user's request. Note that the code output is NOT visible to the user, so after the task is complete, you have to give the output to the user in a markdown format. Similarly, you should only use print/smart_print for your own analysis, the user does not get the output.
27
25
  - If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
28
26
  - Always describe in 2-3 lines about the current progress. In each step, mention what has been achieved and what you are planning to do next.
27
+ - DO NOT use the code execution to communicate with the user. The user is not able to see the output of the code cells.
29
28
 
30
29
  **Coding Best Practices:**
31
30
  - Variables defined at the top level of previous code snippets can be referenced in your code.
@@ -72,7 +71,7 @@ You must output a JSON object with a single key "steps", which is a list of stri
72
71
 
73
72
  Your plan should:
74
73
  1. Identify the key steps in the workflow
75
- 2. Mark user-specific variables that should become the main agent function parameters using `variable_name` syntax. Intermediate variables should not be highlighted using ``
74
+ 2. Mark user-specific variables that should become the main agent function parameters using `variable_name` syntax. Intermediate variables MUST not be highlighted using ``
76
75
  3. Keep the logic generic and reusable
77
76
  4. Be clear and concise
78
77
 
@@ -95,7 +94,8 @@ Your response must be ONLY the Python code for the function.
95
94
  Do not include any other text, markdown, or explanations in your response.
96
95
  Your response should start with `def` or `async def`.
97
96
  The function should be a single, complete piece of code that can be executed independently, based on previously executed code snippets that executed correctly.
98
- The parameters of the function should be the same as the final confirmed agent plan.
97
+ The parameters of the function MUST be exactly the same as the final confirmed agent plan. The variables will are indicated using `` in the plan.
98
+ Any additional functions you require should be child functions inside the main top level function, and thus the first function to appear must be the main agent executable function.
99
99
  """
100
100
 
101
101
 
@@ -132,9 +132,23 @@ def make_safe_function_name(name: str) -> str:
132
132
  return safe_name
133
133
 
134
134
 
135
+ def build_tool_definitions(tools: list[Callable]) -> tuple[list[str], dict[str, Callable]]:
136
+ tool_definitions = []
137
+ context = {}
138
+ for tool in tools:
139
+ tool_name = tool.__name__
140
+ tool_definitions.append(
141
+ f'''{"async " if inspect.iscoroutinefunction(tool) else ""}def {tool_name} {str(inspect.signature(tool))}:
142
+ """{tool.__doc__}"""
143
+ ...'''
144
+ )
145
+ context[tool_name] = tool
146
+ return tool_definitions, context
147
+
148
+
135
149
  def create_default_prompt(
136
- tools: Sequence[StructuredTool],
137
- additional_tools: Sequence[StructuredTool],
150
+ tools: list[Callable],
151
+ additional_tools: list[Callable],
138
152
  base_prompt: str | None = None,
139
153
  apps_string: str | None = None,
140
154
  agent: object | None = None,
@@ -150,39 +164,7 @@ def create_default_prompt(
150
164
  else:
151
165
  system_prompt = ""
152
166
 
153
- tools_context = {}
154
- tool_definitions = []
155
-
156
- for tool in tools:
157
- if hasattr(tool, "func") and tool.func is not None:
158
- tool_callable = tool.func
159
- is_async = False
160
- elif hasattr(tool, "coroutine") and tool.coroutine is not None:
161
- tool_callable = tool.coroutine
162
- is_async = True
163
- tool_definitions.append(
164
- f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
165
- """{tool.description}"""
166
- ...'''
167
- )
168
- safe_name = make_safe_function_name(tool.name)
169
- tools_context[safe_name] = tool_callable
170
-
171
- for tool in additional_tools:
172
- if hasattr(tool, "func") and tool.func is not None:
173
- tool_callable = tool.func
174
- is_async = False
175
- elif hasattr(tool, "coroutine") and tool.coroutine is not None:
176
- tool_callable = tool.coroutine
177
- is_async = True
178
- tool_definitions.append(
179
- f'''{"async " if is_async else ""}def {tool.name} {str(inspect.signature(tool_callable))}:
180
- """{tool.description}"""
181
- ...'''
182
- )
183
- safe_name = make_safe_function_name(tool.name)
184
- tools_context[safe_name] = tool_callable
185
-
167
+ tool_definitions, tools_context = build_tool_definitions(tools + additional_tools)
186
168
  system_prompt += "\n".join(tool_definitions)
187
169
 
188
170
  if is_initial_prompt:
@@ -198,7 +180,7 @@ def create_default_prompt(
198
180
  plan = pb.get("plan")
199
181
  code = pb.get("script")
200
182
  if plan or code:
201
- system_prompt += "\n\nExisting Agent Provided:\n"
183
+ system_prompt += "\n\nYou have been provided an existing agent plan and code for performing a task.:\n"
202
184
  if plan:
203
185
  if isinstance(plan, list):
204
186
  plan_block = "\n".join(f"- {str(s)}" for s in plan)
@@ -206,7 +188,7 @@ def create_default_prompt(
206
188
  plan_block = str(plan)
207
189
  system_prompt += f"Plan Steps:\n{plan_block}\n"
208
190
  if code:
209
- system_prompt += f"\nScript:\n```python\n{str(code)}\n```\n"
191
+ system_prompt += f"\nScript:\n```python\n{str(code)}\n```\nThis function can be called by you using `execute_ipython_code`, either directly or using asyncio.run (if an async function). Do NOT redefine the function, unless it has to be modified. For modifying it, you must enter agent_builder mode first so that it is modified in the database and not just the chat locally."
210
192
  except Exception:
211
193
  # Silently ignore formatting issues
212
194
  pass
@@ -26,6 +26,7 @@ def eval_unsafe(
26
26
  EXCLUDE_TYPES = (
27
27
  types.ModuleType,
28
28
  type(re.match("", "")),
29
+ type(re.compile("")),
29
30
  type(threading.Lock()),
30
31
  type(threading.RLock()),
31
32
  threading.Event,
@@ -44,7 +45,7 @@ def eval_unsafe(
44
45
  exec(code, _locals, _locals)
45
46
  result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
46
47
  except Exception as e:
47
- result_container["output"] = "Error during execution: " + str(e)
48
+ result_container["output"] = f"Error during execution: {type(e).__name__}: {e}"
48
49
 
49
50
  thread = threading.Thread(target=target)
50
51
  thread.start()
@@ -1,6 +1,6 @@
1
1
  from typing import Annotated, Any
2
2
 
3
- from langgraph.prebuilt.chat_agent_executor import AgentState
3
+ from langchain.agents import AgentState
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
 
@@ -8,7 +8,7 @@ from pydantic import Field
8
8
  from universal_mcp.agentr.registry import AgentrRegistry
9
9
  from universal_mcp.types import ToolFormat
10
10
 
11
- from universal_mcp.agents.codeact0.prompts import create_default_prompt
11
+ from universal_mcp.agents.codeact0.prompts import build_tool_definitions
12
12
 
13
13
 
14
14
  def enter_agent_builder_mode():
@@ -181,7 +181,7 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
181
181
  result_parts.append("") # Empty line between apps
182
182
 
183
183
  # Add connection status information
184
- if len(connected_apps_in_results) == 0 and len(apps_in_results) > 0:
184
+ if len(connected_apps_in_results) == 0 and len(apps_in_results) > 1:
185
185
  result_parts.append(
186
186
  "Connection Status: None of the apps in the results are connected. You must ask the user to choose the application."
187
187
  )
@@ -192,7 +192,9 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
192
192
  )
193
193
 
194
194
  result_parts.append("Call load_functions to select the required functions only.")
195
- return "\n".join(result_parts)
195
+ if len(connected_apps_in_results)>len(apps_in_results):
196
+ result_parts.append("Unconnected app functions can also be loaded if required by the user, but prefer connected ones.")
197
+ return " ".join(result_parts)
196
198
 
197
199
  @tool
198
200
  async def load_functions(tool_ids: list[str]) -> str:
@@ -218,16 +220,17 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
218
220
  return "Error: None of the provided tool IDs could be validated or loaded."
219
221
 
220
222
  # Step 2: Export the schemas of the valid tools.
221
- all_exported_tools = await tool_registry.export_tools(valid_tools, ToolFormat.LANGCHAIN)
222
- exported_tools = [tool for tool in all_exported_tools if tool.name in valid_tools]
223
-
223
+ await tool_registry.load_tools(valid_tools)
224
+ exported_tools = await tool_registry.export_tools(
225
+ valid_tools, ToolFormat.NATIVE
226
+ ) # Get definition for only the new tools
224
227
 
225
228
  # Step 3: Build the informational string for the agent.
226
- tool_definitions, new_tools_context = create_default_prompt(exported_tools, [], is_initial_prompt=False)
229
+ tool_definitions, new_tools_context = build_tool_definitions(exported_tools)
227
230
 
228
231
  result_parts = [
229
232
  f"Successfully loaded {len(exported_tools)} functions. They are now available for use inside `execute_ipython_cell`:",
230
- tool_definitions,
233
+ "\n".join(tool_definitions),
231
234
  ]
232
235
 
233
236
  response_string = "\n\n".join(result_parts)
@@ -235,7 +238,6 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
235
238
 
236
239
  return response_string, new_tools_context, valid_tools, unconnected_links
237
240
 
238
- @tool
239
241
  async def web_search(query: str) -> dict:
240
242
  """
241
243
  Get an LLM answer to a question informed by Exa search results. Useful when you need information from a wide range of real-time sources on the web. Do not use this when you need to access contents of a specific webpage.
@@ -4,7 +4,7 @@ import re
4
4
  from collections.abc import Sequence
5
5
  from typing import Any
6
6
 
7
- from langchain_core.messages import BaseMessage
7
+ from langchain_core.messages import AIMessage, ToolMessage, BaseMessage
8
8
  from universal_mcp.types import ToolConfig
9
9
 
10
10
  MAX_CHARS = 5000
@@ -452,3 +452,78 @@ async def get_connected_apps_string(registry) -> str:
452
452
  return "\n".join(apps_list)
453
453
  except Exception:
454
454
  return "Unable to retrieve connected applications."
455
+
456
+
457
+ def create_agent_call(agent: object, agent_args: dict[str, Any]) -> AIMessage:
458
+ """Create an assistant tool-call message to execute the agent script.
459
+
460
+ This inspects the agent's generated script (expected at agent.instructions["script"]) to
461
+ locate the topmost function or async function, then constructs a Python snippet that:
462
+ - embeds the script as-is,
463
+ - deserializes the provided arguments as keyword arguments,
464
+ - invokes the detected function (awaiting it if async), and
465
+ - prints the result via smart_print.
466
+
467
+ If no top-level function is detected or the script cannot be parsed, a safe fallback
468
+ snippet is produced which simply prints the provided arguments.
469
+
470
+ Args:
471
+ agent: Object that provides an `instructions` mapping with a `script` string.
472
+ agent_args: Mapping of argument names to values to be passed as keyword args to the function.
473
+
474
+ Returns:
475
+ AIMessage: A synthetic assistant message containing a single tool call for
476
+ `execute_ipython_cell` with the constructed snippet.
477
+ """
478
+ content = "Running the agent with your provided parameters"
479
+ script = agent.instructions.get("script") if hasattr(agent, "instructions") else None
480
+ args = agent_args or {}
481
+
482
+ func_name = None
483
+ is_async = False
484
+
485
+ if isinstance(script, str) and script.strip():
486
+ try:
487
+ tree = ast.parse(script)
488
+ for node in tree.body:
489
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
490
+ func_name = node.name
491
+ is_async = isinstance(node, ast.AsyncFunctionDef)
492
+ break
493
+ except SyntaxError:
494
+ func_name = None
495
+
496
+ # Fallback content/snippet if no callable function is found
497
+ if not func_name:
498
+ snippet = (
499
+ "import asyncio\n\n# Test fallback: no function detected in script; printing args\n"
500
+ f"smart_print({repr(args)})\n"
501
+ )
502
+ else:
503
+ import json as _json
504
+ args_json = _json.dumps(args)
505
+ if is_async:
506
+ snippet = (
507
+ f"{script}\n\n"
508
+ "import asyncio, json\n"
509
+ f"_kwargs = json.loads('{args_json}')\n"
510
+ f"async def __runner():\n result = await {func_name}(**_kwargs)\n smart_print(result)\n"
511
+ "asyncio.run(__runner())\n"
512
+ )
513
+ else:
514
+ snippet = (
515
+ f"{script}\n\n"
516
+ "import json\n"
517
+ f"_kwargs = json.loads('{args_json}')\n"
518
+ f"result = {func_name}(**_kwargs)\n"
519
+ "smart_print(result)\n"
520
+ )
521
+
522
+ mock_agent_call = {
523
+ "name": "execute_ipython_cell",
524
+ "args": {"snippet": snippet},
525
+ "id": "initial_agent_call",
526
+ "type": "tool_call",
527
+ }
528
+ mock_assistant_message = AIMessage(content=content, tool_calls=[mock_agent_call])
529
+ return mock_assistant_message
@@ -1,5 +1,5 @@
1
+ from langchain.agents import create_agent
1
2
  from langgraph.checkpoint.base import BaseCheckpointSaver
2
- from langgraph.prebuilt import create_react_agent
3
3
  from loguru import logger
4
4
  from rich import print
5
5
  from universal_mcp.agentr.registry import AgentrRegistry
@@ -75,10 +75,10 @@ class ReactAgent(BaseAgent):
75
75
  tools = []
76
76
 
77
77
  logger.debug(f"Initialized ReactAgent: name={self.name}, model={self.model}")
78
- return create_react_agent(
78
+ return create_agent(
79
79
  self.llm,
80
80
  tools,
81
- prompt=self._build_system_message(),
81
+ system_prompt=self._build_system_message(),
82
82
  checkpointer=self.memory,
83
83
  )
84
84
 
@@ -1,90 +1,123 @@
1
+ import ast
2
+ import base64
1
3
  import contextlib
2
- import inspect
3
4
  import io
4
- import queue
5
- import re
6
- import socket
7
- import threading
8
- import types
9
- from typing import Any
5
+ import traceback
10
6
 
11
- from universal_mcp.agents.codeact0.utils import derive_context
7
+ import cloudpickle as pickle
12
8
 
13
9
 
14
10
  class Sandbox:
15
11
  """
16
- A class to execute code safely in a sandboxed environment with a timeout.
12
+ A simulated environment for executing Python code cells with context
13
+ maintained across multiple runs.
17
14
  """
18
15
 
19
- def __init__(self, timeout: int = 180):
16
+ def __init__(self):
17
+ # Dictionary to store variables (context) across runs
18
+ self.context = {}
19
+
20
+ def add_context(self, context: dict[str, any]):
21
+ """
22
+ Adds a dictionary of context to the sandbox.
23
+ """
24
+ self.context.update(context)
25
+
26
+ def save_context(self) -> str:
27
+ """
28
+ Saves the context to a base64 string.
29
+ """
30
+ pickled_data = pickle.dumps(self.context)
31
+ base64_encoded = base64.b64encode(pickled_data).decode("utf-8")
32
+ return base64_encoded
33
+
34
+ def load_context(self, context: str):
35
+ """
36
+ Loads the context from a base64 string.
20
37
  """
21
- Initializes the Sandbox.
38
+ pickled_data = base64.b64decode(context)
39
+ self.context = pickle.loads(pickled_data)
40
+
41
+ def run(self, code: str) -> dict[str, any]:
42
+ """
43
+ Executes the provided Python code string in the maintained context.
44
+
22
45
  Args:
23
- timeout: The timeout for code execution in seconds.
46
+ code (str): The Python code to execute.
47
+
48
+ Returns:
49
+ dict: A dictionary containing the execution results.
24
50
  """
25
- self.timeout = timeout
26
- self._locals: dict[str, Any] = {}
27
- self.add_context: dict[str, Any] = {}
51
+ # Prepare the execution environment:
52
+ # Use a copy of the context for execution locals/globals
53
+ exec_scope = self.context.copy()
54
+
55
+ stdout_capture = io.StringIO()
56
+ stderr_output = ""
57
+
58
+ # Use a true context manager for robust stdout capture
59
+ try:
60
+ with contextlib.redirect_stdout(stdout_capture):
61
+ # Execute the code. Using the same dictionary for globals and locals
62
+ # allows newly created variables to be visible immediately.
63
+ exec(code, exec_scope, exec_scope)
64
+
65
+ # Update the context with any new/modified variables
66
+ # Filter out dunder methods/system keys that might be introduced by exec
67
+ new_context = {k: v for k, v in exec_scope.items() if not k.startswith("__")}
68
+ self.context.update(new_context)
28
69
 
29
- def run(self, code: str) -> tuple[str, dict[str, Any], dict[str, Any]]:
70
+ except Exception:
71
+ # Capture the traceback for better error reporting (simulated stderr)
72
+ stderr_output = traceback.format_exc()
73
+
74
+ # The execution scope might contain partially defined variables,
75
+ # but we continue to maintain the *previous* valid context.
76
+ # We don't update self.context on failure to avoid polluting it.
77
+
78
+ return {"stdout": stdout_capture.getvalue(), "stderr": stderr_output, "success": stderr_output == ""}
79
+
80
+ def get_context(self) -> dict[str, any]:
81
+ """
82
+ Returns a copy of the current execution context.
83
+
84
+ Returns:
85
+ dict: A copy of the context dictionary.
86
+ """
87
+ return self.context.copy()
88
+
89
+ def reset(self):
30
90
  """
31
- Execute code safely with a timeout.
32
- - Returns (output_str, filtered_locals_dict, new_add_context)
33
- - Errors or timeout are returned as output_str.
34
- - Previous variables in _locals persist across calls.
91
+ Resets the sandbox's context, clearing all defined variables.
35
92
  """
93
+ self.context = {}
94
+
95
+ async def arun(self, code: str) -> dict[str, any]:
96
+ """
97
+ Asynchronously executes Python code, supporting top-level await.
98
+ """
99
+ # Use a copy of the context for execution
100
+ exec_scope = self.context.copy()
101
+ stdout_capture = io.StringIO()
102
+ stderr_output = ""
36
103
 
37
- EXCLUDE_TYPES = (
38
- types.ModuleType,
39
- type(re.match("", "")),
40
- type(threading.Lock()),
41
- type(threading.RLock()),
42
- threading.Event,
43
- threading.Condition,
44
- threading.Semaphore,
45
- queue.Queue,
46
- socket.socket,
47
- io.IOBase,
48
- )
49
-
50
- result_container = {"output": "<no output>"}
51
-
52
- def target():
53
- try:
54
- with contextlib.redirect_stdout(io.StringIO()) as f:
55
- exec(code, self._locals, self._locals)
56
- result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
57
- except Exception as e:
58
- result_container["output"] = "Error during execution: " + str(e)
59
-
60
- thread = threading.Thread(target=target)
61
- thread.start()
62
- thread.join(self.timeout)
63
-
64
- if thread.is_alive():
65
- result_container["output"] = f"Code timeout: code execution exceeded {self.timeout} seconds."
66
-
67
- # Filter locals for picklable/storable variables
68
- all_vars = {}
69
- for key, value in self._locals.items():
70
- if key == "__builtins__":
71
- continue
72
- if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
73
- continue
74
- if inspect.isasyncgen(value) or inspect.isasyncgenfunction(value):
75
- continue
76
- if isinstance(value, EXCLUDE_TYPES):
77
- continue
78
- if not callable(value) or not hasattr(value, "__name__"):
79
- all_vars[key] = value
80
-
81
- self._locals = all_vars
82
-
83
- # Safely derive context
84
104
  try:
85
- self.add_context = derive_context(code, self.add_context)
105
+ # Compile the code with the special flag to allow top-level await
106
+ compiled_code = compile(code, "<string>", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
107
+
108
+ with contextlib.redirect_stdout(stdout_capture):
109
+ # Eval the compiled code to get a coroutine
110
+ coroutine = eval(compiled_code, exec_scope, exec_scope)
111
+
112
+ # Await the coroutine to run the code if it's async
113
+ if coroutine:
114
+ await coroutine
115
+
116
+ # Update the context with any new/modified variables
117
+ new_context = {k: v for k, v in exec_scope.items() if not k.startswith("__")}
118
+ self.context.update(new_context)
119
+
86
120
  except Exception:
87
- # Keep the old context if derivation fails
88
- pass
121
+ stderr_output = traceback.format_exc()
89
122
 
90
- return result_container["output"], self._locals, self.add_context
123
+ return {"stdout": stdout_capture.getvalue(), "stderr": stderr_output, "success": stderr_output == ""}
@@ -1,23 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-agents
3
- Version: 0.1.23rc3
3
+ Version: 0.1.23rc4
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/universal-mcp/applications
6
6
  Project-URL: Repository, https://github.com/universal-mcp/applications
7
7
  Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
8
8
  License: MIT
9
9
  Requires-Python: >=3.11
10
+ Requires-Dist: cloudpickle>=3.1.1
10
11
  Requires-Dist: langchain-anthropic>=0.3.19
11
12
  Requires-Dist: langchain-google-genai>=2.1.10
12
13
  Requires-Dist: langchain-openai>=0.3.32
13
14
  Requires-Dist: langgraph>=0.6.6
14
- Requires-Dist: typer>=0.17.4
15
15
  Requires-Dist: universal-mcp-applications>=0.1.25
16
- Requires-Dist: universal-mcp>=0.1.24rc26
16
+ Requires-Dist: universal-mcp>=0.1.24rc27
17
17
  Provides-Extra: dev
18
18
  Requires-Dist: pre-commit; extra == 'dev'
19
19
  Requires-Dist: ruff; extra == 'dev'
20
+ Requires-Dist: typer>=0.17.4; extra == 'dev'
20
21
  Provides-Extra: test
21
- Requires-Dist: pytest-asyncio>=1.1.0; extra == 'test'
22
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == 'test'
22
23
  Requires-Dist: pytest-cov; extra == 'test'
23
24
  Requires-Dist: pytest<9.0.0,>=7.0.0; extra == 'test'
@@ -1,10 +1,10 @@
1
- universal_mcp/agents/__init__.py,sha256=bW7WJopR6YZSLxghLf8nhohhHPWzm0wdGoZlmKDAcZ4,1078
2
- universal_mcp/agents/base.py,sha256=sSC217rac89dEheeIRZoMSU_nk9UfSfQw-0E1BjNSIc,7393
1
+ universal_mcp/agents/__init__.py,sha256=Ythw8tyq7p-w1SPnuO2JtS4TvYEP75PkQpdyvZv-ww4,914
2
+ universal_mcp/agents/base.py,sha256=Sa3ws87OlMklXv9NAs_kXNAvP5DbaAUnFQbx1WqEStM,7410
3
3
  universal_mcp/agents/cli.py,sha256=9CG7majpWUz7C6t0d8xr-Sg2ZPKBuQdykTbYS6KIZ3A,922
4
4
  universal_mcp/agents/hil.py,sha256=_5PCK6q0goGm8qylJq44aSp2MadP-yCPvhOJYKqWLMo,3808
5
5
  universal_mcp/agents/llm.py,sha256=hVRwjZs3MHl5_3BWedmurs2Jt1oZDfFX0Zj9F8KH7fk,1787
6
- universal_mcp/agents/react.py,sha256=8XQvJ0HLVgc-K0qn9Ml48WGcgUGuIKtL67HatlT6Da0,3334
7
- universal_mcp/agents/sandbox.py,sha256=Int2O8JNFPlB8c7gb86KRxlNbuV0zdz5_NCo_GMcCds,2876
6
+ universal_mcp/agents/react.py,sha256=ocYm94HOiJVI2zwTjO1K2PNfVY7EILLJ6cd__jnGHPs,3327
7
+ universal_mcp/agents/sandbox.py,sha256=LL4OfavEzxbmTDcc_NxizRRpQnw5hc3G2bxvFY63scY,4241
8
8
  universal_mcp/agents/simple.py,sha256=NSATg5TWzsRNS7V3LFiDG28WSOCIwCdcC1g7NRwg2nM,2095
9
9
  universal_mcp/agents/utils.py,sha256=P6W9k6XAOBp6tdjC2VTP4tE0B2M4-b1EDmr-ylJ47Pw,7765
10
10
  universal_mcp/agents/bigtool/__init__.py,sha256=mZG8dsaCVyKlm82otxtiTA225GIFLUCUUYPEIPF24uw,2299
@@ -13,7 +13,7 @@ universal_mcp/agents/bigtool/agent.py,sha256=mtCDNN8WjE2hjJjooDqusmbferKBHeJMHrh
13
13
  universal_mcp/agents/bigtool/context.py,sha256=ny7gd-vvVpUOYAeQbAEUT0A6Vm6Nn2qGywxTzPBzYFg,929
14
14
  universal_mcp/agents/bigtool/graph.py,sha256=2Sy0dtevTWeT3hJDq4BDerZFvk_zJqx15j8VH2XLq8Y,5848
15
15
  universal_mcp/agents/bigtool/prompts.py,sha256=Joi5mCzZX63aM_6eBrMOKuNRHjTkceVIibSsGBGqhYE,2041
16
- universal_mcp/agents/bigtool/state.py,sha256=TQeGZD99okclkoCh5oz-VYIlEsC9yLQyDpnBnm7QCN8,759
16
+ universal_mcp/agents/bigtool/state.py,sha256=Voh7HXGC0PVe_0qoRZ8ZYg9akg65_2jQIAV2eIwperE,737
17
17
  universal_mcp/agents/bigtool/tools.py,sha256=-u80ta6xEaqzEMSzDVe3QZiTZm3YlgLkBD8WTghzClw,6315
18
18
  universal_mcp/agents/builder/__main__.py,sha256=VJDJOr-dJJerT53ibh5LVqIsMJ0m0sG2UlzFB784pKw,11680
19
19
  universal_mcp/agents/builder/builder.py,sha256=mh3MZpMVB1FE1DWzvMW9NnfiaF145VGn8cJzKSYUlzY,8587
@@ -21,16 +21,16 @@ universal_mcp/agents/builder/helper.py,sha256=8igR1b3Gy_N2u3WxHYKIWzvw7F5BMnfpO2
21
21
  universal_mcp/agents/builder/prompts.py,sha256=8Xs6uzTUHguDRngVMLak3lkXFkk2VV_uQXaDllzP5cI,4670
22
22
  universal_mcp/agents/builder/state.py,sha256=7DeWllxfN-yD6cd9wJ3KIgjO8TctkJvVjAbZT8W_zqk,922
23
23
  universal_mcp/agents/codeact0/__init__.py,sha256=8-fvUo1Sm6dURGI-lW-X3Kd78LqySYbb5NMkNJ4NDwg,76
24
- universal_mcp/agents/codeact0/__main__.py,sha256=EHW9ePVePEemGI5yMUBc2Mp_JlrP6Apk1liab1y2Rd8,782
25
- universal_mcp/agents/codeact0/agent.py,sha256=LzCshVduxT8weYbZ2mOyL1-rp4o-8H1ODoZAaONAV6o,19322
24
+ universal_mcp/agents/codeact0/__main__.py,sha256=YyIoecUcKVUhTcCACzLlSmYrayMDsdwzDEqaV4VV4CE,766
25
+ universal_mcp/agents/codeact0/agent.py,sha256=jaBntdEGydWI6OvRPpDsrLjnNncDdvQtjJbAgkeYp-U,20545
26
26
  universal_mcp/agents/codeact0/config.py,sha256=H-1woj_nhSDwf15F63WYn723y4qlRefXzGxuH81uYF0,2215
27
27
  universal_mcp/agents/codeact0/langgraph_agent.py,sha256=8nz2wq-LexImx-l1y9_f81fK72IQetnCeljwgnduNGY,420
28
28
  universal_mcp/agents/codeact0/llm_tool.py,sha256=-pAz04OrbZ_dJ2ueysT1qZd02DrbLY4EbU0tiuF_UNU,798
29
- universal_mcp/agents/codeact0/prompts.py,sha256=PEBPHvw4yItNVMAaaVUfNVhp53iijxs283Ln6xhew-4,11623
30
- universal_mcp/agents/codeact0/sandbox.py,sha256=Xw4tbUV_6haYIZZvteJi6lIYsW6ni_3DCRCOkslTKgM,4459
31
- universal_mcp/agents/codeact0/state.py,sha256=241G-ZDIs4dqm_RnfLZuZOBr7llJOh4dQoDc0aiRcPo,1947
32
- universal_mcp/agents/codeact0/tools.py,sha256=ZvAqi2vwarTlPizVdu9cZIAoujtBTF4OWnd69rdIIyA,14711
33
- universal_mcp/agents/codeact0/utils.py,sha256=Gvft0W0Sg1qlFWm8ciX14yssCa8y3x037lql92yGsBQ,18164
29
+ universal_mcp/agents/codeact0/prompts.py,sha256=Zt0ea01ofz6oS7fgyGK2Q2zN9CNMHGubBdR54VgvKic,11684
30
+ universal_mcp/agents/codeact0/sandbox.py,sha256=BeWJk_ucXed3QMHH6ae3FfVkbhSuRAlPXkjUeTUiufw,4504
31
+ universal_mcp/agents/codeact0/state.py,sha256=cf-94hfVub-HSQJk6b7_SzqBS-oxMABjFa8jqyjdDK0,1925
32
+ universal_mcp/agents/codeact0/tools.py,sha256=i2-WppqEfpJXPa7QouLfX3qXJgInBGVY9qxAGxFOUEg,14896
33
+ universal_mcp/agents/codeact0/utils.py,sha256=F2aFnN0tNXbFfe8imO1iccHXTvWwSSulIbsrkwhhpno,21123
34
34
  universal_mcp/agents/shared/__main__.py,sha256=XxH5qGDpgFWfq7fwQfgKULXGiUgeTp_YKfcxftuVZq8,1452
35
35
  universal_mcp/agents/shared/prompts.py,sha256=yjP3zbbuKi87qCj21qwTTicz8TqtkKgnyGSeEjMu3ho,3761
36
36
  universal_mcp/agents/shared/tool_node.py,sha256=DC9F-Ri28Pam0u3sXWNODVgmj9PtAEUb5qP1qOoGgfs,9169
@@ -39,6 +39,6 @@ universal_mcp/applications/filesystem/app.py,sha256=0TRjjm8YnslVRSmfkXI7qQOAlqWl
39
39
  universal_mcp/applications/llm/__init__.py,sha256=_XGRxN3O1--ZS5joAsPf8IlI9Qa6negsJrwJ5VJXno0,46
40
40
  universal_mcp/applications/llm/app.py,sha256=g9mK-luOLUshZzBGyQZMOHBeCSXmh2kCKir40YnsGUo,12727
41
41
  universal_mcp/applications/ui/app.py,sha256=c7OkZsO2fRtndgAzAQbKu-1xXRuRp9Kjgml57YD2NR4,9459
42
- universal_mcp_agents-0.1.23rc3.dist-info/METADATA,sha256=-Bv-20WOl1vY3WTMTmZe0eUShg6RG7vcmPt9HuUFXnU,881
43
- universal_mcp_agents-0.1.23rc3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- universal_mcp_agents-0.1.23rc3.dist-info/RECORD,,
42
+ universal_mcp_agents-0.1.23rc4.dist-info/METADATA,sha256=r3bhzzNiFXm9rGO6TU0sphN2YzqDkGR_5KHej-DEm4c,931
43
+ universal_mcp_agents-0.1.23rc4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ universal_mcp_agents-0.1.23rc4.dist-info/RECORD,,