universal-mcp-agents 0.1.8__py3-none-any.whl → 0.1.10__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 +11 -8
- universal_mcp/agents/base.py +13 -18
- universal_mcp/agents/bigtool2/__init__.py +6 -7
- universal_mcp/agents/bigtool2/__main__.py +2 -4
- universal_mcp/agents/bigtool2/agent.py +1 -0
- universal_mcp/agents/bigtool2/graph.py +48 -184
- universal_mcp/agents/bigtool2/meta_tools.py +120 -0
- universal_mcp/agents/bigtoolcache/__init__.py +31 -22
- universal_mcp/agents/bigtoolcache/__main__.py +1 -4
- universal_mcp/agents/bigtoolcache/agent.py +1 -3
- universal_mcp/agents/bigtoolcache/graph.py +101 -191
- universal_mcp/agents/bigtoolcache/prompts.py +7 -31
- universal_mcp/agents/bigtoolcache/tools.py +141 -0
- universal_mcp/agents/builder.py +10 -20
- universal_mcp/agents/cli.py +1 -2
- universal_mcp/agents/codeact/__init__.py +2 -254
- universal_mcp/agents/codeact/__main__.py +35 -0
- universal_mcp/agents/codeact/agent.py +160 -0
- universal_mcp/agents/codeact/prompts.py +91 -0
- universal_mcp/agents/codeact/sandbox.py +42 -18
- universal_mcp/agents/codeact/state.py +10 -0
- universal_mcp/agents/codeact/utils.py +12 -5
- universal_mcp/agents/hil.py +1 -6
- universal_mcp/agents/planner/__init__.py +1 -3
- universal_mcp/agents/planner/graph.py +1 -3
- universal_mcp/agents/react.py +14 -6
- universal_mcp/agents/shared/prompts.py +31 -17
- universal_mcp/agents/shared/tool_node.py +68 -53
- universal_mcp/agents/simple.py +2 -1
- universal_mcp/agents/utils.py +4 -15
- universal_mcp/applications/ui/app.py +5 -15
- {universal_mcp_agents-0.1.8.dist-info → universal_mcp_agents-0.1.10.dist-info}/METADATA +2 -1
- universal_mcp_agents-0.1.10.dist-info/RECORD +42 -0
- universal_mcp/agents/autoagent/__init__.py +0 -30
- universal_mcp/agents/autoagent/__main__.py +0 -25
- universal_mcp/agents/autoagent/context.py +0 -26
- universal_mcp/agents/autoagent/graph.py +0 -170
- universal_mcp/agents/autoagent/prompts.py +0 -9
- universal_mcp/agents/autoagent/state.py +0 -27
- universal_mcp/agents/autoagent/utils.py +0 -13
- universal_mcp/agents/bigtool/__init__.py +0 -58
- universal_mcp/agents/bigtool/__main__.py +0 -23
- universal_mcp/agents/bigtool/graph.py +0 -210
- universal_mcp/agents/bigtool/prompts.py +0 -31
- universal_mcp/agents/bigtool/state.py +0 -27
- universal_mcp/agents/bigtoolcache/tools_all.txt +0 -956
- universal_mcp/agents/bigtoolcache/tools_important.txt +0 -474
- universal_mcp/agents/codeact/test.py +0 -16
- universal_mcp_agents-0.1.8.dist-info/RECORD +0 -51
- {universal_mcp_agents-0.1.8.dist-info → universal_mcp_agents-0.1.10.dist-info}/WHEEL +0 -0
|
@@ -1,27 +1,51 @@
|
|
|
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], timeout: int = 10) -> 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"
|
|
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.
|
|
12
32
|
try:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
output = await asyncio.wait_for(asyncio.to_thread(sync_eval_in_thread), timeout=timeout)
|
|
34
|
+
except asyncio.TimeoutError:
|
|
35
|
+
output = f"Error: Code execution timed out after {timeout} seconds."
|
|
36
|
+
result += output
|
|
37
|
+
|
|
38
|
+
# Identify all variables that are not part of the original builtins
|
|
39
|
+
# and were not in the initial _locals, or were changed.
|
|
40
|
+
changed_vars = {}
|
|
41
|
+
builtin_keys = set(builtins.__dict__.keys())
|
|
42
|
+
|
|
43
|
+
for key, value in execution_env.items():
|
|
44
|
+
if key in builtin_keys:
|
|
45
|
+
continue # Skip builtins
|
|
46
|
+
|
|
47
|
+
# Check if the key is new or if the value has changed
|
|
48
|
+
if key not in _locals or _locals[key] is not value:
|
|
49
|
+
changed_vars[key] = value
|
|
50
|
+
|
|
51
|
+
return result, changed_vars
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
+
from universal_mcp.logger import logger
|
|
4
|
+
|
|
3
5
|
BACKTICK_PATTERN = r"(?:^|\n)```(.*?)(?:```(?:\n|$))"
|
|
4
6
|
|
|
5
7
|
|
|
@@ -37,7 +39,12 @@ def extract_and_combine_codeblocks(text: str) -> str:
|
|
|
37
39
|
"""
|
|
38
40
|
# Find all code blocks in the text using regex
|
|
39
41
|
# Pattern matches anything between triple backticks, with or without a language identifier
|
|
40
|
-
|
|
42
|
+
try:
|
|
43
|
+
code_blocks = re.findall(BACKTICK_PATTERN, text, re.DOTALL)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.error(f"Error extracting code blocks: {e}")
|
|
46
|
+
logger.error(f"Text: {text}")
|
|
47
|
+
return ""
|
|
41
48
|
|
|
42
49
|
if not code_blocks:
|
|
43
50
|
return ""
|
|
@@ -46,15 +53,15 @@ def extract_and_combine_codeblocks(text: str) -> str:
|
|
|
46
53
|
processed_blocks = []
|
|
47
54
|
for block in code_blocks:
|
|
48
55
|
# Strip leading and trailing whitespace
|
|
49
|
-
|
|
56
|
+
cleaned_block = block.strip()
|
|
50
57
|
|
|
51
58
|
# If the first line looks like a language identifier, remove it
|
|
52
|
-
lines =
|
|
59
|
+
lines = cleaned_block.split("\n")
|
|
53
60
|
if lines and (not lines[0].strip() or " " not in lines[0].strip()):
|
|
54
61
|
# First line is empty or likely a language identifier (no spaces)
|
|
55
|
-
|
|
62
|
+
cleaned_block = "\n".join(lines[1:])
|
|
56
63
|
|
|
57
|
-
processed_blocks.append(
|
|
64
|
+
processed_blocks.append(cleaned_block)
|
|
58
65
|
|
|
59
66
|
# Combine all codeblocks with newlines between them
|
|
60
67
|
combined_code = "\n\n".join(processed_blocks)
|
universal_mcp/agents/hil.py
CHANGED
|
@@ -63,12 +63,7 @@ def handle_interrupt(interrupt: Interrupt) -> str | bool:
|
|
|
63
63
|
value = input("Do you accept this? (y/n): " + interrupt.value["question"])
|
|
64
64
|
return value.lower() in ["y", "yes"]
|
|
65
65
|
elif interrupt_type == "choice":
|
|
66
|
-
value = input(
|
|
67
|
-
"Enter your choice: "
|
|
68
|
-
+ interrupt.value["question"]
|
|
69
|
-
+ " "
|
|
70
|
-
+ ", ".join(interrupt.value["choices"])
|
|
71
|
-
)
|
|
66
|
+
value = input("Enter your choice: " + interrupt.value["question"] + " " + ", ".join(interrupt.value["choices"]))
|
|
72
67
|
if value in interrupt.value["choices"]:
|
|
73
68
|
return value
|
|
74
69
|
else:
|
|
@@ -26,9 +26,7 @@ class PlannerAgent(BaseAgent):
|
|
|
26
26
|
self.executor_agent_cls = executor_agent_cls
|
|
27
27
|
|
|
28
28
|
def _build_system_message(self):
|
|
29
|
-
return DEVELOPER_PROMPT.format(
|
|
30
|
-
name=self.name, instructions=self.instructions
|
|
31
|
-
)
|
|
29
|
+
return DEVELOPER_PROMPT.format(name=self.name, instructions=self.instructions)
|
|
32
30
|
|
|
33
31
|
async def _build_graph(self):
|
|
34
32
|
return build_graph(
|
|
@@ -18,9 +18,7 @@ def build_graph(llm, registry, instructions, model, executor_agent_cls):
|
|
|
18
18
|
task = state["messages"][-1].content
|
|
19
19
|
logger.info(f"Running tool finder for task: {task}")
|
|
20
20
|
tool_finder_graph = build_tool_node_graph(llm, registry)
|
|
21
|
-
tool_finder_state = await tool_finder_graph.ainvoke(
|
|
22
|
-
{"original_task": task, "messages": state["messages"]}
|
|
23
|
-
)
|
|
21
|
+
tool_finder_state = await tool_finder_graph.ainvoke({"original_task": task, "messages": state["messages"]})
|
|
24
22
|
|
|
25
23
|
if not tool_finder_state.get("apps_required"):
|
|
26
24
|
logger.info("Tool finder determined no apps are required.")
|
universal_mcp/agents/react.py
CHANGED
|
@@ -4,6 +4,7 @@ from loguru import logger
|
|
|
4
4
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
5
5
|
from universal_mcp.tools.registry import ToolRegistry
|
|
6
6
|
from universal_mcp.types import ToolConfig, ToolFormat
|
|
7
|
+
from rich import print
|
|
7
8
|
|
|
8
9
|
from universal_mcp.agents.base import BaseAgent
|
|
9
10
|
from universal_mcp.agents.llm import load_chat_model
|
|
@@ -40,7 +41,18 @@ class ReactAgent(BaseAgent):
|
|
|
40
41
|
self.llm = load_chat_model(model)
|
|
41
42
|
self.tools = tools or {}
|
|
42
43
|
if "ui" not in self.tools:
|
|
43
|
-
self.tools["ui"] = [
|
|
44
|
+
self.tools["ui"] = [
|
|
45
|
+
"create_bar_chart",
|
|
46
|
+
"create_line_chart",
|
|
47
|
+
"create_pie_chart",
|
|
48
|
+
"create_table",
|
|
49
|
+
"http_get",
|
|
50
|
+
"http_post",
|
|
51
|
+
"http_put",
|
|
52
|
+
"http_delete",
|
|
53
|
+
"http_patch",
|
|
54
|
+
"read_file",
|
|
55
|
+
]
|
|
44
56
|
self.max_iterations = max_iterations
|
|
45
57
|
self.registry = registry
|
|
46
58
|
|
|
@@ -54,7 +66,6 @@ class ReactAgent(BaseAgent):
|
|
|
54
66
|
else:
|
|
55
67
|
tools = []
|
|
56
68
|
|
|
57
|
-
|
|
58
69
|
logger.debug(f"Initialized ReactAgent: name={self.name}, model={self.model}")
|
|
59
70
|
return create_react_agent(
|
|
60
71
|
self.llm,
|
|
@@ -75,10 +86,7 @@ async def main():
|
|
|
75
86
|
tools={"google-mail": ["send_email"]},
|
|
76
87
|
registry=AgentrRegistry(),
|
|
77
88
|
)
|
|
78
|
-
result = await agent.invoke(
|
|
79
|
-
"Send an email with the subject 'testing react agent' to manoj@agentr.dev"
|
|
80
|
-
)
|
|
81
|
-
from rich import print
|
|
89
|
+
result = await agent.invoke("Send an email with the subject 'testing react agent' to manoj@agentr.dev")
|
|
82
90
|
|
|
83
91
|
print(messages_to_list(result["messages"]))
|
|
84
92
|
|
|
@@ -6,7 +6,7 @@ You are an expert planner. Your goal is to consolidate a complex user request in
|
|
|
6
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
7
|
3. **Assume Internal Capabilities:** Do NOT create sub-tasks for abstract cognitive work like 'summarize' or 'analyze'.
|
|
8
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
|
-
|
|
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
10
|
**--- EXAMPLES ---**
|
|
11
11
|
|
|
12
12
|
**EXAMPLE 1:**
|
|
@@ -30,25 +30,37 @@ You are an expert planner. Your goal is to consolidate a complex user request in
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
APP_SEARCH_QUERY_PROMPT = """
|
|
33
|
-
You are an expert at
|
|
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
34
|
|
|
35
|
-
|
|
36
|
-
1. Read the sub-task carefully.
|
|
37
|
-
2. If an application is explicitly named (e.g., "Perplexity", "Gmail", "GitHub"), your query should be ONLY that name.
|
|
38
|
-
3. If no specific application is named, generate a query for the *category* of application (e.g., "web search", "email client", "document editor").
|
|
39
|
-
4. The query should be concise.
|
|
35
|
+
Analyze the current sub-task in the context of the original user goal and the ENTIRE PLAN so far.
|
|
40
36
|
|
|
41
|
-
**
|
|
42
|
-
- **Sub-task:** "Perform a web search using Perplexity to find the best restaurants in Goa."
|
|
43
|
-
- **Query:** "Perplexity"
|
|
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.
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
- **Query:** "Gmail"
|
|
39
|
+
**--- EXAMPLES ---**
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
- **
|
|
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"
|
|
50
48
|
|
|
51
|
-
**
|
|
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:**
|
|
52
64
|
"{sub_task}"
|
|
53
65
|
|
|
54
66
|
**YOUR CONCISE APP SEARCH QUERY:**
|
|
@@ -62,6 +74,7 @@ You are an expert at summarizing the core *action* of a sub-task into a concise
|
|
|
62
74
|
1. Focus only on the verb or action being performed in the sub-task.
|
|
63
75
|
2. Include key entities related to the action.
|
|
64
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.
|
|
65
78
|
|
|
66
79
|
**EXAMPLES:**
|
|
67
80
|
- **Sub-task:** "Perform a web search using Perplexity to find the best restaurants in Goa."
|
|
@@ -103,10 +116,11 @@ You are an AI assistant that selects the most appropriate tool(s) from a list to
|
|
|
103
116
|
**INSTRUCTIONS:**
|
|
104
117
|
1. Carefully review the sub-task to understand the required action.
|
|
105
118
|
2. Examine the list of available tools and their descriptions.
|
|
106
|
-
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
|
|
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
|
|
107
120
|
or names. It is always good to have more tools than having insufficent tools.
|
|
108
121
|
4. If no tool is a good fit, return an empty list.
|
|
109
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
|
|
110
124
|
|
|
111
125
|
**SUB-TASK:**
|
|
112
126
|
"{sub_task}"
|
|
@@ -115,4 +129,4 @@ or names. It is always good to have more tools than having insufficent tools.
|
|
|
115
129
|
{tool_candidates}
|
|
116
130
|
|
|
117
131
|
**YOUR SELECTED TOOL ID(s):**
|
|
118
|
-
"""
|
|
132
|
+
"""
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Annotated, TypedDict
|
|
2
|
+
from typing import Annotated, TypedDict
|
|
3
3
|
|
|
4
4
|
from langchain_core.language_models import BaseChatModel
|
|
5
5
|
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage
|
|
@@ -17,43 +17,50 @@ from universal_mcp.agents.shared.prompts import (
|
|
|
17
17
|
TOOL_SELECTION_PROMPT,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
MAX_DECOMPOSITION_ATTEMPTS = 2
|
|
22
21
|
|
|
23
22
|
# --- Pydantic Models for Structured LLM Outputs ---
|
|
24
23
|
|
|
24
|
+
|
|
25
25
|
class TaskDecomposition(BaseModel):
|
|
26
|
-
sub_tasks:
|
|
26
|
+
sub_tasks: list[str] = Field(description="A list of sub-task descriptions.")
|
|
27
|
+
|
|
27
28
|
|
|
28
29
|
class SearchQuery(BaseModel):
|
|
29
30
|
query: str = Field(description="A concise search query.")
|
|
30
31
|
|
|
32
|
+
|
|
31
33
|
class ToolSelection(BaseModel):
|
|
32
|
-
tool_ids:
|
|
34
|
+
tool_ids: list[str] = Field(description="The IDs of the selected tools.")
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
# --- LangGraph Agent State ---
|
|
36
38
|
|
|
39
|
+
|
|
37
40
|
class SubTask(TypedDict, total=False):
|
|
38
41
|
"""Represents a single step in the execution plan."""
|
|
42
|
+
|
|
39
43
|
task: str
|
|
40
44
|
status: str # "pending", "success", "failed"
|
|
41
45
|
app_id: str
|
|
42
|
-
tool_ids:
|
|
46
|
+
tool_ids: list[str]
|
|
43
47
|
reasoning: str
|
|
44
48
|
|
|
49
|
+
|
|
45
50
|
class AgentState(TypedDict):
|
|
46
51
|
"""The central state of our agent graph."""
|
|
52
|
+
|
|
47
53
|
original_task: str
|
|
48
54
|
decomposition_attempts: int
|
|
49
55
|
failed_sub_task_info: str # To inform re-decomposition
|
|
50
|
-
sub_tasks:
|
|
51
|
-
execution_plan:
|
|
56
|
+
sub_tasks: list[SubTask]
|
|
57
|
+
execution_plan: list[SubTask]
|
|
52
58
|
messages: Annotated[list[AnyMessage], add_messages]
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
# --- Graph Builder ---
|
|
56
62
|
|
|
63
|
+
|
|
57
64
|
def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGraph:
|
|
58
65
|
"""Builds the adaptive LangGraph workflow for tool selection."""
|
|
59
66
|
|
|
@@ -65,19 +72,14 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
65
72
|
|
|
66
73
|
if attempts > 0 and failed_info:
|
|
67
74
|
logger.warning(f"Revising decomposition. Attempt {attempts + 1}.")
|
|
68
|
-
prompt = REVISE_DECOMPOSITION_PROMPT.format(
|
|
69
|
-
task=task, failed_sub_task=failed_info
|
|
70
|
-
)
|
|
75
|
+
prompt = REVISE_DECOMPOSITION_PROMPT.format(task=task, failed_sub_task=failed_info)
|
|
71
76
|
else:
|
|
72
77
|
logger.info("Performing initial task decomposition.")
|
|
73
78
|
prompt = TASK_DECOMPOSITION_PROMPT.format(task=task)
|
|
74
79
|
|
|
75
80
|
response = await llm.with_structured_output(TaskDecomposition).ainvoke(prompt)
|
|
76
|
-
sub_tasks = [
|
|
77
|
-
|
|
78
|
-
for sub_task_str in response.sub_tasks
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
+
sub_tasks = [{"task": sub_task_str, "status": "pending"} for sub_task_str in response.sub_tasks]
|
|
82
|
+
|
|
81
83
|
return {
|
|
82
84
|
"sub_tasks": sub_tasks,
|
|
83
85
|
"decomposition_attempts": attempts + 1,
|
|
@@ -85,38 +87,50 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
async def _resolve_sub_tasks(state: AgentState) -> AgentState:
|
|
88
|
-
"""Iterates through sub-tasks,
|
|
90
|
+
"""Iterates through sub-tasks, providing full plan context to the app selection prompt."""
|
|
89
91
|
sub_tasks = state["sub_tasks"]
|
|
92
|
+
original_task = state["original_task"]
|
|
90
93
|
current_plan = []
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
for i, sub_task in enumerate(sub_tasks):
|
|
93
96
|
task_desc = sub_task["task"]
|
|
94
97
|
logger.info(f"Resolving sub-task: '{task_desc}'")
|
|
95
98
|
|
|
96
|
-
# 1.
|
|
97
|
-
|
|
99
|
+
# 1. Build the FULL context string from the entire plan so far
|
|
100
|
+
if not current_plan:
|
|
101
|
+
plan_context_str = "None. This is the first step."
|
|
102
|
+
else:
|
|
103
|
+
context_lines = [
|
|
104
|
+
f"- The sub-task '{step['task']}' was assigned to app '{step['app_id']}'." for step in current_plan
|
|
105
|
+
]
|
|
106
|
+
plan_context_str = "\n".join(context_lines)
|
|
107
|
+
|
|
108
|
+
# 2. Generate the App-specific query using the NEW full-context prompt
|
|
109
|
+
app_query_prompt = APP_SEARCH_QUERY_PROMPT.format(
|
|
110
|
+
original_task=original_task, plan_context=plan_context_str, sub_task=task_desc
|
|
111
|
+
)
|
|
98
112
|
app_query_response = await llm.with_structured_output(SearchQuery).ainvoke(app_query_prompt)
|
|
99
113
|
app_search_query = app_query_response.query
|
|
100
|
-
logger.info(f"Generated app search query: '{app_search_query}'")
|
|
114
|
+
logger.info(f"Generated context-aware app search query: '{app_search_query}'")
|
|
101
115
|
|
|
102
|
-
#
|
|
116
|
+
# 3. Search for candidate apps (the rest of the logic is the same)
|
|
103
117
|
candidate_apps = await registry.search_apps(query=app_search_query, limit=5)
|
|
104
118
|
if not candidate_apps:
|
|
105
119
|
logger.error(f"No apps found for query '{app_search_query}' from sub-task: '{task_desc}'")
|
|
106
120
|
return {"failed_sub_task_info": task_desc, "sub_tasks": []}
|
|
107
121
|
|
|
108
|
-
#
|
|
122
|
+
# 4. Generate Action-specific query for finding the tool
|
|
109
123
|
tool_query_prompt = TOOL_SEARCH_QUERY_PROMPT.format(sub_task=task_desc)
|
|
110
124
|
tool_query_response = await llm.with_structured_output(SearchQuery).ainvoke(tool_query_prompt)
|
|
111
125
|
tool_search_query = tool_query_response.query
|
|
112
126
|
logger.info(f"Generated tool search query: '{tool_search_query}'")
|
|
113
127
|
|
|
114
|
-
#
|
|
128
|
+
# 5. Find a suitable tool within the candidate apps
|
|
115
129
|
tool_found = False
|
|
116
130
|
for app in candidate_apps:
|
|
117
131
|
app_id = app["id"]
|
|
118
132
|
logger.info(f"Searching for tools in app '{app_id}' with query '{tool_search_query}'...")
|
|
119
|
-
|
|
133
|
+
|
|
120
134
|
found_tools = await registry.search_tools(query=tool_search_query, app_id=app_id, limit=5)
|
|
121
135
|
if not found_tools:
|
|
122
136
|
continue
|
|
@@ -124,19 +138,21 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
124
138
|
tool_candidates_str = "\n - ".join([f"{tool['name']}: {tool['description']}" for tool in found_tools])
|
|
125
139
|
selection_prompt = TOOL_SELECTION_PROMPT.format(sub_task=task_desc, tool_candidates=tool_candidates_str)
|
|
126
140
|
selection_response = await llm.with_structured_output(ToolSelection).ainvoke(selection_prompt)
|
|
127
|
-
|
|
141
|
+
|
|
128
142
|
if selection_response.tool_ids:
|
|
129
143
|
logger.success(f"Found and selected tool(s) {selection_response.tool_ids} in app '{app_id}'.")
|
|
130
|
-
sub_task.update(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
sub_task.update(
|
|
145
|
+
{
|
|
146
|
+
"status": "success",
|
|
147
|
+
"app_id": app_id,
|
|
148
|
+
"tool_ids": selection_response.tool_ids,
|
|
149
|
+
"reasoning": f"Selected tool(s) {selection_response.tool_ids} from app '{app_id}' for sub-task.",
|
|
150
|
+
}
|
|
151
|
+
)
|
|
136
152
|
current_plan.append(sub_task)
|
|
137
153
|
tool_found = True
|
|
138
154
|
break
|
|
139
|
-
|
|
155
|
+
|
|
140
156
|
if not tool_found:
|
|
141
157
|
logger.error(f"Could not find any suitable tool for sub-task: '{task_desc}'")
|
|
142
158
|
return {"failed_sub_task_info": task_desc, "sub_tasks": []}
|
|
@@ -147,7 +163,11 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
147
163
|
"""Handles the case where all decomposition attempts have failed."""
|
|
148
164
|
logger.error("Maximum decomposition attempts reached. Planning failed.")
|
|
149
165
|
return {
|
|
150
|
-
"messages": [
|
|
166
|
+
"messages": [
|
|
167
|
+
AIMessage(
|
|
168
|
+
content="I am unable to create a complete plan for this task with the available tools. Please try rephrasing your request."
|
|
169
|
+
)
|
|
170
|
+
]
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
def _consolidate_plan(state: AgentState) -> AgentState:
|
|
@@ -157,7 +177,7 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
157
177
|
"""
|
|
158
178
|
logger.info("Consolidating final execution plan.")
|
|
159
179
|
plan = state["execution_plan"]
|
|
160
|
-
merged_apps:
|
|
180
|
+
merged_apps: dict[str, SubTask] = {}
|
|
161
181
|
|
|
162
182
|
for step in plan:
|
|
163
183
|
app_id = step["app_id"]
|
|
@@ -174,9 +194,8 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
174
194
|
for app_id, step_data in merged_apps.items():
|
|
175
195
|
step_data["tool_ids"] = sorted(list(step_data["tool_ids"]))
|
|
176
196
|
final_plan.append(step_data)
|
|
177
|
-
|
|
178
|
-
return {"execution_plan": final_plan}
|
|
179
197
|
|
|
198
|
+
return {"execution_plan": final_plan}
|
|
180
199
|
|
|
181
200
|
# --- Graph Definition ---
|
|
182
201
|
|
|
@@ -184,26 +203,26 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
184
203
|
|
|
185
204
|
workflow.add_node("decompose_task", _decompose_task)
|
|
186
205
|
workflow.add_node("resolve_sub_tasks", _resolve_sub_tasks)
|
|
187
|
-
workflow.add_node("consolidate_plan", _consolidate_plan)
|
|
206
|
+
workflow.add_node("consolidate_plan", _consolidate_plan) # NEW NODE
|
|
188
207
|
workflow.add_node("handle_planning_failure", _handle_planning_failure)
|
|
189
208
|
|
|
190
209
|
workflow.set_entry_point("decompose_task")
|
|
191
210
|
|
|
192
211
|
def should_continue(state: AgentState):
|
|
193
|
-
if not state.get("sub_tasks"):
|
|
212
|
+
if not state.get("sub_tasks"): # Resolution failed or succeeded
|
|
194
213
|
if state.get("execution_plan"):
|
|
195
|
-
return "consolidate_plan"
|
|
214
|
+
return "consolidate_plan" # MODIFIED: Go to consolidate on success
|
|
196
215
|
elif state["decomposition_attempts"] >= MAX_DECOMPOSITION_ATTEMPTS:
|
|
197
216
|
return "handle_planning_failure"
|
|
198
217
|
else:
|
|
199
|
-
return "decompose_task"
|
|
218
|
+
return "decompose_task" # Re-try decomposition
|
|
200
219
|
else:
|
|
201
220
|
return "resolve_sub_tasks"
|
|
202
221
|
|
|
203
222
|
workflow.add_conditional_edges("decompose_task", lambda s: "resolve_sub_tasks")
|
|
204
223
|
workflow.add_conditional_edges("resolve_sub_tasks", should_continue)
|
|
205
|
-
|
|
206
|
-
workflow.add_edge("consolidate_plan", END)
|
|
224
|
+
|
|
225
|
+
workflow.add_edge("consolidate_plan", END) # NEW EDGE
|
|
207
226
|
workflow.add_edge("handle_planning_failure", END)
|
|
208
227
|
|
|
209
228
|
return workflow.compile()
|
|
@@ -212,15 +231,16 @@ def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGr
|
|
|
212
231
|
async def main():
|
|
213
232
|
"""Main function to run the agent."""
|
|
214
233
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
234
|
+
|
|
215
235
|
from universal_mcp.agents.llm import load_chat_model
|
|
216
236
|
|
|
217
237
|
registry = AgentrRegistry()
|
|
218
238
|
llm = load_chat_model("anthropic/claude-4-sonnet-20250514")
|
|
219
|
-
|
|
239
|
+
|
|
220
240
|
graph = build_tool_node_graph(llm, registry)
|
|
221
241
|
|
|
222
|
-
task = "
|
|
223
|
-
|
|
242
|
+
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"
|
|
243
|
+
|
|
224
244
|
initial_state = {
|
|
225
245
|
"original_task": task,
|
|
226
246
|
"messages": [HumanMessage(content=task)],
|
|
@@ -229,17 +249,12 @@ async def main():
|
|
|
229
249
|
|
|
230
250
|
final_state = await graph.ainvoke(initial_state)
|
|
231
251
|
|
|
232
|
-
print("\n--- Final Agent State ---")
|
|
233
252
|
if final_state.get("execution_plan"):
|
|
234
|
-
print("Successfully created a consolidated execution plan:")
|
|
235
253
|
for step in final_state["execution_plan"]:
|
|
236
|
-
|
|
237
|
-
print(f" - App: {step['app_id']}")
|
|
238
|
-
print(f" - Tool(s): {', '.join(step['tool_ids'])}")
|
|
254
|
+
pass
|
|
239
255
|
else:
|
|
240
|
-
|
|
241
|
-
print(f"Final message: {final_state['messages'][-1].content}")
|
|
256
|
+
pass
|
|
242
257
|
|
|
243
258
|
|
|
244
259
|
if __name__ == "__main__":
|
|
245
|
-
asyncio.run(main())
|
|
260
|
+
asyncio.run(main())
|
universal_mcp/agents/simple.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Annotated
|
|
|
4
4
|
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
5
5
|
from langgraph.graph import END, START, StateGraph
|
|
6
6
|
from langgraph.graph.message import add_messages
|
|
7
|
+
from rich import print
|
|
7
8
|
from typing_extensions import TypedDict
|
|
8
9
|
|
|
9
10
|
from universal_mcp.agents.base import BaseAgent
|
|
@@ -52,10 +53,10 @@ class SimpleAgent(BaseAgent):
|
|
|
52
53
|
graph_builder.add_edge("chatbot", END)
|
|
53
54
|
return graph_builder.compile(checkpointer=self.memory)
|
|
54
55
|
|
|
56
|
+
|
|
55
57
|
async def main():
|
|
56
58
|
agent = SimpleAgent("Simple Agent", "Act as a 14 year old kid, reply in Gen-Z lingo", "azure/gpt-5-mini")
|
|
57
59
|
output = await agent.invoke("What is the capital of France?")
|
|
58
|
-
from rich import print
|
|
59
60
|
print(messages_to_list(output["messages"]))
|
|
60
61
|
|
|
61
62
|
|
universal_mcp/agents/utils.py
CHANGED
|
@@ -8,9 +8,6 @@ from rich.markdown import Markdown
|
|
|
8
8
|
from rich.panel import Panel
|
|
9
9
|
from rich.prompt import Prompt
|
|
10
10
|
from rich.table import Table
|
|
11
|
-
from universal_mcp.tools.manager import ToolManager
|
|
12
|
-
from universal_mcp.types import ToolFormat
|
|
13
|
-
|
|
14
11
|
|
|
15
12
|
|
|
16
13
|
class RichCLI:
|
|
@@ -28,9 +25,7 @@ Available commands:
|
|
|
28
25
|
- `/tools` - List available tools
|
|
29
26
|
- `/exit` - Exit the application
|
|
30
27
|
"""
|
|
31
|
-
self.console.print(
|
|
32
|
-
Panel(Markdown(welcome_text), title="🤖 AI Agent CLI", border_style="blue")
|
|
33
|
-
)
|
|
28
|
+
self.console.print(Panel(Markdown(welcome_text), title="🤖 AI Agent CLI", border_style="blue"))
|
|
34
29
|
|
|
35
30
|
def display_agent_response(self, response: str, agent_name: str):
|
|
36
31
|
"""Display agent response with formatting"""
|
|
@@ -54,13 +49,9 @@ Available commands:
|
|
|
54
49
|
# Check if type has changed and reset content if so
|
|
55
50
|
if self.type_ != type_:
|
|
56
51
|
if type_ == "thinking":
|
|
57
|
-
self.content +=
|
|
58
|
-
"\n[bold yellow]💭 Thinking:[/bold yellow] :"
|
|
59
|
-
)
|
|
52
|
+
self.content += "\n[bold yellow]💭 Thinking:[/bold yellow] :"
|
|
60
53
|
elif type_ == "text":
|
|
61
|
-
self.content +=
|
|
62
|
-
f"\n[bold green]🤖 {agent_name}[/bold green] :"
|
|
63
|
-
)
|
|
54
|
+
self.content += f"\n[bold green]🤖 {agent_name}[/bold green] :"
|
|
64
55
|
self.type_ = type_
|
|
65
56
|
self.content += chunk
|
|
66
57
|
content_text = "".join(self.content)
|
|
@@ -120,9 +111,7 @@ Available commands:
|
|
|
120
111
|
value = Prompt.ask(interrupt.value["question"])
|
|
121
112
|
return value
|
|
122
113
|
elif interrupt_type == "bool":
|
|
123
|
-
value = Prompt.ask(
|
|
124
|
-
interrupt.value["question"], choices=["y", "n"], default="y"
|
|
125
|
-
)
|
|
114
|
+
value = Prompt.ask(interrupt.value["question"], choices=["y", "n"], default="y")
|
|
126
115
|
return value
|
|
127
116
|
elif interrupt_type == "choice":
|
|
128
117
|
value = Prompt.ask(
|