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

@@ -1,13 +1,12 @@
1
- # agents/base.py
2
- from typing import cast
1
+ from typing import Any, cast
3
2
  from uuid import uuid4
3
+ import asyncio
4
4
 
5
5
  from langchain_core.messages import AIMessageChunk
6
6
  from langgraph.checkpoint.base import BaseCheckpointSaver
7
7
  from langgraph.graph import StateGraph
8
8
  from langgraph.types import Command
9
9
  from universal_mcp.logger import logger
10
-
11
10
  from .utils import RichCLI
12
11
 
13
12
 
@@ -57,40 +56,48 @@ class BaseAgent:
57
56
  }
58
57
 
59
58
  last_ai_chunk = None
60
- async for event, meta in self._graph.astream(
61
- {"messages": [{"role": "user", "content": user_input}]},
62
- config=run_config,
63
- context={"system_prompt": self.instructions, "model": self.model},
64
- stream_mode=["messages", "custom"],
65
- stream_usage=True,
66
- ):
67
- if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2: # noqa: PLR2004
68
- payload, meta_dict = meta
69
- is_agent_builder = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "agent_builder"
70
- additional_kwargs = getattr(payload, "additional_kwargs", {}) or {}
71
- if is_agent_builder and not additional_kwargs.get("stream"):
72
- continue
73
- if isinstance(payload, AIMessageChunk):
74
- last_ai_chunk = payload
75
- aggregate = payload if aggregate is None else aggregate + payload
76
- if "finish_reason" in payload.response_metadata:
77
- logger.debug(
78
- f"Finish event: {payload}, reason: {payload.response_metadata['finish_reason']}, Metadata: {meta_dict}"
59
+ try:
60
+ async for event, meta in self._graph.astream(
61
+ {"messages": [{"role": "user", "content": user_input}]},
62
+ config=run_config,
63
+ context={"system_prompt": self.instructions, "model": self.model},
64
+ stream_mode=["messages", "custom"],
65
+ stream_usage=True,
66
+ ):
67
+ if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2: # noqa: PLR2004
68
+ payload, meta_dict = meta
69
+ is_agent_builder = (
70
+ isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "agent_builder"
79
71
  )
80
- pass
81
- logger.debug(f"Event: {payload}, Metadata: {meta_dict}")
82
- yield payload
83
-
84
- if event == "custom":
85
- yield meta
86
-
87
- # Send a final finished message if we saw any AI chunks (to carry usage)
88
- if last_ai_chunk is not None and aggregate is not None:
89
- event = cast(AIMessageChunk, last_ai_chunk)
90
- event.usage_metadata = aggregate.usage_metadata
91
- logger.debug(f"Usage metadata: {event.usage_metadata}")
92
- event.content = "" # Clear the message since it would have already been streamed above
93
- yield event
72
+ additional_kwargs = getattr(payload, "additional_kwargs", {}) or {}
73
+ if is_agent_builder and not additional_kwargs.get("stream"):
74
+ continue
75
+ if isinstance(payload, AIMessageChunk):
76
+ last_ai_chunk = payload
77
+ aggregate = payload if aggregate is None else aggregate + payload
78
+ if "finish_reason" in payload.response_metadata:
79
+ logger.debug(
80
+ f"Finish event: {payload}, reason: {payload.response_metadata['finish_reason']}, Metadata: {meta_dict}"
81
+ )
82
+ pass
83
+ logger.debug(f"Event: {payload}, Metadata: {meta_dict}")
84
+ yield payload
85
+
86
+ if event == "custom":
87
+ yield meta
88
+
89
+ except asyncio.CancelledError:
90
+ logger.info(f"Stream for thread_id {thread_id} was cancelled by the user.")
91
+ # Perform any cleanup here if necessary
92
+ finally:
93
+ # This block will run whether the stream finished normally or was cancelled
94
+ # Send a final finished message if we saw any AI chunks (to carry usage)
95
+ if last_ai_chunk is not None and aggregate is not None:
96
+ event = cast(AIMessageChunk, last_ai_chunk)
97
+ event.usage_metadata = aggregate.usage_metadata
98
+ logger.debug(f"Usage metadata: {event.usage_metadata}")
99
+ event.content = "" # Clear the message
100
+ yield event
94
101
 
95
102
  async def stream_interactive(self, thread_id: str, user_input: str):
96
103
  await self.ainit()
@@ -20,6 +20,7 @@ from universal_mcp.agents.codeact0.prompts import (
20
20
  AGENT_BUILDER_META_PROMPT,
21
21
  AGENT_BUILDER_PLANNING_PROMPT,
22
22
  create_default_prompt,
23
+ build_tool_definitions
23
24
  )
24
25
  from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
25
26
  from universal_mcp.agents.codeact0.state import AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan, CodeActState
@@ -27,7 +28,7 @@ from universal_mcp.agents.codeact0.tools import (
27
28
  create_meta_tools,
28
29
  enter_agent_builder_mode,
29
30
  )
30
- from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string, create_agent_call
31
+ from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string
31
32
  from universal_mcp.agents.llm import load_chat_model
32
33
  from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
33
34
 
@@ -52,7 +53,7 @@ class CodeActPlaybookAgent(BaseAgent):
52
53
  **kwargs,
53
54
  )
54
55
  self.model_instance = load_chat_model(model)
55
- self.agent_builder_model_instance = load_chat_model("azure/gpt-4.1")
56
+ self.agent_builder_model_instance = load_chat_model("anthropic:claude-sonnet-4-5-20250929", thinking = False)
56
57
  self.registry = registry
57
58
  self.agent_builder_registry = agent_builder_registry
58
59
  self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
@@ -215,7 +216,7 @@ class CodeActPlaybookAgent(BaseAgent):
215
216
  if agent_builder_mode == "planning":
216
217
  plan_id = str(uuid.uuid4())
217
218
  writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": bool(self.agent)}})
218
- planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT
219
+ planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT + self.preloaded_defs
219
220
  messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
220
221
 
221
222
  model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
@@ -318,7 +319,7 @@ class CodeActPlaybookAgent(BaseAgent):
318
319
  return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
319
320
 
320
321
  elif agent_builder_mode == "generating":
321
- generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT
322
+ generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT + self.preloaded_defs
322
323
  messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
323
324
 
324
325
  model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
@@ -359,7 +360,34 @@ class CodeActPlaybookAgent(BaseAgent):
359
360
  tools=tool_dict,
360
361
  )
361
362
  except Exception as e:
362
- raise e
363
+ # In case of error, add the code to the exit message content
364
+
365
+ mock_exit_tool_call = {
366
+ "name": "exit_agent_builder_mode",
367
+ "args": {},
368
+ "id": "exit_builder_1"
369
+ }
370
+
371
+ # Create a minimal assistant message to maintain flow
372
+ mock_assistant_message = AIMessage(
373
+ content=json.dumps(response.model_dump()),
374
+ tool_calls=[mock_exit_tool_call],
375
+ additional_kwargs={
376
+ "type": "generating",
377
+ "id": "ignore",
378
+ "update": bool(self.agent),
379
+ "name": final_name.replace(" ", "_"),
380
+ "description": final_description,
381
+ },
382
+ )
383
+ mock_exit_tool_response = ToolMessage(
384
+ content=json.dumps(
385
+ f"An error occurred. Displaying the function code:\n\n{func_code}\nFinal Name: {final_name}\nDescription: {final_description}"
386
+ ),
387
+ name="exit_agent_builder_mode",
388
+ tool_call_id="exit_builder_1"
389
+ )
390
+ return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "agent_builder_mode": "normal"})
363
391
 
364
392
  writer(
365
393
  {
@@ -401,18 +429,24 @@ class CodeActPlaybookAgent(BaseAgent):
401
429
 
402
430
  async def route_entry(state: CodeActState) -> Command[Literal["call_model", "agent_builder", "execute_tools"]]:
403
431
  """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)
432
+ pre_tools = await self.registry.export_tools(format=ToolFormat.NATIVE)
406
433
 
407
434
  # Create the initial system prompt and tools_context in one go
408
435
  self.final_instructions, self.tools_context = create_default_prompt(
409
- all_tools,
436
+ pre_tools,
410
437
  self.additional_tools,
411
438
  self.instructions,
412
439
  await get_connected_apps_string(self.registry),
413
440
  self.agent,
414
441
  is_initial_prompt=True,
415
442
  )
443
+ self.preloaded_defs, _ = build_tool_definitions(pre_tools)
444
+ self.preloaded_defs = '\n'.join(self.preloaded_defs)
445
+ await self.registry.load_tools(state["selected_tool_ids"])
446
+ exported_tools = await self.registry.export_tools(state["selected_tool_ids"],ToolFormat.NATIVE) # Get definition for only the new tools
447
+ _, loaded_tools_context = build_tool_definitions(exported_tools)
448
+ self.tools_context.update(loaded_tools_context)
449
+
416
450
  if len(state['messages']) == 1 and self.agent: # Inject the agent's script function into add_context for execution
417
451
  script = self.agent.instructions.get('script')
418
452
  add_context = {"functions":[script]}
@@ -34,68 +34,97 @@ Your job is to answer the user's question or perform the task they ask for.
34
34
  - You can only import libraries that come pre-installed with Python. However, do consider searching for external functions first, using the search and load tools to access them in the code.
35
35
  - For displaying final results to the user, you must present your output in markdown format, including image links, so that they are rendered and displayed to the user. The code output is NOT visible to the user.
36
36
  - Call all functions using keyword arguments only, never positional arguments.
37
-
38
- **Async Functions (Critical Rules):**
39
- Use async functions only as follows:
40
- - Case 1: Top-level await without asyncio.run()
41
- Wrap in async function and call with asyncio.run():
42
- ```python
43
- async def main():
44
- result = await some_async_function()
45
- return result
46
- asyncio.run(main())
47
- ```
48
- - Case 2: Using asyncio.run() directly
49
- If code already contains asyncio.run(), use as-is — do not wrap again:
50
- ```python
51
- asyncio.run(some_async_function())
52
- ```
53
- Rules:
54
- - Never use await outside an async function
55
- - Never use await asyncio.run()
56
- - Never nest asyncio.run() calls
37
+ - NEVER use execute_ipython_cell for:
38
+ - Static analysis or commentary
39
+ - Text that could be written as markdown
40
+ - Final output summarization after analysis
41
+ - Anything that's just formatted print statements
57
42
 
58
43
  **Final Output Requirements:**
59
- - Once you have all the information about the task, return the text directly to user in markdown format. No need to call `execute_ipython_cell` again.
44
+ - Once you have all the information about the task, return the text directly to user in markdown format. Do NOT call `execute_ipython_cell` again just for summarization.
60
45
  - Always respond in github flavoured markdown format.
61
46
  - For charts and diagrams, use mermaid chart in markdown directly.
62
47
  - 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.
63
48
  """
64
49
 
65
- AGENT_BUILDER_PLANNING_PROMPT = """Now, you are tasked with creating a reusable agent from the user's previous workflow.
50
+ AGENT_BUILDER_PLANNING_PROMPT = """TASK: Analyze the conversation history and code execution to create a step-by-step non-technical plan for a reusable function.
51
+ Rules:
52
+ - Do NOT include the searching and loading of functions. Assume that the functions have already been loaded.
53
+ - The plan is a sequence of steps corresponding to the key logical steps taken to achieve the user's task in the conversation history, without focusing on technical specifics.
54
+ - 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.
55
+ - Identify user-provided information as variables that should become the main agent input parameters using `variable_name` syntax, enclosed by backticks `...`. Intermediate variables should be highlighted using italics, i.e. *...*, NEVER `...`
56
+ - Keep the logic generic and reusable. Avoid hardcoding any names/constants. Instead, keep them as variables with defaults. They should be represented as `variable_name(default = default_value)`.
57
+ - Have a human-friendly plan and inputs format. That is, it must not use internal IDs or keys used by APIs as either inputs or outputs to the overall plan; using them internally is okay.
58
+ - Be as concise as possible, especially for internal processing steps.
59
+ - For steps where the assistant's intelligence was used outside of the code to infer/decide/analyse something, replace it with the use of *llm__* functions in the plan if required.
60
+
61
+ Example Conversation History:
62
+ User Message: "Create an image using Gemini for Marvel Cinematic Universe in comic style"
63
+ Code snippet: image_result = await google_gemini__generate_image(prompt=prompt)
64
+ Assistant Message: "The image has been successfully generated [image_result]."
65
+ User Message: "Save the image in my OneDrive"
66
+ Code snippet: image_data = base64.b64decode(image_result['data'])
67
+ temp_file_path = tempfile.mktemp(suffix='.png')
68
+ with open(temp_file_path, 'wb') as f:
69
+ f.write(image_data)
70
+ # Upload the image to OneDrive with a descriptive filename
71
+ onedrive_filename = "Marvel_Cinematic_Universe_Comic_Style.png"
72
+
73
+ print(f"Uploading to OneDrive as: {onedrive_filename}")
74
+
75
+ # Upload to OneDrive root folder
76
+ upload_result = onedrive__upload_file(
77
+ file_path=temp_file_path,
78
+ parent_id='root',
79
+ file_name=onedrive_filename
80
+ )
81
+
82
+ Generated Steps:
83
+ "steps": [
84
+ "Generate an image using Gemini model with `image_prompt` and `style(default = 'comic')`",
85
+ "Upload the obtained image to OneDrive using `onedrive_filename(default = 'generated_image.png')` and `onedrive_parent_folder(default = 'root')`",
86
+ "Return confirmation of upload including file name and destination path, and link to the upload"
87
+ ]
88
+ Note that internal variables like upload_result, image_result are not highlighted in the plan, and intermediate processing details are skipped.
89
+ Now create a plan based on the conversation history. Do not include any other text or explanation in your response. Just the JSON object.
90
+ Note that the following tools are pre-loaded for the agent's use, and can be inluded in your plan if needed as internal variables (especially the llm tools)-\n
91
+ """
66
92
 
67
- TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
68
- Do not include the searching and loading of tools. Assume that the tools have already been loaded.
69
- The plan is a sequence of steps.
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.
71
93
 
72
- Your plan should:
73
- 1. Identify the key steps in the workflow
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 ``
75
- 3. Keep the logic generic and reusable
76
- 4. Be clear and concise
94
+ AGENT_BUILDER_GENERATING_PROMPT = """
95
+ You are tasked with generating a reusable agent function based on the final confirmed agent plan and preceding conversation history including user messages, assistant messages, and code executions.
96
+ Create an appropriately named python function that combines relevent previously executed code from the conversation history to achieve the plan objectives.
97
+ Rules-
98
+ - Do NOT include the searching and loading of functions. Assume that the functions have already been loaded. Imports should be included.
99
+ - Your response must be **ONLY Python code** for the function.
100
+ - Do not include any text, explanations, or Markdown.
101
+ - The response must start with `def` or `async def` and define a single, complete, executable function.
102
+ - The function parameters **must exactly match the external variables** in the agent plan. External variables are marked using backticks `` `variable_name` ``. Any variables in italics (i.e. enclosed in *...*) are to be used internally, but not as the main function paramters.
103
+ - Any imports, variables, helper or child functions required must be defined **inside the main top-level function**.
104
+ - Ensure that the outer function is self-contained and can run independently, based on previously validated code snippets.
77
105
 
78
106
  Example:
79
- {
80
- "steps": [
81
- "Connect to database using `db_connection_string`",
82
- "Query user data for `user_id`",
83
- "Process results and calculate `metric_name`",
84
- "Send notification to `email_address`"
85
- ]
86
- }
87
107
 
88
- Now create a plan based on the conversation history. Do not include any other text or explanation in your response. Just the JSON object.
89
- """
108
+ If the plan has:
90
109
 
110
+ "steps": [
111
+ "Receive creative description as image_prompt",
112
+ "Generate image using Gemini with style(default = 'comic')",
113
+ "Save temporary image internally as *temp_file_path*",
114
+ "Upload *temp_file_path* to OneDrive folder onedrive_parent_folder(default = 'root')"
115
+ ]
91
116
 
92
- AGENT_BUILDER_GENERATING_PROMPT = """Now, you are tasked with generating the agent function.
93
- Your response must be ONLY the Python code for the function.
94
- Do not include any other text, markdown, or explanations in your response.
95
- Your response should start with `def` or `async def`.
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.
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.
117
+ Then the function signature should be:
118
+
119
+ ```python
120
+ def image_generator(image_prompt, style="comic", onedrive_parent_folder="root"):
121
+ #Code based on previously executed snippets
122
+
123
+ And all internal variables (e.g., *temp_file_path*) should be defined inside the function.
124
+
125
+
126
+ Use this convention consistently to generate the final agent function.
127
+ Note that the following tools are pre-loaded for the agent's use, and can be included in your code-\n
99
128
  """
100
129
 
101
130
 
@@ -132,17 +161,39 @@ def make_safe_function_name(name: str) -> str:
132
161
  return safe_name
133
162
 
134
163
 
164
+ # Compile regex once for better performance
165
+ _RAISES_PATTERN = re.compile(r'\n\s*[Rr]aises\s*:.*$', re.DOTALL)
166
+
167
+ def _clean_docstring(docstring: str | None) -> str:
168
+ """Remove the 'Raises:' section and everything after it from a docstring."""
169
+ if not docstring:
170
+ return ""
171
+
172
+ # Use pre-compiled regex for better performance
173
+ cleaned = _RAISES_PATTERN.sub('', docstring)
174
+ return cleaned.strip()
175
+
176
+
135
177
  def build_tool_definitions(tools: list[Callable]) -> tuple[list[str], dict[str, Callable]]:
136
178
  tool_definitions = []
137
179
  context = {}
138
- for tool in tools:
180
+
181
+ # Pre-allocate lists for better performance
182
+ tool_definitions = [None] * len(tools)
183
+
184
+ for i, tool in enumerate(tools):
139
185
  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__}"""
186
+ cleaned_docstring = _clean_docstring(tool.__doc__)
187
+
188
+ # Pre-compute string parts to avoid repeated string operations
189
+ async_prefix = "async " if inspect.iscoroutinefunction(tool) else ""
190
+ signature = str(inspect.signature(tool))
191
+
192
+ tool_definitions[i] = f'''{async_prefix}def {tool_name} {signature}:
193
+ """{cleaned_docstring}"""
143
194
  ...'''
144
- )
145
195
  context[tool_name] = tool
196
+
146
197
  return tool_definitions, context
147
198
 
148
199
 
@@ -157,7 +208,7 @@ def create_default_prompt(
157
208
  if is_initial_prompt:
158
209
  system_prompt = uneditable_prompt.strip()
159
210
  if apps_string:
160
- system_prompt += f"\n\n**Connected external applications (These apps have been logged into by the user):**\n{apps_string}\n\n Use `search_functions` to search for functions you can perform using the above. You can also discover more applications using the `search_functions` tool to find additional tools and integrations, if required.\n"
211
+ system_prompt += f"\n\n**Connected external applications (These apps have been logged into by the user):**\n{apps_string}\n\n Use `search_functions` to search for functions you can perform using the above. You can also discover more applications using the `search_functions` tool to find additional tools and integrations, if required. However, you MUST not assume the application when multiple apps are connected for a particular usecase.\n"
161
212
  system_prompt += (
162
213
  "\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
163
214
  )
@@ -188,7 +239,7 @@ def create_default_prompt(
188
239
  plan_block = str(plan)
189
240
  system_prompt += f"Plan Steps:\n{plan_block}\n"
190
241
  if code:
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."
242
+ system_prompt += f"\nScript:\n```python\n{str(code)}\n```\nThis function can be called by you using `execute_ipython_code`. 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."
192
243
  except Exception:
193
244
  # Silently ignore formatting issues
194
245
  pass
@@ -8,13 +8,14 @@ import threading
8
8
  import types
9
9
  from typing import Any
10
10
  import pickle
11
+ import ast
11
12
 
12
13
  from langchain_core.tools import tool
13
14
 
14
15
  from universal_mcp.agents.codeact0.utils import derive_context, inject_context, smart_truncate
15
16
 
16
17
 
17
- def eval_unsafe(
18
+ async def eval_unsafe(
18
19
  code: str, _locals: dict[str, Any], add_context: dict[str, Any], timeout: int = 180
19
20
  ) -> tuple[str, dict[str, Any], dict[str, Any]]:
20
21
  """
@@ -39,21 +40,17 @@ def eval_unsafe(
39
40
  )
40
41
 
41
42
  result_container = {"output": "<no output>"}
42
-
43
- def target():
44
- try:
45
- with contextlib.redirect_stdout(io.StringIO()) as f:
46
- exec(code, _locals, _locals)
47
- result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
48
- except Exception as e:
49
- result_container["output"] = f"Error during execution: {type(e).__name__}: {e}"
50
-
51
- thread = threading.Thread(target=target)
52
- thread.start()
53
- thread.join(timeout)
54
-
55
- if thread.is_alive():
56
- result_container["output"] = f"Code timeout: code execution exceeded {timeout} seconds."
43
+
44
+ try:
45
+ compiled_code = compile(code, "<string>", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
46
+ with contextlib.redirect_stdout(io.StringIO()) as f:
47
+ coroutine = eval(compiled_code, _locals, _locals)
48
+ # Await the coroutine to run the code if it's async
49
+ if coroutine:
50
+ await coroutine
51
+ result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
52
+ except Exception as e:
53
+ result_container["output"] = f"Error during execution: {type(e).__name__}: {e}"
57
54
 
58
55
  # If NameError for provider__tool occurred, append guidance (no retry)
59
56
  try:
@@ -192,9 +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
- 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)
195
+ if len(connected_apps_in_results)<len(apps_in_results) and len(connected_apps_in_results)>0:
196
+ result_parts.append("Unconnected app functions can also be loaded if required by the user, but prefer connected ones. And do ask the user to choose if none of the relevant apps are connected")
197
+ return "\n".join(result_parts)
198
198
 
199
199
  @tool
200
200
  async def load_functions(tool_ids: list[str]) -> str:
@@ -452,78 +452,3 @@ 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
@@ -166,7 +166,7 @@ class LlmApp(BaseApplication):
166
166
  .with_retry(stop_after_attempt=MAX_RETRIES)
167
167
  .invoke(prompt)
168
168
  )
169
- return response.model_dump_json(indent=2)
169
+ return response.model_dump()
170
170
 
171
171
  def extract_data(
172
172
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-agents
3
- Version: 0.1.23rc5
3
+ Version: 0.1.23rc7
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
@@ -1,5 +1,5 @@
1
1
  universal_mcp/agents/__init__.py,sha256=Ythw8tyq7p-w1SPnuO2JtS4TvYEP75PkQpdyvZv-ww4,914
2
- universal_mcp/agents/base.py,sha256=Sa3ws87OlMklXv9NAs_kXNAvP5DbaAUnFQbx1WqEStM,7410
2
+ universal_mcp/agents/base.py,sha256=pnPf5EgVVoycg_mrgdIwqEiENny1Dcx6GDZWmOVw2NU,7837
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
@@ -22,23 +22,23 @@ universal_mcp/agents/builder/prompts.py,sha256=8Xs6uzTUHguDRngVMLak3lkXFkk2VV_uQ
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
24
  universal_mcp/agents/codeact0/__main__.py,sha256=YyIoecUcKVUhTcCACzLlSmYrayMDsdwzDEqaV4VV4CE,766
25
- universal_mcp/agents/codeact0/agent.py,sha256=jaBntdEGydWI6OvRPpDsrLjnNncDdvQtjJbAgkeYp-U,20545
25
+ universal_mcp/agents/codeact0/agent.py,sha256=lixuPcFLEaWl3IgJ7pY9JSLz9UxH5t9F9FJVEIELydA,22507
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=Zt0ea01ofz6oS7fgyGK2Q2zN9CNMHGubBdR54VgvKic,11684
30
- universal_mcp/agents/codeact0/sandbox.py,sha256=AYy512wsOKgh4nmXccEF0mntuXDBeHaccsz0aUSIQMI,4696
29
+ universal_mcp/agents/codeact0/prompts.py,sha256=re1DHkfC6kyy1Y2pgmPLMZ_TceKZHZk-0-csCPHnQjw,15344
30
+ universal_mcp/agents/codeact0/sandbox.py,sha256=FcJgJ64upa8NMcFDLXkT7FT69AQvUvPBiXyqW937AUo,4701
31
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
32
+ universal_mcp/agents/codeact0/tools.py,sha256=e-ucTRkXuHEagEAWo2OPWh28UGeYlKzeNhi5cM7lqPc,15007
33
+ universal_mcp/agents/codeact0/utils.py,sha256=a0ux1icTSB6ETIZ_X2azZxlP44LBx95bi7wchQWpnuY,18188
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
37
37
  universal_mcp/applications/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  universal_mcp/applications/filesystem/app.py,sha256=0TRjjm8YnslVRSmfkXI7qQOAlqWlD1eEn8Jm0xBeigs,5561
39
39
  universal_mcp/applications/llm/__init__.py,sha256=_XGRxN3O1--ZS5joAsPf8IlI9Qa6negsJrwJ5VJXno0,46
40
- universal_mcp/applications/llm/app.py,sha256=g9mK-luOLUshZzBGyQZMOHBeCSXmh2kCKir40YnsGUo,12727
40
+ universal_mcp/applications/llm/app.py,sha256=4aMDlbBFCJIe_yzSq3Jphtk5ctvjWhHkHfSfnh3_Mso,12714
41
41
  universal_mcp/applications/ui/app.py,sha256=c7OkZsO2fRtndgAzAQbKu-1xXRuRp9Kjgml57YD2NR4,9459
42
- universal_mcp_agents-0.1.23rc5.dist-info/METADATA,sha256=JN1atZb7RAGx_PCxS2MMWgRXQwkCHT1a-oi_2GzRDFA,931
43
- universal_mcp_agents-0.1.23rc5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- universal_mcp_agents-0.1.23rc5.dist-info/RECORD,,
42
+ universal_mcp_agents-0.1.23rc7.dist-info/METADATA,sha256=Up-hGsxUw2JH3vSa9FipSTF45ROWOMiySV0DItN4Cx8,931
43
+ universal_mcp_agents-0.1.23rc7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ universal_mcp_agents-0.1.23rc7.dist-info/RECORD,,