universal-mcp-agents 0.1.19rc1__py3-none-any.whl → 0.1.24rc3__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.
Files changed (57) hide show
  1. universal_mcp/agents/__init__.py +15 -16
  2. universal_mcp/agents/base.py +46 -35
  3. universal_mcp/agents/bigtool/state.py +1 -1
  4. universal_mcp/agents/cli.py +2 -5
  5. universal_mcp/agents/codeact0/__init__.py +2 -3
  6. universal_mcp/agents/codeact0/__main__.py +4 -7
  7. universal_mcp/agents/codeact0/agent.py +444 -96
  8. universal_mcp/agents/codeact0/langgraph_agent.py +1 -1
  9. universal_mcp/agents/codeact0/llm_tool.py +2 -254
  10. universal_mcp/agents/codeact0/prompts.py +247 -137
  11. universal_mcp/agents/codeact0/sandbox.py +52 -18
  12. universal_mcp/agents/codeact0/state.py +26 -6
  13. universal_mcp/agents/codeact0/tools.py +400 -74
  14. universal_mcp/agents/codeact0/utils.py +175 -11
  15. universal_mcp/agents/codeact00/__init__.py +3 -0
  16. universal_mcp/agents/{unified → codeact00}/__main__.py +4 -6
  17. universal_mcp/agents/codeact00/agent.py +578 -0
  18. universal_mcp/agents/codeact00/config.py +77 -0
  19. universal_mcp/agents/{unified → codeact00}/langgraph_agent.py +2 -2
  20. universal_mcp/agents/{unified → codeact00}/llm_tool.py +1 -1
  21. universal_mcp/agents/codeact00/prompts.py +364 -0
  22. universal_mcp/agents/{unified → codeact00}/sandbox.py +52 -18
  23. universal_mcp/agents/codeact00/state.py +66 -0
  24. universal_mcp/agents/codeact00/tools.py +525 -0
  25. universal_mcp/agents/codeact00/utils.py +678 -0
  26. universal_mcp/agents/codeact01/__init__.py +3 -0
  27. universal_mcp/agents/{codeact → codeact01}/__main__.py +4 -11
  28. universal_mcp/agents/codeact01/agent.py +413 -0
  29. universal_mcp/agents/codeact01/config.py +77 -0
  30. universal_mcp/agents/codeact01/langgraph_agent.py +14 -0
  31. universal_mcp/agents/codeact01/llm_tool.py +25 -0
  32. universal_mcp/agents/codeact01/prompts.py +246 -0
  33. universal_mcp/agents/codeact01/sandbox.py +162 -0
  34. universal_mcp/agents/{unified → codeact01}/state.py +26 -10
  35. universal_mcp/agents/codeact01/tools.py +648 -0
  36. universal_mcp/agents/{unified → codeact01}/utils.py +175 -11
  37. universal_mcp/agents/llm.py +14 -4
  38. universal_mcp/agents/react.py +3 -3
  39. universal_mcp/agents/sandbox.py +124 -69
  40. universal_mcp/applications/llm/app.py +76 -24
  41. {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/METADATA +6 -5
  42. universal_mcp_agents-0.1.24rc3.dist-info/RECORD +66 -0
  43. universal_mcp/agents/codeact/__init__.py +0 -3
  44. universal_mcp/agents/codeact/agent.py +0 -240
  45. universal_mcp/agents/codeact/models.py +0 -11
  46. universal_mcp/agents/codeact/prompts.py +0 -82
  47. universal_mcp/agents/codeact/sandbox.py +0 -85
  48. universal_mcp/agents/codeact/state.py +0 -11
  49. universal_mcp/agents/codeact/utils.py +0 -68
  50. universal_mcp/agents/codeact0/playbook_agent.py +0 -355
  51. universal_mcp/agents/unified/README.md +0 -45
  52. universal_mcp/agents/unified/__init__.py +0 -3
  53. universal_mcp/agents/unified/agent.py +0 -289
  54. universal_mcp/agents/unified/prompts.py +0 -192
  55. universal_mcp/agents/unified/tools.py +0 -188
  56. universal_mcp_agents-0.1.19rc1.dist-info/RECORD +0 -64
  57. {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/WHEEL +0 -0
@@ -1,355 +0,0 @@
1
- import inspect
2
- import json
3
- import re
4
- from collections.abc import Callable
5
- from typing import Literal, cast
6
-
7
- from langchain_core.messages import AIMessage, ToolMessage
8
- from langchain_core.tools import StructuredTool
9
- from langchain_core.tools import tool as create_tool
10
- from langgraph.checkpoint.base import BaseCheckpointSaver
11
- from langgraph.graph import START, StateGraph
12
- from langgraph.types import Command, RetryPolicy
13
- from universal_mcp.tools.registry import ToolRegistry
14
- from universal_mcp.types import ToolConfig, ToolFormat
15
-
16
- from universal_mcp.agents.base import BaseAgent
17
- from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
18
- from universal_mcp.agents.codeact0.prompts import (
19
- create_default_prompt,
20
- )
21
- from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
22
- from universal_mcp.agents.codeact0.state import CodeActState
23
- from universal_mcp.agents.codeact0.tools import (
24
- create_meta_tools,
25
- enter_playbook_mode,
26
- get_valid_tools,
27
- )
28
- from universal_mcp.agents.codeact0.utils import inject_context, smart_truncate
29
- from universal_mcp.agents.llm import load_chat_model
30
- from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
31
-
32
- PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
33
-
34
- TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function. Do not include the searching and loading of tools. Assume that the tools have already been loaded.
35
-
36
- Your plan should:
37
- 1. Identify the key steps in the workflow
38
- 2. Mark user-specific variables that should become the main playbook function parameters using `variable_name` syntax. Intermediate variables should not be highlighted using ``
39
- 3. Keep the logic generic and reusable
40
- 4. Be clear and concise
41
-
42
- Example:
43
- ```
44
- 1. Connect to database using `db_connection_string`
45
- 2. Query user data for `user_id`
46
- 3. Process results and calculate `metric_name`
47
- 4. Send notification to `email_address`
48
- ```
49
-
50
- Now create a plan based on the conversation history. Enclose it between ``` and ```. Ask the user if the plan is okay."""
51
-
52
-
53
- PLAYBOOK_CONFIRMING_PROMPT = """Now, you are tasked with confirming the playbook plan. Return True if the user is happy with the plan, False otherwise. Do not say anything else in your response. The user response will be the last message in the chain.
54
- """
55
-
56
- PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function. Return the function in Python code.
57
- Do not include any other text in your response.
58
- The function should be a single, complete piece of code that can be executed independently, based on previously executed code snippets that executed correctly.
59
- The parameters of the function should be the same as the final confirmed playbook plan.
60
- Do not include anything other than python code in your response
61
- """
62
-
63
-
64
- class CodeActPlaybookAgent(BaseAgent):
65
- def __init__(
66
- self,
67
- name: str,
68
- instructions: str,
69
- model: str,
70
- memory: BaseCheckpointSaver | None = None,
71
- tools: ToolConfig | None = None,
72
- registry: ToolRegistry | None = None,
73
- playbook_registry: object | None = None,
74
- sandbox_timeout: int = 20,
75
- **kwargs,
76
- ):
77
- super().__init__(
78
- name=name,
79
- instructions=instructions,
80
- model=model,
81
- memory=memory,
82
- **kwargs,
83
- )
84
- self.model_instance = load_chat_model(model)
85
- self.tools_config = tools or []
86
- self.registry = registry
87
- self.playbook_registry = playbook_registry
88
- self.eval_fn = eval_unsafe
89
- self.sandbox_timeout = sandbox_timeout
90
- self.processed_tools: list[StructuredTool | Callable] = []
91
-
92
- async def _build_graph(self):
93
- meta_tools = create_meta_tools(self.registry)
94
- additional_tools = [smart_print, data_extractor, ai_classify, call_llm, meta_tools["web_search"]]
95
- self.additional_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in additional_tools]
96
-
97
- async def call_model(state: CodeActState) -> Command[Literal["sandbox", "execute_tools"]]:
98
- self.exported_tools = []
99
- if self.tools_config:
100
- # Convert dict format to list format if needed
101
- if isinstance(self.tools_config, dict):
102
- self.tools_config = [
103
- f"{provider}__{tool}" for provider, tools in self.tools_config.items() for tool in tools
104
- ]
105
- if not self.registry:
106
- raise ValueError("Tools are configured but no registry is provided")
107
- # Langchain tools are fine
108
- self.tools_config.extend(state.get("selected_tool_ids", []))
109
- self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
110
- self.final_instructions, self.tools_context = create_default_prompt(
111
- self.exported_tools, self.additional_tools, self.instructions
112
- )
113
- messages = [{"role": "system", "content": self.final_instructions}] + state["messages"]
114
-
115
- # Run the model and potentially loop for reflection
116
- model_with_tools = self.model_instance.bind_tools(
117
- tools=[
118
- execute_ipython_cell,
119
- enter_playbook_mode,
120
- meta_tools["search_functions"],
121
- meta_tools["load_functions"],
122
- ],
123
- tool_choice="auto",
124
- )
125
- response = cast(AIMessage, model_with_tools.invoke(messages))
126
- if response.tool_calls:
127
- return Command(goto="execute_tools", update={"messages": [response]})
128
- else:
129
- return Command(update={"messages": [response], "model_with_tools": model_with_tools})
130
-
131
- # if response.tool_calls:
132
- # if len(response.tool_calls) > 1:
133
- # raise Exception("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
134
- # if response.tool_calls[0]["name"] == "enter_playbook_mode":
135
- # return Command(goto="playbook", update = {"playbook_mode": "planning"})
136
- # if response.tool_calls[0]["name"] != "execute_ipython_cell":
137
- # raise Exception(
138
- # f"Unexpected tool call: {response.tool_calls[0]['name']}. Expected 'execute_ipython_cell'."
139
- # )
140
- # if (
141
- # response.tool_calls[0]["args"].get("snippet") is None
142
- # or not response.tool_calls[0]["args"]["snippet"].strip()
143
- # ):
144
- # raise Exception("Tool call 'execute_ipython_cell' requires a non-empty 'snippet' argument.")
145
- # return Command(goto="sandbox", update={"messages": [response]})
146
- # else:
147
- # return Command(update={"messages": [response]})
148
-
149
- async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "playbook", "sandbox"]]:
150
- """Execute tool calls"""
151
- last_message = state["messages"][-1]
152
- tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
153
-
154
- tool_messages = []
155
- new_tool_ids = []
156
- ask_user = False
157
- ai_msg = ""
158
- tool_result = ""
159
-
160
- for tool_call in tool_calls:
161
- try:
162
- if tool_call["name"] == "enter_playbook_mode":
163
- tool_message = ToolMessage(
164
- content=json.dumps("Entered Playbook Mode."),
165
- name=tool_call["name"],
166
- tool_call_id=tool_call["id"],
167
- )
168
- return Command(
169
- goto="playbook",
170
- update={"playbook_mode": "planning", "messages": [tool_message]}, # Entered Playbook mode
171
- )
172
- elif tool_call["name"] == "execute_ipython_cell":
173
- return Command(goto="sandbox")
174
- elif tool_call["name"] == "load_functions": # Handle load_functions separately
175
- valid_tools, unconnected_links = await get_valid_tools(
176
- tool_ids=tool_call["args"]["tool_ids"], registry=self.registry
177
- )
178
- new_tool_ids.extend(valid_tools)
179
- # Create tool message response
180
- tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
181
- links = "\n".join(unconnected_links)
182
- if links:
183
- ask_user = True
184
- ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {links} "
185
- elif tool_call["name"] == "search_functions":
186
- tool_result = await meta_tools["search_functions"].ainvoke(tool_call["args"])
187
- except Exception as e:
188
- tool_result = f"Error during {tool_call}: {e}"
189
-
190
- tool_message = ToolMessage(
191
- content=json.dumps(tool_result),
192
- name=tool_call["name"],
193
- tool_call_id=tool_call["id"],
194
- )
195
- tool_messages.append(tool_message)
196
-
197
- if new_tool_ids:
198
- self.tools_config.extend(new_tool_ids)
199
- self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
200
- self.final_instructions, self.tools_context = create_default_prompt(
201
- self.exported_tools, self.additional_tools, self.instructions
202
- )
203
-
204
- if ask_user:
205
- tool_messages.append(AIMessage(content=ai_msg))
206
- return Command(update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
207
-
208
- return Command(goto="call_model", update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
209
-
210
- # If eval_fn is a async, we define async node function.
211
- if inspect.iscoroutinefunction(self.eval_fn):
212
- raise ValueError("eval_fn must be a synchronous function, not a coroutine.")
213
- # async def sandbox(state: StateSchema):
214
- # existing_context = state.get("context", {})
215
- # context = {**existing_context, **tools_context}
216
- # # Execute the script in the sandbox
217
- # output, new_vars = await eval_fn(state["script"], context)
218
- # new_context = {**existing_context, **new_vars}
219
- # return {
220
- # "messages": [{"role": "user", "content": output}],
221
- # "context": new_context,
222
- # }
223
- else:
224
-
225
- def sandbox(state: CodeActState) -> Command[Literal["call_model"]]:
226
- tool_call = state["messages"][-1].tool_calls[0] # type: ignore
227
- code = tool_call["args"]["snippet"]
228
- previous_add_context = state.get("add_context", {})
229
- add_context = inject_context(previous_add_context, self.tools_context)
230
- existing_context = state.get("context", {})
231
- context = {**existing_context, **add_context}
232
- # Execute the script in the sandbox
233
-
234
- output, new_context, new_add_context = self.eval_fn(
235
- code, context, previous_add_context, 180
236
- ) # default timeout 3 min
237
- output = smart_truncate(output)
238
-
239
- return Command(
240
- goto="call_model",
241
- update={
242
- "messages": [
243
- ToolMessage(
244
- content=output,
245
- name=tool_call["name"],
246
- tool_call_id=tool_call["id"],
247
- )
248
- ],
249
- "context": new_context,
250
- "add_context": new_add_context,
251
- },
252
- )
253
-
254
- def playbook(state: CodeActState) -> Command[Literal["call_model"]]:
255
- playbook_mode = state.get("playbook_mode")
256
- if playbook_mode == "planning":
257
- planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
258
- messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
259
-
260
- response = self.model_instance.invoke(messages)
261
- response = cast(AIMessage, response)
262
- response_text = get_message_text(response)
263
- # Extract plan from response text between triple backticks
264
- plan_match = re.search(r"```(.*?)```", response_text, re.DOTALL)
265
- if plan_match:
266
- plan = plan_match.group(1).strip()
267
- else:
268
- plan = response_text.strip()
269
- return Command(update={"messages": [response], "playbook_mode": "confirming", "plan": plan})
270
-
271
- elif playbook_mode == "confirming":
272
- confirmation_instructions = self.instructions + PLAYBOOK_CONFIRMING_PROMPT
273
- messages = [{"role": "system", "content": confirmation_instructions}] + state["messages"]
274
- response = self.model_instance.invoke(messages, stream=False)
275
- response = get_message_text(response)
276
- if "true" in response.lower():
277
- return Command(goto="playbook", update={"playbook_mode": "generating"})
278
- else:
279
- return Command(goto="playbook", update={"playbook_mode": "planning"})
280
-
281
- elif playbook_mode == "generating":
282
- generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
283
- messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
284
- response = cast(AIMessage, self.model_instance.invoke(messages))
285
- raw_content = get_message_text(response)
286
- func_code = raw_content.strip()
287
- func_code = func_code.replace("```python", "").replace("```", "")
288
- func_code = func_code.strip()
289
-
290
- # Extract function name (handle both regular and async functions)
291
- match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
292
- if match:
293
- function_name = match.group(1)
294
- else:
295
- function_name = "generated_playbook"
296
-
297
- # Save or update an Agent using the helper registry
298
- saved_note = ""
299
- try:
300
- if not self.playbook_registry:
301
- raise ValueError("Playbook registry is not configured")
302
-
303
- # Build instructions payload embedding the plan and function code
304
- instructions_payload = {
305
- "playbookPlan": state["plan"],
306
- "playbookScript": {
307
- "name": function_name,
308
- "code": func_code,
309
- },
310
- }
311
-
312
- # Convert tool ids list to dict
313
- tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
314
-
315
- res = self.playbook_registry.create_agent(
316
- name=function_name,
317
- description=f"Generated playbook: {function_name}",
318
- instructions=instructions_payload,
319
- tools=tool_dict,
320
- visibility="private",
321
- )
322
- saved_note = f"Successfully created your playbook! Check it out here: [View Playbook](https://wingmen.info/agents/{res.id})"
323
- except Exception as e:
324
- saved_note = f"Failed to save generated playbook as Agent '{function_name}': {e}"
325
-
326
- # Mock tool call for exit_playbook_mode (for testing/demonstration)
327
- mock_exit_tool_call = {"name": "exit_playbook_mode", "args": {}, "id": "mock_exit_playbook_123"}
328
- mock_assistant_message = AIMessage(content=saved_note, tool_calls=[mock_exit_tool_call])
329
-
330
- # Mock tool response for exit_playbook_mode
331
- mock_exit_tool_response = ToolMessage(
332
- content=json.dumps(f"Exited Playbook Mode.{saved_note}"),
333
- name="exit_playbook_mode",
334
- tool_call_id="mock_exit_playbook_123",
335
- )
336
-
337
- return Command(
338
- update={"messages": [mock_assistant_message, mock_exit_tool_response], "playbook_mode": "normal"}
339
- )
340
-
341
- def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
342
- """Route to either normal mode or playbook creation"""
343
- if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
344
- return "playbook"
345
-
346
- return "call_model"
347
-
348
- agent = StateGraph(state_schema=CodeActState)
349
- agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
350
- agent.add_node(sandbox)
351
- agent.add_node(playbook)
352
- agent.add_node(execute_tools)
353
- agent.add_conditional_edges(START, route_entry)
354
- # agent.add_edge(START, "call_model")
355
- return agent.compile(checkpointer=self.memory)
@@ -1,45 +0,0 @@
1
- # Unified Agent
2
-
3
- The Unified Agent is a sophisticated AI assistant designed to understand and execute tasks by writing and running Python code. It operates within a secure sandbox environment and can leverage a variety of tools to interact with external systems and perform complex operations. A key feature of the Unified Agent is its ability to create reusable "playbooks" from user workflows, enabling automation of repeated tasks.
4
-
5
- ## Architecture
6
-
7
- The agent's architecture is built upon the LangGraph library, creating a state machine that cycles between thinking (calling a language model) and acting (executing code or tools).
8
-
9
- ### Core Components:
10
-
11
- * **`UnifiedAgent`**: The fundamental agent implementation. It processes user requests, writes Python code, and executes it in a sandbox to achieve the desired outcome. It also has a "playbook mode" to generate reusable Python functions from a user's workflow.
12
- * **State Graph (`CodeActState`)**: The agent's logic is defined as a state graph. The primary nodes are:
13
- * `call_model`: Invokes the language model to generate Python code or select a tool based on the current state and user input.
14
- * `sandbox`: Executes the generated Python code using a safe `eval` function with a timeout. The results and any errors are captured and fed back into the state.
15
- * `execute_tools`: Handles the execution of meta-tools for searching, loading, and interacting with external functions.
16
- * `playbook`: Manages the playbook creation process, including planning, user confirmation, and code generation.
17
- * **Sandbox (`sandbox.py`)**: A secure execution environment that runs Python code in a separate thread with a timeout. It ensures that the agent's code execution is isolated and cannot harm the host system.
18
- * **Tools**: The agent has access to a set of powerful tools:
19
- * `execute_ipython_cell`: The primary tool for executing arbitrary Python code snippets.
20
- * **AI Functions (`llm_tool.py`)**: A suite of functions (`generate_text`, `classify_data`, `extract_data`, `call_llm`) that allow the agent to delegate complex reasoning, classification, and data extraction tasks to a language model.
21
- * **Meta Tools (`tools.py`)**: Functions like `search_functions` and `load_functions` that enable the agent to dynamically discover and load new tools from a `ToolRegistry`.
22
-
23
- ## Playbook Mode
24
-
25
- A key feature of the Unified Agent is its ability to create reusable "playbooks". When a user performs a task that they might want to repeat in the future, they can trigger the playbook mode. The agent will then:
26
-
27
- 1. **Plan:** Analyze the workflow and create a step-by-step plan for a reusable function, identifying user-specific variables that should become function parameters.
28
- 2. **Confirm:** Ask the user for confirmation of the generated plan.
29
- 3. **Generate:** Generate a Python function based on the confirmed plan. This function can be saved and executed later to automate the task.
30
-
31
- ## Getting Started (`__main__.py`)
32
-
33
- The `__main__.py` file serves as a simple command-line interface for interacting with the agent. It demonstrates how to instantiate the `UnifiedAgent`, configure it with tools, and invoke it with a user request. This allows for easy testing and experimentation with the agent's capabilities.
34
-
35
- To run the agent, execute the following command from the root of the repository:
36
- ```bash
37
- uv run python -m src.universal_mcp.agents.unified.__main__
38
- ```
39
-
40
- Major TODO:
41
- - [] Improve LLM Tools
42
- - [] Use smaller dedicated models for universal_write, clasify etc
43
- - Improve Sandbox
44
- - [] Support saving loading context
45
- - [] Direct async tool support
@@ -1,3 +0,0 @@
1
- from .agent import UnifiedAgent
2
-
3
- __all__ = ["UnifiedAgent"]