universal-mcp-agents 0.1.7__py3-none-any.whl → 0.1.9__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.
- universal_mcp/agents/__init__.py +4 -1
- universal_mcp/agents/bigtool2/graph.py +49 -6
- universal_mcp/agents/builder.py +29 -8
- universal_mcp/agents/codeact/__init__.py +2 -254
- universal_mcp/agents/codeact/__main__.py +25 -0
- universal_mcp/agents/codeact/agent.py +171 -0
- universal_mcp/agents/codeact/prompts.py +92 -0
- universal_mcp/agents/codeact/sandbox.py +40 -19
- universal_mcp/agents/codeact/state.py +12 -0
- universal_mcp/agents/llm.py +1 -1
- universal_mcp/agents/planner/graph.py +1 -1
- universal_mcp/agents/shared/prompts.py +132 -0
- universal_mcp/agents/shared/tool_node.py +214 -205
- universal_mcp/applications/ui/app.py +1 -1
- {universal_mcp_agents-0.1.7.dist-info → universal_mcp_agents-0.1.9.dist-info}/METADATA +3 -2
- {universal_mcp_agents-0.1.7.dist-info → universal_mcp_agents-0.1.9.dist-info}/RECORD +17 -13
- universal_mcp/agents/codeact/test.py +0 -16
- {universal_mcp_agents-0.1.7.dist-info → universal_mcp_agents-0.1.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional, Sequence
|
|
4
|
+
|
|
5
|
+
from langchain_core.tools import StructuredTool, tool as create_tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_safe_function_name(name: str) -> str:
|
|
9
|
+
"""Convert a tool name to a valid Python function name."""
|
|
10
|
+
# Replace non-alphanumeric characters with underscores
|
|
11
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
12
|
+
# Ensure the name doesn't start with a digit
|
|
13
|
+
if safe_name and safe_name[0].isdigit():
|
|
14
|
+
safe_name = f"tool_{safe_name}"
|
|
15
|
+
# Handle empty name edge case
|
|
16
|
+
if not safe_name:
|
|
17
|
+
safe_name = "unnamed_tool"
|
|
18
|
+
return safe_name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_default_prompt(
|
|
22
|
+
tools: Sequence[StructuredTool],
|
|
23
|
+
base_prompt: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
"""Create default prompt for the CodeAct agent."""
|
|
26
|
+
prompt = f"{base_prompt}\n\n" if base_prompt else ""
|
|
27
|
+
prompt += """You will be given a task to perform. You should output either
|
|
28
|
+
- a Python code snippet that provides the solution to the task, or a step towards the solution. Any output you want to extract from the code should be printed to the console. Code should be output in a fenced code block.
|
|
29
|
+
- text to be shown directly to the user, if you want to ask for more information or provide the final answer.
|
|
30
|
+
|
|
31
|
+
In addition to the Python Standard Library, you can use the following functions:"""
|
|
32
|
+
|
|
33
|
+
for tool in tools:
|
|
34
|
+
# Use coroutine if it exists, otherwise use func
|
|
35
|
+
tool_callable = (
|
|
36
|
+
tool.coroutine
|
|
37
|
+
if hasattr(tool, "coroutine") and tool.coroutine is not None
|
|
38
|
+
else tool.func
|
|
39
|
+
)
|
|
40
|
+
# Create a safe function name
|
|
41
|
+
safe_name = make_safe_function_name(tool.name)
|
|
42
|
+
# Determine if it's an async function
|
|
43
|
+
is_async = inspect.iscoroutinefunction(tool_callable)
|
|
44
|
+
# Add appropriate function definition
|
|
45
|
+
prompt += f'''\n{"async " if is_async else ""}def {safe_name}{str(inspect.signature(tool_callable))}:
|
|
46
|
+
"""{tool.description}"""
|
|
47
|
+
...
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
prompt += """
|
|
51
|
+
|
|
52
|
+
Variables defined at the top level of previous code snippets can be referenced in your code.
|
|
53
|
+
|
|
54
|
+
Always use print() statements to explore data structures and function outputs. Simply returning values will not display them back to you for inspection. For example, use print(result) instead of just 'result'.
|
|
55
|
+
|
|
56
|
+
As you don't know the output schema of the additional Python functions you have access to, start from exploring their contents before building a final solution.
|
|
57
|
+
|
|
58
|
+
IMPORTANT CODING STRATEGY:
|
|
59
|
+
1. Only write code up to the point where you make an API call/tool usage with an output
|
|
60
|
+
2. Print the type/shape and a sample entry of this output, and using that knowledge proceed to write the further code
|
|
61
|
+
|
|
62
|
+
This means:
|
|
63
|
+
- Write code that makes the API call or tool usage
|
|
64
|
+
- Print the result with type information: print(f"Type: {type(result)}")
|
|
65
|
+
- Print the shape/structure: print(f"Shape/Keys: {result.keys() if isinstance(result, dict) else len(result) if isinstance(result, (list, tuple)) else 'N/A'}")
|
|
66
|
+
- Print a sample entry: print(f"Sample: {result[0] if isinstance(result, (list, tuple)) and len(result) > 0 else result}")
|
|
67
|
+
- Then, based on this knowledge, write the code to process/use this data
|
|
68
|
+
|
|
69
|
+
Reminder: use Python code snippets to call tools"""
|
|
70
|
+
return prompt
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
REFLECTION_PROMPT = """
|
|
74
|
+
Review the assistant's latest code for as per the quality rules:
|
|
75
|
+
|
|
76
|
+
<conversation_history>
|
|
77
|
+
{conversation_history}
|
|
78
|
+
</conversation_history>
|
|
79
|
+
|
|
80
|
+
If you find ANY of these issues, describe the problem briefly and clearly.
|
|
81
|
+
If NO issues are found, respond with EXACTLY: "NONE"
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
RETRY_PROMPT = """
|
|
85
|
+
I need you to completely regenerate your previous response based on this feedback:
|
|
86
|
+
|
|
87
|
+
'''
|
|
88
|
+
{reflection_result}
|
|
89
|
+
'''
|
|
90
|
+
|
|
91
|
+
DO NOT reference the feedback directly. Instead, provide a completely new response that addresses the issues.
|
|
92
|
+
"""
|
|
@@ -1,27 +1,48 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import builtins
|
|
2
3
|
import contextlib
|
|
3
4
|
import io
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
def eval_unsafe(code: str, _locals: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
async def eval_unsafe(code: str, _locals: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
9
|
+
"""
|
|
10
|
+
Execute code in a non-blocking way and return the output and changed variables.
|
|
11
|
+
"""
|
|
10
12
|
result = f"Executing code...\n{code}\n\nOutput:\n"
|
|
11
13
|
result += "=" * 50 + "\n"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
|
|
15
|
+
# Create a combined globals/locals environment that includes builtins
|
|
16
|
+
# and the provided context. This allows nested functions to access tools.
|
|
17
|
+
execution_env = {**builtins.__dict__, **_locals}
|
|
18
|
+
|
|
19
|
+
def sync_eval_in_thread():
|
|
20
|
+
"""Synchronously execute code and capture output."""
|
|
21
|
+
try:
|
|
22
|
+
with contextlib.redirect_stdout(io.StringIO()) as f:
|
|
23
|
+
exec(code, execution_env)
|
|
24
|
+
output = f.getvalue()
|
|
25
|
+
if not output:
|
|
26
|
+
output = "<code ran, no output printed to stdout>"
|
|
27
|
+
return output
|
|
28
|
+
except Exception as e:
|
|
29
|
+
return f"Error during execution: {repr(e)}"
|
|
30
|
+
|
|
31
|
+
# Run the synchronous exec in a separate thread to avoid blocking the event loop.
|
|
32
|
+
output = await asyncio.to_thread(sync_eval_in_thread)
|
|
33
|
+
result += output
|
|
34
|
+
|
|
35
|
+
# Identify all variables that are not part of the original builtins
|
|
36
|
+
# and were not in the initial _locals, or were changed.
|
|
37
|
+
changed_vars = {}
|
|
38
|
+
builtin_keys = set(builtins.__dict__.keys())
|
|
39
|
+
|
|
40
|
+
for key, value in execution_env.items():
|
|
41
|
+
if key in builtin_keys:
|
|
42
|
+
continue # Skip builtins
|
|
43
|
+
|
|
44
|
+
# Check if the key is new or if the value has changed
|
|
45
|
+
if key not in _locals or _locals[key] is not value:
|
|
46
|
+
changed_vars[key] = value
|
|
47
|
+
|
|
48
|
+
return result, changed_vars
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from langgraph.graph import MessagesState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CodeActState(MessagesState):
|
|
7
|
+
"""State for CodeAct agent."""
|
|
8
|
+
|
|
9
|
+
script: Optional[str]
|
|
10
|
+
"""The Python code script to be executed."""
|
|
11
|
+
context: dict[str, Any]
|
|
12
|
+
"""Dictionary containing the execution context with available tools and variables."""
|
universal_mcp/agents/llm.py
CHANGED
|
@@ -8,7 +8,7 @@ from langchain_openai import AzureChatOpenAI
|
|
|
8
8
|
|
|
9
9
|
@lru_cache(maxsize=8)
|
|
10
10
|
def load_chat_model(
|
|
11
|
-
fully_specified_name: str, temperature: float = 1.0, tags: list[str] | None = None, thinking: bool =
|
|
11
|
+
fully_specified_name: str, temperature: float = 1.0, tags: list[str] | None = None, thinking: bool = True
|
|
12
12
|
) -> BaseChatModel:
|
|
13
13
|
"""Load a chat model from a fully specified name.
|
|
14
14
|
Args:
|
|
@@ -19,7 +19,7 @@ def build_graph(llm, registry, instructions, model, executor_agent_cls):
|
|
|
19
19
|
logger.info(f"Running tool finder for task: {task}")
|
|
20
20
|
tool_finder_graph = build_tool_node_graph(llm, registry)
|
|
21
21
|
tool_finder_state = await tool_finder_graph.ainvoke(
|
|
22
|
-
{"
|
|
22
|
+
{"original_task": task, "messages": state["messages"]}
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
if not tool_finder_state.get("apps_required"):
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
TASK_DECOMPOSITION_PROMPT = """
|
|
2
|
+
You are an expert planner. Your goal is to consolidate a complex user request into the minimum number of high-level sub-tasks required. Each sub-task should correspond to a major, consolidated action within a single target application.
|
|
3
|
+
|
|
4
|
+
**CORE PRINCIPLES:**
|
|
5
|
+
1. **App-Centric Grouping:** Group all related actions for a single application into ONE sub-task.
|
|
6
|
+
2. **Focus on Data Handoffs:** A good decomposition often involves one sub-task to *retrieve* information and a subsequent sub-task to *use* that information.
|
|
7
|
+
3. **Assume Internal Capabilities:** Do NOT create sub-tasks for abstract cognitive work like 'summarize' or 'analyze'.
|
|
8
|
+
4. **Simplify Single Actions:** If the user's task is already a single, simple action, the output should be a single sub-task that concisely describes that action. Do not make it identical to the user's input.
|
|
9
|
+
5. **General purpose sub tasks:** You also need to realise that these subtasks are going to be used to search for tools and apps. And the names and description of these tools and apps are going to be general in nature so the sub tasks should not be too specific. The task which you will get may be specific in nature but the sub taks must be general.
|
|
10
|
+
**--- EXAMPLES ---**
|
|
11
|
+
|
|
12
|
+
**EXAMPLE 1:**
|
|
13
|
+
- **User Task:** "Create a Google Doc summarizing the last 5 merged pull requests in my GitHub repo universal-mcp/universal-mcp."
|
|
14
|
+
- **CORRECT DECOMPOSITION:**
|
|
15
|
+
- "Fetch the last 5 merged pull requests from the GitHub repository 'universal-mcp/universal-mcp'."
|
|
16
|
+
- "Create a new Google Doc containing the summary of the pull requests."
|
|
17
|
+
|
|
18
|
+
**EXAMPLE 2:**
|
|
19
|
+
- **User Task:** "Find the best restaurants in Goa using perplexity web search."
|
|
20
|
+
- **CORRECT DECOMPOSITION:**
|
|
21
|
+
- "Perform a web search using Perplexity to find the best restaurants in Goa."
|
|
22
|
+
|
|
23
|
+
**--- YOUR TASK ---**
|
|
24
|
+
|
|
25
|
+
**USER TASK:**
|
|
26
|
+
"{task}"
|
|
27
|
+
|
|
28
|
+
**YOUR DECOMPOSITION (as a list of strings):**
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
APP_SEARCH_QUERY_PROMPT = """
|
|
33
|
+
You are an expert at selecting an application to perform a specific sub-task. Your goal is to generate a concise query for an app search engine.
|
|
34
|
+
|
|
35
|
+
Analyze the current sub-task in the context of the original user goal and the ENTIRE PLAN so far.
|
|
36
|
+
|
|
37
|
+
**CORE INSTRUCTION:** If any application already used in the plan is capable of performing the current sub-task, your query MUST BE the name of that application to ensure continuity and efficiency. Otherwise, generate a concise query for the category of application needed.
|
|
38
|
+
|
|
39
|
+
**--- EXAMPLES ---**
|
|
40
|
+
|
|
41
|
+
**EXAMPLE 1: Reusing an app from two steps ago**
|
|
42
|
+
- **Original User Task:** "Find my latest order confirmation in Gmail, search for reviews of the main product on perplexity, and then send an email to ankit@agentr.dev telling about the reviews"
|
|
43
|
+
- **Plan So Far:**
|
|
44
|
+
- The sub-task 'Find order confirmation in Gmail' was assigned to app 'google_mail'.
|
|
45
|
+
- The sub-task 'Search for product reviews on perplexity' was assigned to app 'perplexity'.
|
|
46
|
+
- **Current Sub-task:** "send an email to ankit@agentr.dev"
|
|
47
|
+
- **CORRECT QUERY:** "google_mail"
|
|
48
|
+
|
|
49
|
+
**EXAMPLE 2: First Step (No previous context)**
|
|
50
|
+
- **Original User Task:** "Find the best restaurants in Goa."
|
|
51
|
+
- **Plan So Far:** None. This is the first step.
|
|
52
|
+
- **Current Sub-task:** "Perform a web search to find the best restaurants in Goa."
|
|
53
|
+
- **CORRECT QUERY:** "web search"
|
|
54
|
+
|
|
55
|
+
**--- YOUR TASK ---**
|
|
56
|
+
|
|
57
|
+
**Original User Task:**
|
|
58
|
+
"{original_task}"
|
|
59
|
+
|
|
60
|
+
**Plan So Far:**
|
|
61
|
+
{plan_context}
|
|
62
|
+
|
|
63
|
+
**Current Sub-task:**
|
|
64
|
+
"{sub_task}"
|
|
65
|
+
|
|
66
|
+
**YOUR CONCISE APP SEARCH QUERY:**
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
TOOL_SEARCH_QUERY_PROMPT = """
|
|
71
|
+
You are an expert at summarizing the core *action* of a sub-task into a concise query for finding a tool. This query should ignore any application names.
|
|
72
|
+
|
|
73
|
+
**INSTRUCTIONS:**
|
|
74
|
+
1. Focus only on the verb or action being performed in the sub-task.
|
|
75
|
+
2. Include key entities related to the action.
|
|
76
|
+
3. Do NOT include the names of applications (e.g., "Perplexity", "Gmail").
|
|
77
|
+
4. You also need to realise that this query is going to be used to search for tools in a particular app. And the names and description of these tools are going to be general in nature so the query should not be too specific. The sub task which you will get may be specific in nature but the query must be general.
|
|
78
|
+
|
|
79
|
+
**EXAMPLES:**
|
|
80
|
+
- **Sub-task:** "Perform a web search using Perplexity to find the best restaurants in Goa."
|
|
81
|
+
- **Query:** "web search for restaurants"
|
|
82
|
+
|
|
83
|
+
- **Sub-task:** "Fetch all marketing emails received from Gmail in the last 7 days."
|
|
84
|
+
- **Query:** "get emails by date"
|
|
85
|
+
|
|
86
|
+
- **Sub-task:** "Create a new Google Doc and append a summary."
|
|
87
|
+
- **Query:** "create document, append text"
|
|
88
|
+
|
|
89
|
+
**SUB-TASK:**
|
|
90
|
+
"{sub_task}"
|
|
91
|
+
|
|
92
|
+
**YOUR CONCISE TOOL SEARCH QUERY:**
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
REVISE_DECOMPOSITION_PROMPT = """
|
|
96
|
+
You are an expert planner who revises plans that have failed. Your previous attempt to break down a task resulted in a sub-task that could not be matched with any available tools.
|
|
97
|
+
|
|
98
|
+
**INSTRUCTIONS:**
|
|
99
|
+
1. Analyze the original user task and the failed sub-task.
|
|
100
|
+
2. Generate a NEW, alternative decomposition of the original task.
|
|
101
|
+
3. This new plan should try to achieve the same overall goal but with different, perhaps broader or more combined, sub-tasks to increase the chance of finding a suitable tool.
|
|
102
|
+
|
|
103
|
+
**ORIGINAL USER TASK:**
|
|
104
|
+
"{task}"
|
|
105
|
+
|
|
106
|
+
**FAILED SUB-TASK FROM PREVIOUS PLAN:**
|
|
107
|
+
"{failed_sub_task}"
|
|
108
|
+
|
|
109
|
+
**YOUR NEW, REVISED DECOMPOSITION (as a list of strings):**
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
TOOL_SELECTION_PROMPT = """
|
|
114
|
+
You are an AI assistant that selects the most appropriate tool(s) from a list to accomplish a specific sub-task.
|
|
115
|
+
|
|
116
|
+
**INSTRUCTIONS:**
|
|
117
|
+
1. Carefully review the sub-task to understand the required action.
|
|
118
|
+
2. Examine the list of available tools and their descriptions.
|
|
119
|
+
3. Select the best tool ID that matches the sub-task. You are encouraged to select multiple tools if there are multiple tools with similar capabilties
|
|
120
|
+
or names. It is always good to have more tools than having insufficent tools.
|
|
121
|
+
4. If no tool is a good fit, return an empty list.
|
|
122
|
+
5. Only return the tool IDs.
|
|
123
|
+
6. You should understand that the sub task maybe specific in nature but the tools are made to be general purpose and therefore the tool_candidates you will get will be very general purpose but that should not stop you from selecting the tools as these tools will be given to a very smart agent who will be able to use these tools for the specific sub-taks
|
|
124
|
+
|
|
125
|
+
**SUB-TASK:**
|
|
126
|
+
"{sub_task}"
|
|
127
|
+
|
|
128
|
+
**AVAILABLE TOOLS:**
|
|
129
|
+
{tool_candidates}
|
|
130
|
+
|
|
131
|
+
**YOUR SELECTED TOOL ID(s):**
|
|
132
|
+
"""
|