universal-mcp-agents 0.1.23rc2__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,11 +64,11 @@ 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
- is_playbook = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "playbook"
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 {}
71
- if is_playbook and not additional_kwargs.get("stream"):
71
+ if is_agent_builder and not additional_kwargs.get("stream"):
72
72
  continue
73
73
  if isinstance(payload, AIMessageChunk):
74
74
  last_ai_chunk = payload
@@ -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
@@ -16,19 +16,18 @@ from universal_mcp.types import ToolFormat
16
16
  from universal_mcp.agents.base import BaseAgent
17
17
  from universal_mcp.agents.codeact0.llm_tool import smart_print
18
18
  from universal_mcp.agents.codeact0.prompts import (
19
- PLAYBOOK_GENERATING_PROMPT,
20
- PLAYBOOK_META_PROMPT,
21
- PLAYBOOK_PLANNING_PROMPT,
19
+ AGENT_BUILDER_GENERATING_PROMPT,
20
+ AGENT_BUILDER_META_PROMPT,
21
+ AGENT_BUILDER_PLANNING_PROMPT,
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, PlaybookCode, PlaybookMeta, PlaybookPlan
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
- enter_playbook_mode,
29
- get_valid_tools,
28
+ enter_agent_builder_mode,
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
 
@@ -41,7 +40,7 @@ class CodeActPlaybookAgent(BaseAgent):
41
40
  model: str,
42
41
  memory: BaseCheckpointSaver | None = None,
43
42
  registry: ToolRegistry | None = None,
44
- playbook_registry: object | None = None,
43
+ agent_builder_registry: object | None = None,
45
44
  sandbox_timeout: int = 20,
46
45
  **kwargs,
47
46
  ):
@@ -53,11 +52,13 @@ class CodeActPlaybookAgent(BaseAgent):
53
52
  **kwargs,
54
53
  )
55
54
  self.model_instance = load_chat_model(model)
56
- self.playbook_model_instance = load_chat_model("azure/gpt-4.1")
55
+ self.agent_builder_model_instance = load_chat_model("azure/gpt-4.1")
57
56
  self.registry = registry
58
- self.playbook_registry = playbook_registry
59
- self.playbook = playbook_registry.get_agent() if playbook_registry else None
60
- self.tools_config = self.playbook.tools if self.playbook else {}
57
+ self.agent_builder_registry = agent_builder_registry
58
+ self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
59
+
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
63
64
  self.default_tools_config = {
@@ -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."""
@@ -91,7 +88,7 @@ class CodeActPlaybookAgent(BaseAgent):
91
88
 
92
89
  agent_facing_tools = [
93
90
  execute_ipython_cell,
94
- enter_playbook_mode,
91
+ enter_agent_builder_mode,
95
92
  meta_tools["search_functions"],
96
93
  meta_tools["load_functions"],
97
94
  ]
@@ -113,14 +110,13 @@ 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:
121
117
  return Command(update={"messages": [response], "model_with_tools": model_with_tools})
122
118
 
123
- async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "playbook"]]:
119
+ async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "agent_builder"]]:
124
120
  """Execute tool calls"""
125
121
  last_message = state["messages"][-1]
126
122
  tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
@@ -128,6 +124,8 @@ class CodeActPlaybookAgent(BaseAgent):
128
124
  tool_messages = []
129
125
  new_tool_ids = []
130
126
  tool_result = ""
127
+ ask_user = False
128
+ ai_msg = ""
131
129
  effective_previous_add_context = state.get("add_context", {})
132
130
  effective_existing_context = state.get("context", {})
133
131
  # logging.info(f"Initial new_tool_ids_for_context: {new_tool_ids_for_context}")
@@ -136,15 +134,18 @@ class CodeActPlaybookAgent(BaseAgent):
136
134
  tool_name = tool_call["name"]
137
135
  tool_args = tool_call["args"]
138
136
  try:
139
- if tool_name == "enter_playbook_mode":
137
+ if tool_name == "enter_agent_builder_mode":
140
138
  tool_message = ToolMessage(
141
- content=json.dumps("Entered Playbook Mode."),
139
+ content=json.dumps("Entered Agent Builder Mode."),
142
140
  name=tool_call["name"],
143
141
  tool_call_id=tool_call["id"],
144
142
  )
145
143
  return Command(
146
- goto="playbook",
147
- update={"playbook_mode": "planning", "messages": [tool_message]}, # Entered Playbook mode
144
+ goto="agent_builder",
145
+ update={
146
+ "agent_builder_mode": "planning",
147
+ "messages": [tool_message],
148
+ }, # Entered Agent Builder mode
148
149
  )
149
150
  elif tool_name == "execute_ipython_cell":
150
151
  code = tool_call["args"]["snippet"]
@@ -160,24 +161,23 @@ class CodeActPlaybookAgent(BaseAgent):
160
161
  tool_result = output
161
162
  elif tool_name == "load_functions":
162
163
  # The tool now does all the work of validation and formatting.
163
- tool_result = await meta_tools["load_functions"].ainvoke(tool_args)
164
-
164
+ tool_result, new_context_for_sandbox, valid_tools, unconnected_links = await meta_tools[
165
+ "load_functions"
166
+ ].ainvoke(tool_args)
165
167
  # We still need to update the sandbox context for `execute_ipython_cell`
166
- valid_tools, _ = await get_valid_tools(tool_ids=tool_args["tool_ids"], registry=self.registry)
167
168
  new_tool_ids.extend(valid_tools)
168
169
  if new_tool_ids:
169
- newly_exported = await self.registry.export_tools(new_tool_ids, ToolFormat.LANGCHAIN)
170
- _, new_context_for_sandbox = create_default_prompt(
171
- newly_exported, [], "", "", None
172
- ) # is_initial_prompt is False by default
173
170
  self.tools_context.update(new_context_for_sandbox)
171
+ if unconnected_links:
172
+ ask_user = True
173
+ ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {unconnected_links} "
174
174
 
175
175
  elif tool_name == "search_functions":
176
176
  tool_result = await meta_tools["search_functions"].ainvoke(tool_args)
177
177
  else:
178
178
  raise Exception(
179
179
  f"Unexpected tool call: {tool_call['name']}. "
180
- "tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'. For using functions, call them in code using 'execute_ipython_cell'."
180
+ "tool calls must be one of 'enter_agent_builder_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'. For using functions, call them in code using 'execute_ipython_cell'."
181
181
  )
182
182
  except Exception as e:
183
183
  tool_result = str(e)
@@ -189,6 +189,17 @@ class CodeActPlaybookAgent(BaseAgent):
189
189
  )
190
190
  tool_messages.append(tool_message)
191
191
 
192
+ if ask_user:
193
+ tool_messages.append(AIMessage(content=ai_msg))
194
+ return Command(
195
+ update={
196
+ "messages": tool_messages,
197
+ "selected_tool_ids": new_tool_ids,
198
+ "context": effective_existing_context,
199
+ "add_context": effective_previous_add_context,
200
+ }
201
+ )
202
+
192
203
  return Command(
193
204
  goto="call_model",
194
205
  update={
@@ -199,17 +210,19 @@ class CodeActPlaybookAgent(BaseAgent):
199
210
  },
200
211
  )
201
212
 
202
- def playbook(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
203
- playbook_mode = state.get("playbook_mode")
204
- if playbook_mode == "planning":
213
+ async def agent_builder(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
214
+ agent_builder_mode = state.get("agent_builder_mode")
215
+ if agent_builder_mode == "planning":
205
216
  plan_id = str(uuid.uuid4())
206
- writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": bool(self.playbook)}})
207
- planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
217
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": bool(self.agent)}})
218
+ planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT
208
219
  messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
209
220
 
210
- model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookPlan)
211
- response = model_with_structured_output.invoke(messages)
212
- plan = cast(PlaybookPlan, response)
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)
225
+ plan = cast(AgentBuilderPlan, response)
213
226
 
214
227
  writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
215
228
  return Command(
@@ -220,16 +233,16 @@ class CodeActPlaybookAgent(BaseAgent):
220
233
  additional_kwargs={
221
234
  "type": "planning",
222
235
  "plan": plan.steps,
223
- "update": bool(self.playbook),
236
+ "update": bool(self.agent),
224
237
  },
225
238
  )
226
239
  ],
227
- "playbook_mode": "confirming",
240
+ "agent_builder_mode": "confirming",
228
241
  "plan": plan.steps,
229
242
  }
230
243
  )
231
244
 
232
- elif playbook_mode == "confirming":
245
+ elif agent_builder_mode == "confirming":
233
246
  # Deterministic routing based on three exact button inputs from UI
234
247
  user_text = ""
235
248
  for m in reversed(state["messages"]):
@@ -245,10 +258,10 @@ class CodeActPlaybookAgent(BaseAgent):
245
258
  if t == "yes, this is great":
246
259
  self.meta_id = str(uuid.uuid4())
247
260
  name, description = None, None
248
- if self.playbook:
261
+ if self.agent:
249
262
  # Update flow: use existing name/description and do not re-generate
250
- name = getattr(self.playbook, "name", None)
251
- description = getattr(self.playbook, "description", None)
263
+ name = getattr(self.agent, "name", None)
264
+ description = getattr(self.agent, "description", None)
252
265
  writer(
253
266
  {
254
267
  "type": "custom",
@@ -264,12 +277,14 @@ class CodeActPlaybookAgent(BaseAgent):
264
277
  else:
265
278
  writer({"type": "custom", id: self.meta_id, "name": "generating", "data": {"update": False}})
266
279
 
267
- meta_instructions = self.instructions + PLAYBOOK_META_PROMPT
280
+ meta_instructions = self.instructions + AGENT_BUILDER_META_PROMPT
268
281
  messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
269
282
 
270
- model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookMeta)
271
- meta_response = model_with_structured_output.invoke(messages)
272
- meta = cast(PlaybookMeta, meta_response)
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)
287
+ meta = cast(AgentBuilderMeta, meta_response)
273
288
  name, description = meta.name, meta.description
274
289
 
275
290
  # Emit intermediary UI update with created name/description
@@ -283,11 +298,11 @@ class CodeActPlaybookAgent(BaseAgent):
283
298
  )
284
299
 
285
300
  return Command(
286
- goto="playbook",
301
+ goto="agent_builder",
287
302
  update={
288
- "playbook_mode": "generating",
289
- "playbook_name": name,
290
- "playbook_description": description,
303
+ "agent_builder_mode": "generating",
304
+ "agent_name": name,
305
+ "agent_description": description,
291
306
  },
292
307
  )
293
308
  if t == "i would like to modify the plan":
@@ -295,52 +310,53 @@ class CodeActPlaybookAgent(BaseAgent):
295
310
  content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.",
296
311
  additional_kwargs={"stream": "true"},
297
312
  )
298
- return Command(update={"playbook_mode": "planning", "messages": [prompt_ai]})
313
+ return Command(update={"agent_builder_mode": "planning", "messages": [prompt_ai]})
299
314
  if t == "let's do something else":
300
- return Command(goto="call_model", update={"playbook_mode": "inactive"})
315
+ return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
301
316
 
302
317
  # Fallback safe default
303
- return Command(goto="call_model", update={"playbook_mode": "inactive"})
318
+ return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
304
319
 
305
- elif playbook_mode == "generating":
306
- generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
320
+ elif agent_builder_mode == "generating":
321
+ generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT
307
322
  messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
308
323
 
309
- model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookCode)
310
- response = model_with_structured_output.invoke(messages)
311
- func_code = cast(PlaybookCode, response).code
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)
328
+ func_code = cast(AgentBuilderCode, response).code
312
329
 
313
330
  # Extract function name (handle both regular and async functions)
314
331
  match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
315
332
  if match:
316
333
  function_name = match.group(1)
317
334
  else:
318
- function_name = "generated_playbook"
335
+ function_name = "generated_agent"
319
336
 
320
337
  # Use generated metadata if available
321
- final_name = state.get("playbook_.pyname") or function_name
322
- final_description = state.get("playbook_description") or f"Generated playbook: {function_name}"
338
+ final_name = state.get("agent_name") or function_name
339
+ final_description = state.get("agent_description") or f"Generated agent: {function_name}"
323
340
 
324
341
  # Save or update an Agent using the helper registry
325
342
  try:
326
- if not self.playbook_registry:
327
- raise ValueError("Playbook registry is not configured")
343
+ if not self.agent_builder_registry:
344
+ raise ValueError("AgentBuilder registry is not configured")
328
345
 
329
346
  # Build instructions payload embedding the plan and function code
330
347
  instructions_payload = {
331
- "playbookPlan": state["plan"],
332
- "playbookScript": func_code,
348
+ "plan": state["plan"],
349
+ "script": func_code,
333
350
  }
334
351
 
335
352
  # Convert tool ids list to dict
336
353
  tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
337
354
 
338
- res = self.playbook_registry.upsert_agent(
355
+ res = self.agent_builder_registry.upsert_agent(
339
356
  name=final_name,
340
357
  description=final_description,
341
358
  instructions=instructions_payload,
342
359
  tools=tool_dict,
343
- visibility="private",
344
360
  )
345
361
  except Exception as e:
346
362
  raise e
@@ -352,29 +368,41 @@ class CodeActPlaybookAgent(BaseAgent):
352
368
  "name": "generating",
353
369
  "data": {
354
370
  "id": str(res.id),
355
- "update": bool(self.playbook),
371
+ "update": bool(self.agent),
356
372
  "name": final_name,
357
373
  "description": final_description,
358
374
  },
359
375
  }
360
376
  )
377
+ mock_exit_tool_call = {
378
+ "name": "exit_agent_builder_mode",
379
+ "args": {},
380
+ "id": "exit_builder_1"
381
+ }
361
382
  mock_assistant_message = AIMessage(
362
383
  content=json.dumps(response.model_dump()),
384
+ tool_calls=[mock_exit_tool_call],
363
385
  additional_kwargs={
364
386
  "type": "generating",
365
387
  "id": str(res.id),
366
- "update": bool(self.playbook),
367
- "name": final_name,
388
+ "update": bool(self.agent),
389
+ "name": final_name.replace(" ", "_"),
368
390
  "description": final_description,
369
391
  },
370
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
+ )
371
399
 
372
- return Command(update={"messages": [mock_assistant_message], "playbook_mode": "normal"})
400
+ return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "agent_builder_mode": "normal"})
373
401
 
374
- async def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
375
- """Route to either normal mode or playbook creation"""
376
- all_tools = await self.registry.export_tools(state["selected_tool_ids"], ToolFormat.LANGCHAIN)
377
- # print(all_tools)
402
+ async def route_entry(state: CodeActState) -> Command[Literal["call_model", "agent_builder", "execute_tools"]]:
403
+ """Route to either normal mode or agent builder creation"""
404
+ await self.registry.load_tools(state["selected_tool_ids"])
405
+ all_tools = await self.registry.export_tools(format=ToolFormat.NATIVE)
378
406
 
379
407
  # Create the initial system prompt and tools_context in one go
380
408
  self.final_instructions, self.tools_context = create_default_prompt(
@@ -382,16 +410,22 @@ class CodeActPlaybookAgent(BaseAgent):
382
410
  self.additional_tools,
383
411
  self.instructions,
384
412
  await get_connected_apps_string(self.registry),
385
- self.playbook,
413
+ self.agent,
386
414
  is_initial_prompt=True,
387
415
  )
388
- if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
389
- return "playbook"
390
- return "call_model"
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
+
421
+ if state.get("agent_builder_mode") in ["planning", "confirming", "generating"]:
422
+ return Command(goto="agent_builder")
423
+ return Command(goto="call_model")
391
424
 
392
425
  agent = StateGraph(state_schema=CodeActState)
393
426
  agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
394
- agent.add_node(playbook)
427
+ agent.add_node(agent_builder)
395
428
  agent.add_node(execute_tools)
396
- agent.add_conditional_edges(START, route_entry)
429
+ agent.add_node(route_entry)
430
+ agent.add_edge(START, "route_entry")
397
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.
@@ -63,16 +62,16 @@ Rules:
63
62
  - Your final response should contain the complete answer to the user's request in a clear, well-formatted manner that directly addresses what they asked for.
64
63
  """
65
64
 
66
- PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
65
+ AGENT_BUILDER_PLANNING_PROMPT = """Now, you are tasked with creating a reusable agent from the user's previous workflow.
67
66
 
68
67
  TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
69
68
  Do not include the searching and loading of tools. Assume that the tools have already been loaded.
70
69
  The plan is a sequence of steps.
71
- You must output a JSON object with a single key "steps", which is a list of strings. Each string is a step in the playbook.
70
+ You must output a JSON object with a single key "steps", which is a list of strings. Each string is a step in the agent.
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 playbook 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
 
@@ -90,26 +89,27 @@ Now create a plan based on the conversation history. Do not include any other te
90
89
  """
91
90
 
92
91
 
93
- PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function.
92
+ AGENT_BUILDER_GENERATING_PROMPT = """Now, you are tasked with generating the agent function.
94
93
  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 playbook 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
 
102
- PLAYBOOK_META_PROMPT = """
103
- You are preparing metadata for a reusable playbook based on the confirmed step-by-step plan.
102
+ AGENT_BUILDER_META_PROMPT = """
103
+ You are preparing metadata for a reusable agent based on the confirmed step-by-step plan.
104
104
 
105
- TASK: Create a concise, human-friendly name and a short description for the playbook.
105
+ TASK: Create a concise, human-friendly name and a short description for the agent.
106
106
 
107
107
  INPUTS:
108
108
  - Conversation context and plan steps will be provided in prior messages
109
109
 
110
110
  REQUIREMENTS:
111
111
  1. Name: 3-6 words, Title Case, no punctuation except hyphens if needed
112
- 2. Description: Single sentence, <= 140 characters, clearly states what the playbook does
112
+ 2. Description: Single sentence, <= 140 characters, clearly states what the agent does
113
113
 
114
114
  OUTPUT: Return ONLY a JSON object with exactly these keys:
115
115
  {
@@ -132,12 +132,26 @@ 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
- playbook: object | None = None,
154
+ agent: object | None = None,
141
155
  is_initial_prompt: bool = False,
142
156
  ):
143
157
  if is_initial_prompt:
@@ -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:
@@ -191,14 +173,14 @@ def create_default_prompt(
191
173
  f"\n\nUse the following information/instructions while completing your tasks:\n\n{base_prompt}"
192
174
  )
193
175
 
194
- # Append existing playbook (plan + code) if provided
176
+ # Append existing agent (plan + code) if provided
195
177
  try:
196
- if playbook and hasattr(playbook, "instructions"):
197
- pb = playbook.instructions or {}
198
- plan = pb.get("playbookPlan")
199
- code = pb.get("playbookScript")
178
+ if agent and hasattr(agent, "instructions"):
179
+ pb = agent.instructions or {}
180
+ plan = pb.get("plan")
181
+ code = pb.get("script")
200
182
  if plan or code:
201
- system_prompt += "\n\nExisting Playbook 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,19 +1,19 @@
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
 
7
- class PlaybookPlan(BaseModel):
8
- steps: list[str] = Field(description="The steps of the playbook.")
7
+ class AgentBuilderPlan(BaseModel):
8
+ steps: list[str] = Field(description="The steps of the agent.")
9
9
 
10
10
 
11
- class PlaybookCode(BaseModel):
12
- code: str = Field(description="The Python code for the playbook.")
11
+ class AgentBuilderCode(BaseModel):
12
+ code: str = Field(description="The Python code for the agent.")
13
13
 
14
14
 
15
- class PlaybookMeta(BaseModel):
16
- name: str = Field(description="Concise, title-cased playbook name (3-6 words).")
15
+ class AgentBuilderMeta(BaseModel):
16
+ name: str = Field(description="Concise, title-cased agent name (3-6 words).")
17
17
  description: str = Field(description="Short, one-sentence description (<= 140 chars).")
18
18
 
19
19
 
@@ -46,13 +46,13 @@ class CodeActState(AgentState):
46
46
  """Dictionary containing the execution context with available tools and variables."""
47
47
  add_context: dict[str, Any]
48
48
  """Dictionary containing the additional context (functions, classes, imports) to be added to the execution context."""
49
- playbook_mode: str | None
50
- """State for the playbook agent."""
49
+ agent_builder_mode: str | None
50
+ """State for the agent builder agent."""
51
51
  selected_tool_ids: Annotated[list[str], _enqueue]
52
52
  """Queue for tools exported from registry"""
53
53
  plan: list[str] | None
54
- """Plan for the playbook agent."""
55
- playbook_name: str | None
56
- """Generated playbook name after confirmation."""
57
- playbook_description: str | None
54
+ """Plan for the agent builder agent."""
55
+ agent_name: str | None
56
+ """Generated agent name after confirmation."""
57
+ agent_description: str | None
58
58
  """Generated short description after confirmation."""
@@ -8,11 +8,11 @@ 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
- def enter_playbook_mode():
15
- """Call this function to enter playbook mode. Playbook mode is when the user wants to store a repeated task as a script with some inputs for the future."""
14
+ def enter_agent_builder_mode():
15
+ """Call this function to enter agent builder mode. Agent builder mode is when the user wants to store a repeated task as a script with some inputs for the future."""
16
16
  return
17
17
 
18
18
 
@@ -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:
@@ -212,39 +214,30 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
212
214
  return "No tool IDs provided to load."
213
215
 
214
216
  # Step 1: Validate which tools are usable and get login links for others.
215
- valid_tool_ids, unconnected_links = await get_valid_tools(tool_ids=tool_ids, registry=tool_registry)
217
+ valid_tools, unconnected_links = await get_valid_tools(tool_ids=tool_ids, registry=tool_registry)
216
218
 
217
- if not valid_tool_ids:
219
+ if not valid_tools:
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
- try:
222
- # Create a temporary, clean registry to export only the requested tools
223
- temp_registry = AgentrRegistry()
224
- exported_tools = await temp_registry.export_tools(valid_tool_ids, ToolFormat.LANGCHAIN)
225
- except Exception as e:
226
- return f"Error exporting tools: {e}"
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
227
227
 
228
228
  # Step 3: Build the informational string for the agent.
229
- tool_definitions, _ = create_default_prompt(exported_tools, [], is_initial_prompt=False)
229
+ tool_definitions, new_tools_context = build_tool_definitions(exported_tools)
230
230
 
231
231
  result_parts = [
232
232
  f"Successfully loaded {len(exported_tools)} functions. They are now available for use inside `execute_ipython_cell`:",
233
- tool_definitions,
233
+ "\n".join(tool_definitions),
234
234
  ]
235
235
 
236
236
  response_string = "\n\n".join(result_parts)
237
+ unconnected_links = "\n".join(unconnected_links)
237
238
 
238
- # Append login links if any apps were not connected
239
- if unconnected_links:
240
- links = "\n".join(unconnected_links)
241
- response_string += (
242
- f"\n\nPlease ask the user to log in to the following app(s) to use their full functionality:\n{links}"
243
- )
244
-
245
- return response_string
239
+ return response_string, new_tools_context, valid_tools, unconnected_links
246
240
 
247
- @tool
248
241
  async def web_search(query: str) -> dict:
249
242
  """
250
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.23rc2
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=CEnY8y2as_XR311t9v2iqd4DOCSyhpOPOBDcZKNJMpc,7378
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=9YwcCeRE_bSl77JG54SOJjQxqZxCd_ZfdBJmgb1N0RA,18818
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=qFcGDsISqW3iUNpW4yNDCH_vDwx75QS0acQsWI0Ul2w,11666
30
- universal_mcp/agents/codeact0/sandbox.py,sha256=Xw4tbUV_6haYIZZvteJi6lIYsW6ni_3DCRCOkslTKgM,4459
31
- universal_mcp/agents/codeact0/state.py,sha256=ESlxz68bwudasuL8jCI7GhweTqgLbYQqZM0mPkE06hQ,1938
32
- universal_mcp/agents/codeact0/tools.py,sha256=bCIJDNkyE_6SULoU8LGvupOrRrvaa9JZVMPMTdsGDEs,15017
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.23rc2.dist-info/METADATA,sha256=v2vW8cUeI8cn6XY502FcEhBaYVeLpXu1n2D9FpUmJ4Y,881
43
- universal_mcp_agents-0.1.23rc2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- universal_mcp_agents-0.1.23rc2.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,,