universal-mcp-agents 0.1.13__py3-none-any.whl → 0.1.15__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 +1 -1
- universal_mcp/agents/base.py +3 -0
- universal_mcp/agents/bigtool/__init__.py +1 -1
- universal_mcp/agents/bigtool/__main__.py +4 -3
- universal_mcp/agents/bigtool/agent.py +3 -2
- universal_mcp/agents/bigtool/graph.py +68 -31
- universal_mcp/agents/bigtool/prompts.py +2 -2
- universal_mcp/agents/bigtool/tools.py +17 -4
- universal_mcp/agents/builder/__main__.py +129 -28
- universal_mcp/agents/builder/builder.py +149 -161
- universal_mcp/agents/builder/helper.py +71 -0
- universal_mcp/agents/builder/prompts.py +94 -160
- universal_mcp/agents/codeact0/__init__.py +2 -1
- universal_mcp/agents/codeact0/agent.py +13 -5
- universal_mcp/agents/codeact0/langgraph_agent.py +14 -0
- universal_mcp/agents/codeact0/llm_tool.py +1 -2
- universal_mcp/agents/codeact0/playbook_agent.py +353 -0
- universal_mcp/agents/codeact0/prompts.py +126 -41
- universal_mcp/agents/codeact0/sandbox.py +43 -32
- universal_mcp/agents/codeact0/state.py +27 -3
- universal_mcp/agents/codeact0/tools.py +180 -0
- universal_mcp/agents/codeact0/utils.py +89 -75
- universal_mcp/agents/shared/__main__.py +44 -0
- universal_mcp/agents/shared/prompts.py +49 -98
- universal_mcp/agents/shared/tool_node.py +160 -176
- universal_mcp/agents/utils.py +71 -0
- universal_mcp/applications/ui/app.py +2 -2
- {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/METADATA +3 -3
- universal_mcp_agents-0.1.15.dist-info/RECORD +50 -0
- universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
- universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -13
- universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
- universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
- universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
- universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
- universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
- universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
- universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
- universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
- universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
- universal_mcp/agents/planner/__init__.py +0 -51
- universal_mcp/agents/planner/__main__.py +0 -28
- universal_mcp/agents/planner/graph.py +0 -85
- universal_mcp/agents/planner/prompts.py +0 -14
- universal_mcp/agents/planner/state.py +0 -11
- universal_mcp_agents-0.1.13.dist-info/RECORD +0 -63
- {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -1,132 +1,83 @@
|
|
|
1
|
-
|
|
2
|
-
You are an expert
|
|
1
|
+
TOOL_SEARCH_QUERIES_PROMPT = """
|
|
2
|
+
You are an expert at breaking down a complex user task into a list of simple, atomic search queries for finding tools. Your goal is to generate a list of queries that will cover all aspects of the user's request.
|
|
3
3
|
|
|
4
4
|
**CORE PRINCIPLES:**
|
|
5
|
-
1. **
|
|
6
|
-
2. **
|
|
7
|
-
3. **
|
|
8
|
-
4. **
|
|
9
|
-
|
|
5
|
+
1. **Deconstruct the Task:** Analyze the user's request and identify the distinct actions or sub-tasks required.
|
|
6
|
+
2. **Include Application Context:** If the user mentions a specific application (e.g., Gmail, Google Docs, Exa), include it in the query.
|
|
7
|
+
3. **Focus on the Action:** Each query must describe a general capability. It should combine the core action (verb) and the general type of object it acts on (e.g., "create document", "get pull requests", "web search").
|
|
8
|
+
4. **STRIP SPECIFIC DETAILS:** **This is critical.** Do NOT include specific data, parameters, names, or details from the user's prompt in your queries. Your goal is to find a general tool, not to run the specific command.
|
|
9
|
+
|
|
10
10
|
**--- EXAMPLES ---**
|
|
11
11
|
|
|
12
12
|
**EXAMPLE 1:**
|
|
13
13
|
- **User Task:** "Create a Google Doc summarizing the last 5 merged pull requests in my GitHub repo universal-mcp/universal-mcp."
|
|
14
|
-
- **CORRECT
|
|
15
|
-
- "
|
|
16
|
-
- "
|
|
14
|
+
- **CORRECT QUERIES:**
|
|
15
|
+
- "github get pull requests from repository"
|
|
16
|
+
- "google docs create document"
|
|
17
|
+
- "google docs append text to document"
|
|
18
|
+
- **INCORRECT QUERIES:**
|
|
19
|
+
- "github get pull requests from universal-mcp/universal-mcp" (Contains specific repo name)
|
|
20
|
+
- "google docs create 'summary' document" (Contains specific document title)
|
|
21
|
+
|
|
17
22
|
|
|
18
23
|
**EXAMPLE 2:**
|
|
19
|
-
- **User Task:** "Find the best restaurants in Goa using
|
|
20
|
-
- **CORRECT
|
|
21
|
-
- "
|
|
24
|
+
- **User Task:** "Find the best restaurants in Goa using exa web search, then email the list to my friend at test@example.com."
|
|
25
|
+
- **CORRECT QUERIES:**
|
|
26
|
+
- "exa web search"
|
|
27
|
+
- "send email"
|
|
28
|
+
- **INCORRECT QUERIES:**
|
|
29
|
+
- "exa search for best restaurants in Goa" (Contains specific search details)
|
|
30
|
+
- "email list to test@example.com" (Contains specific parameters)
|
|
31
|
+
|
|
32
|
+
**EXAMPLE 3:**
|
|
33
|
+
- **User Task:** "add an event to my google calendar at 2pm called 'Walk in the park'?"
|
|
34
|
+
- **CORRECT QUERIES:**
|
|
35
|
+
- "google calendar create calendar event"
|
|
36
|
+
- **INCORRECT QUERIES:**
|
|
37
|
+
- "google calendar create event 'Walk in the park' at 2pm" (Contains specific event details)
|
|
22
38
|
|
|
23
39
|
**--- YOUR TASK ---**
|
|
24
40
|
|
|
25
41
|
**USER TASK:**
|
|
26
42
|
"{task}"
|
|
27
43
|
|
|
28
|
-
**YOUR
|
|
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:**
|
|
44
|
+
**YOUR SEARCH QUERIES (as a list of strings):**
|
|
67
45
|
"""
|
|
68
46
|
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
47
|
+
APP_SELECTION_PROMPT = """
|
|
48
|
+
You are an AI assistant that selects the most appropriate applications (apps) from a list to accomplish a user's task.
|
|
72
49
|
|
|
73
50
|
**INSTRUCTIONS:**
|
|
74
|
-
1.
|
|
75
|
-
2.
|
|
76
|
-
3.
|
|
77
|
-
4.
|
|
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.
|
|
51
|
+
1. Carefully review the original user task to understand the complete goal.
|
|
52
|
+
2. Examine the list of available apps, their IDs, and their descriptions.
|
|
53
|
+
3. Select ALL app IDs that are necessary to complete the entire task.
|
|
54
|
+
4. If the user's task mentions a specific app, you MUST select it.
|
|
55
|
+
5. If no apps are a good fit, return an empty list.
|
|
102
56
|
|
|
103
57
|
**ORIGINAL USER TASK:**
|
|
104
58
|
"{task}"
|
|
105
59
|
|
|
106
|
-
**
|
|
107
|
-
|
|
60
|
+
**AVAILABLE APPS:**
|
|
61
|
+
{app_candidates}
|
|
108
62
|
|
|
109
|
-
**YOUR
|
|
63
|
+
**YOUR SELECTED APP ID(s) (as a list of strings):**
|
|
110
64
|
"""
|
|
111
65
|
|
|
112
|
-
|
|
113
66
|
TOOL_SELECTION_PROMPT = """
|
|
114
|
-
You are an AI assistant that selects the most appropriate tool(s) from a list to accomplish a
|
|
67
|
+
You are an AI assistant that selects the most appropriate tool(s) from a list to accomplish a user's overall task.
|
|
115
68
|
|
|
116
69
|
**INSTRUCTIONS:**
|
|
117
|
-
1. Carefully review the
|
|
118
|
-
2. Examine the list of available tools and their descriptions.
|
|
119
|
-
3. Select
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
70
|
+
1. Carefully review the original user task to understand the complete goal.
|
|
71
|
+
2. Examine the list of available tools, their IDs, and their descriptions. These tools have been found using a search based on the task.
|
|
72
|
+
3. Select all tool IDs that are necessary to complete the entire task. It is critical to select all tools needed for a multi-step task.
|
|
73
|
+
4. If no tools are a good fit for the task, return an empty list.
|
|
74
|
+
5. Only return the tool IDs. The tools are general purpose, but you are smart enough to see how they can be used for the specific task.
|
|
124
75
|
|
|
125
|
-
**
|
|
126
|
-
"{
|
|
76
|
+
**ORIGINAL USER TASK:**
|
|
77
|
+
"{task}"
|
|
127
78
|
|
|
128
79
|
**AVAILABLE TOOLS:**
|
|
129
80
|
{tool_candidates}
|
|
130
81
|
|
|
131
|
-
**YOUR SELECTED TOOL ID(s):**
|
|
82
|
+
**YOUR SELECTED TOOL ID(s) (as a list of strings):**
|
|
132
83
|
"""
|
|
@@ -1,227 +1,211 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections import defaultdict
|
|
1
3
|
from typing import Annotated, TypedDict
|
|
2
4
|
|
|
3
5
|
from langchain_core.language_models import BaseChatModel
|
|
4
6
|
from langchain_core.messages import AIMessage, AnyMessage
|
|
5
7
|
from langgraph.graph import END, StateGraph
|
|
6
8
|
from langgraph.graph.message import add_messages
|
|
9
|
+
from langgraph.types import Command
|
|
7
10
|
from loguru import logger
|
|
8
11
|
from pydantic import BaseModel, Field
|
|
9
12
|
from universal_mcp.tools.registry import ToolRegistry
|
|
13
|
+
from universal_mcp.types import ToolConfig
|
|
10
14
|
|
|
11
15
|
from universal_mcp.agents.shared.prompts import (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
TASK_DECOMPOSITION_PROMPT,
|
|
15
|
-
TOOL_SEARCH_QUERY_PROMPT,
|
|
16
|
+
APP_SELECTION_PROMPT,
|
|
17
|
+
TOOL_SEARCH_QUERIES_PROMPT,
|
|
16
18
|
TOOL_SELECTION_PROMPT,
|
|
17
19
|
)
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
MAX_RETRIES = 1
|
|
20
22
|
|
|
21
|
-
# --- Pydantic Models for Structured LLM Outputs ---
|
|
22
23
|
|
|
24
|
+
class SearchQueries(BaseModel):
|
|
25
|
+
queries: list[str] = Field(description="A list of search queries for finding tools.")
|
|
23
26
|
|
|
24
|
-
class TaskDecomposition(BaseModel):
|
|
25
|
-
sub_tasks: list[str] = Field(description="A list of sub-task descriptions.")
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
query: str = Field(description="A concise search query.")
|
|
28
|
+
class AppSelection(BaseModel):
|
|
29
|
+
app_ids: list[str] = Field(description="The IDs of the selected applications.")
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class ToolSelection(BaseModel):
|
|
33
33
|
tool_ids: list[str] = Field(description="The IDs of the selected tools.")
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
class AgentState(TypedDict):
|
|
37
|
+
"""The central state of our agent graph."""
|
|
37
38
|
|
|
39
|
+
original_task: str
|
|
40
|
+
queries: list[str]
|
|
41
|
+
candidate_tools: list[dict]
|
|
42
|
+
execution_plan: ToolConfig
|
|
43
|
+
messages: Annotated[list[AnyMessage], add_messages]
|
|
44
|
+
retry_count: int
|
|
38
45
|
|
|
39
|
-
class SubTask(TypedDict, total=False):
|
|
40
|
-
"""Represents a single step in the execution plan."""
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
app_id: str
|
|
45
|
-
tool_ids: list[str]
|
|
46
|
-
reasoning: str
|
|
47
|
+
def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGraph:
|
|
48
|
+
"""Builds a workflow for tool selection with a retry mechanism."""
|
|
47
49
|
|
|
50
|
+
async def _search_for_tools(state: AgentState) -> Command:
|
|
51
|
+
"""
|
|
52
|
+
Performs a hierarchical search:
|
|
53
|
+
1. Generates search queries for the task.
|
|
54
|
+
2. Searches for candidate *applications*.
|
|
55
|
+
3. Uses an LLM to select the most relevant applications.
|
|
56
|
+
4. Searches for tools only within the selected applications.
|
|
57
|
+
If any step fails, it can trigger a retry.
|
|
58
|
+
"""
|
|
59
|
+
task = state["original_task"]
|
|
60
|
+
logger.info(f"Starting hierarchical tool search for task: '{task}'")
|
|
61
|
+
|
|
62
|
+
prompt = TOOL_SEARCH_QUERIES_PROMPT.format(task=task)
|
|
63
|
+
response = await llm.with_structured_output(SearchQueries).ainvoke(prompt)
|
|
64
|
+
queries = response.queries
|
|
65
|
+
logger.info(f"Generated search queries: {queries}")
|
|
66
|
+
|
|
67
|
+
if not queries:
|
|
68
|
+
logger.error("LLM failed to generate any search queries.")
|
|
69
|
+
return Command(
|
|
70
|
+
update={"messages": [AIMessage(content="I could not understand the task to search for tools.")]},
|
|
71
|
+
goto="handle_failure",
|
|
72
|
+
)
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
# Always store queries for potential retry
|
|
75
|
+
update_state = {"queries": queries}
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
sub_tasks: list[SubTask]
|
|
56
|
-
execution_plan: list[SubTask]
|
|
57
|
-
messages: Annotated[list[AnyMessage], add_messages]
|
|
77
|
+
app_search_tasks = [registry.search_apps(query, distance_threshold=0.7) for query in queries]
|
|
78
|
+
app_results = await asyncio.gather(*app_search_tasks)
|
|
79
|
+
unique_apps = {app["id"]: app for app_list in app_results for app in app_list}
|
|
58
80
|
|
|
81
|
+
if not unique_apps:
|
|
82
|
+
logger.warning(f"No applications found for queries: {queries}. Triggering retry.")
|
|
83
|
+
return Command(update=update_state, goto="general_search_and_select")
|
|
59
84
|
|
|
60
|
-
|
|
85
|
+
logger.info(f"Found {len(unique_apps)} candidate applications.")
|
|
61
86
|
|
|
87
|
+
app_candidates_str = "\n - ".join([f"{app['id']}: {app['description']}" for app in unique_apps.values()])
|
|
88
|
+
app_selection_prompt = APP_SELECTION_PROMPT.format(task=task, app_candidates=app_candidates_str)
|
|
89
|
+
app_selection_response = await llm.with_structured_output(AppSelection).ainvoke(app_selection_prompt)
|
|
90
|
+
selected_app_ids = app_selection_response.app_ids
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
if not selected_app_ids:
|
|
93
|
+
logger.warning("LLM did not select any applications from the candidate list. Triggering retry.")
|
|
94
|
+
return Command(update=update_state, goto="general_search_and_select")
|
|
65
95
|
|
|
66
|
-
|
|
67
|
-
"""Decomposes the main task or revises a failed decomposition."""
|
|
68
|
-
attempts = state.get("decomposition_attempts", 0)
|
|
69
|
-
task = state["original_task"]
|
|
70
|
-
failed_info = state.get("failed_sub_task_info")
|
|
96
|
+
logger.success(f"Selected {len(selected_app_ids)} applications: {selected_app_ids}")
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
prompt = TASK_DECOMPOSITION_PROMPT.format(task=task)
|
|
78
|
-
|
|
79
|
-
response = await llm.with_structured_output(TaskDecomposition).ainvoke(prompt)
|
|
80
|
-
sub_tasks = [{"task": sub_task_str, "status": "pending"} for sub_task_str in response.sub_tasks]
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
"sub_tasks": sub_tasks,
|
|
84
|
-
"decomposition_attempts": attempts + 1,
|
|
85
|
-
"messages": [AIMessage(content=f"New plan created with {len(sub_tasks)} steps.")],
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async def _resolve_sub_tasks(state: AgentState) -> AgentState:
|
|
89
|
-
"""Iterates through sub-tasks, providing full plan context to the app selection prompt."""
|
|
90
|
-
sub_tasks = state["sub_tasks"]
|
|
91
|
-
original_task = state["original_task"]
|
|
92
|
-
current_plan = []
|
|
93
|
-
|
|
94
|
-
for i, sub_task in enumerate(sub_tasks):
|
|
95
|
-
task_desc = sub_task["task"]
|
|
96
|
-
logger.info(f"Resolving sub-task: '{task_desc}'")
|
|
97
|
-
|
|
98
|
-
# 1. Build the FULL context string from the entire plan so far
|
|
99
|
-
if not current_plan:
|
|
100
|
-
plan_context_str = "None. This is the first step."
|
|
101
|
-
else:
|
|
102
|
-
context_lines = [
|
|
103
|
-
f"- The sub-task '{step['task']}' was assigned to app '{step['app_id']}'." for step in current_plan
|
|
104
|
-
]
|
|
105
|
-
plan_context_str = "\n".join(context_lines)
|
|
106
|
-
|
|
107
|
-
# 2. Generate the App-specific query using the NEW full-context prompt
|
|
108
|
-
app_query_prompt = APP_SEARCH_QUERY_PROMPT.format(
|
|
109
|
-
original_task=original_task, plan_context=plan_context_str, sub_task=task_desc
|
|
110
|
-
)
|
|
111
|
-
app_query_response = await llm.with_structured_output(SearchQuery).ainvoke(app_query_prompt)
|
|
112
|
-
app_search_query = app_query_response.query
|
|
113
|
-
logger.info(f"Generated context-aware app search query: '{app_search_query}'")
|
|
114
|
-
|
|
115
|
-
# 3. Search for candidate apps (the rest of the logic is the same)
|
|
116
|
-
candidate_apps = await registry.search_apps(query=app_search_query, limit=5)
|
|
117
|
-
if not candidate_apps:
|
|
118
|
-
logger.error(f"No apps found for query '{app_search_query}' from sub-task: '{task_desc}'")
|
|
119
|
-
return {"failed_sub_task_info": task_desc, "sub_tasks": []}
|
|
120
|
-
|
|
121
|
-
# 4. Generate Action-specific query for finding the tool
|
|
122
|
-
tool_query_prompt = TOOL_SEARCH_QUERY_PROMPT.format(sub_task=task_desc)
|
|
123
|
-
tool_query_response = await llm.with_structured_output(SearchQuery).ainvoke(tool_query_prompt)
|
|
124
|
-
tool_search_query = tool_query_response.query
|
|
125
|
-
logger.info(f"Generated tool search query: '{tool_search_query}'")
|
|
126
|
-
|
|
127
|
-
# 5. Find a suitable tool within the candidate apps
|
|
128
|
-
tool_found = False
|
|
129
|
-
for app in candidate_apps:
|
|
130
|
-
app_id = app["id"]
|
|
131
|
-
logger.info(f"Searching for tools in app '{app_id}' with query '{tool_search_query}'...")
|
|
132
|
-
|
|
133
|
-
found_tools = await registry.search_tools(query=tool_search_query, app_id=app_id, limit=5)
|
|
134
|
-
if not found_tools:
|
|
135
|
-
continue
|
|
136
|
-
|
|
137
|
-
tool_candidates_str = "\n - ".join([f"{tool['name']}: {tool['description']}" for tool in found_tools])
|
|
138
|
-
selection_prompt = TOOL_SELECTION_PROMPT.format(sub_task=task_desc, tool_candidates=tool_candidates_str)
|
|
139
|
-
selection_response = await llm.with_structured_output(ToolSelection).ainvoke(selection_prompt)
|
|
140
|
-
|
|
141
|
-
if selection_response.tool_ids:
|
|
142
|
-
logger.success(f"Found and selected tool(s) {selection_response.tool_ids} in app '{app_id}'.")
|
|
143
|
-
sub_task.update(
|
|
144
|
-
{
|
|
145
|
-
"status": "success",
|
|
146
|
-
"app_id": app_id,
|
|
147
|
-
"tool_ids": selection_response.tool_ids,
|
|
148
|
-
"reasoning": f"Selected tool(s) {selection_response.tool_ids} from app '{app_id}' for sub-task.",
|
|
149
|
-
}
|
|
150
|
-
)
|
|
151
|
-
current_plan.append(sub_task)
|
|
152
|
-
tool_found = True
|
|
153
|
-
break
|
|
154
|
-
|
|
155
|
-
if not tool_found:
|
|
156
|
-
logger.error(f"Could not find any suitable tool for sub-task: '{task_desc}'")
|
|
157
|
-
return {"failed_sub_task_info": task_desc, "sub_tasks": []}
|
|
158
|
-
|
|
159
|
-
return {"execution_plan": current_plan, "sub_tasks": []}
|
|
160
|
-
|
|
161
|
-
def _handle_planning_failure(state: AgentState) -> AgentState:
|
|
162
|
-
"""Handles the case where all decomposition attempts have failed."""
|
|
163
|
-
logger.error("Maximum decomposition attempts reached. Planning failed.")
|
|
164
|
-
return {
|
|
165
|
-
"messages": [
|
|
166
|
-
AIMessage(
|
|
167
|
-
content="I am unable to create a complete plan for this task with the available tools. Please try rephrasing your request."
|
|
168
|
-
)
|
|
169
|
-
]
|
|
170
|
-
}
|
|
98
|
+
tool_search_tasks = [
|
|
99
|
+
registry.search_tools(task, app_id=app_id, distance_threshold=0.8) for app_id in selected_app_ids
|
|
100
|
+
]
|
|
101
|
+
tool_results = await asyncio.gather(*tool_search_tasks)
|
|
102
|
+
candidate_tools = [tool for tool_list in tool_results for tool in tool_list]
|
|
171
103
|
|
|
172
|
-
|
|
104
|
+
if not candidate_tools:
|
|
105
|
+
logger.warning(f"No tools found within the selected applications: {selected_app_ids}. Triggering retry.")
|
|
106
|
+
return Command(update=update_state, goto="general_search_and_select")
|
|
107
|
+
|
|
108
|
+
logger.success(f"Found {len(candidate_tools)} candidate tools from selected apps.")
|
|
109
|
+
update_state["candidate_tools"] = candidate_tools
|
|
110
|
+
return Command(update=update_state, goto="select_tools_for_plan")
|
|
111
|
+
|
|
112
|
+
async def _general_search_and_select(state: AgentState) -> Command:
|
|
173
113
|
"""
|
|
174
|
-
|
|
175
|
-
It combines their tool_ids into a single unique list.
|
|
114
|
+
A retry node that performs a general tool search without app filters.
|
|
176
115
|
"""
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
merged_apps[app_id]["tool_ids"].update(step["tool_ids"])
|
|
116
|
+
state["original_task"]
|
|
117
|
+
queries = state["queries"]
|
|
118
|
+
retry_count = state.get("retry_count", 0)
|
|
119
|
+
|
|
120
|
+
if retry_count >= MAX_RETRIES:
|
|
121
|
+
logger.error("Max retries reached. Failing the planning process.")
|
|
122
|
+
return Command(
|
|
123
|
+
update={
|
|
124
|
+
"messages": [AIMessage(content="I could not find any relevant tools after extensive searching.")]
|
|
125
|
+
},
|
|
126
|
+
goto="handle_failure",
|
|
127
|
+
)
|
|
190
128
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
129
|
+
logger.info(f"--- RETRY {retry_count + 1}/{MAX_RETRIES} ---")
|
|
130
|
+
logger.info("Performing a general tool search without app filters.")
|
|
131
|
+
|
|
132
|
+
general_search_tasks = [registry.search_tools(query, distance_threshold=0.85) for query in queries]
|
|
133
|
+
tool_results = await asyncio.gather(*general_search_tasks)
|
|
134
|
+
|
|
135
|
+
unique_tools = {tool["id"]: tool for tool_list in tool_results for tool in tool_list}
|
|
136
|
+
candidate_tools = list(unique_tools.values())
|
|
137
|
+
|
|
138
|
+
if not candidate_tools:
|
|
139
|
+
logger.error("General search (retry) also failed to find any tools.")
|
|
140
|
+
return Command(
|
|
141
|
+
update={
|
|
142
|
+
"messages": [
|
|
143
|
+
AIMessage(content="I could not find any tools for your request, even with a broader search.")
|
|
144
|
+
],
|
|
145
|
+
"retry_count": retry_count + 1,
|
|
146
|
+
},
|
|
147
|
+
goto="handle_failure",
|
|
148
|
+
)
|
|
196
149
|
|
|
197
|
-
|
|
150
|
+
logger.success(f"General search found {len(candidate_tools)} candidate tools.")
|
|
151
|
+
return Command(
|
|
152
|
+
update={"candidate_tools": candidate_tools, "retry_count": retry_count + 1},
|
|
153
|
+
goto="select_tools_for_plan",
|
|
154
|
+
)
|
|
198
155
|
|
|
199
|
-
|
|
156
|
+
async def _select_tools_for_plan(state: AgentState) -> Command:
|
|
157
|
+
"""Selects the best tools from the candidates and builds the final execution plan."""
|
|
158
|
+
task = state["original_task"]
|
|
159
|
+
candidate_tools = state["candidate_tools"]
|
|
160
|
+
retry_count = state.get("retry_count", 0)
|
|
161
|
+
logger.info("Starting tool selection from candidate list.")
|
|
162
|
+
|
|
163
|
+
tool_candidates_str = "\n - ".join([f"{tool['id']}: {tool['description']}" for tool in candidate_tools])
|
|
164
|
+
prompt = TOOL_SELECTION_PROMPT.format(task=task, tool_candidates=tool_candidates_str)
|
|
165
|
+
response = await llm.with_structured_output(ToolSelection).ainvoke(prompt)
|
|
166
|
+
selected_tool_ids = response.tool_ids
|
|
167
|
+
|
|
168
|
+
if not selected_tool_ids:
|
|
169
|
+
if retry_count >= MAX_RETRIES:
|
|
170
|
+
logger.error("LLM did not select any tools, even after a retry. Failing.")
|
|
171
|
+
return Command(
|
|
172
|
+
update={
|
|
173
|
+
"messages": [AIMessage(content="I found potential tools, but could not create a final plan.")]
|
|
174
|
+
},
|
|
175
|
+
goto="handle_failure",
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
logger.warning(
|
|
179
|
+
"LLM did not select any tools from the current candidate list. Triggering general search."
|
|
180
|
+
)
|
|
181
|
+
return Command(goto="general_search_and_select")
|
|
200
182
|
|
|
201
|
-
|
|
183
|
+
logger.success(f"Selected {len(selected_tool_ids)} tools for the final plan: {selected_tool_ids}")
|
|
202
184
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
185
|
+
final_plan = defaultdict(list)
|
|
186
|
+
for tool_id in selected_tool_ids:
|
|
187
|
+
if "__" in tool_id:
|
|
188
|
+
app_id, tool_name = tool_id.split("__", 1)
|
|
189
|
+
final_plan[app_id].append(tool_name)
|
|
207
190
|
|
|
208
|
-
|
|
191
|
+
sorted_final_plan = {app_id: sorted(tools) for app_id, tools in final_plan.items()}
|
|
192
|
+
return Command(update={"execution_plan": sorted_final_plan}, goto=END)
|
|
209
193
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return "handle_planning_failure"
|
|
216
|
-
else:
|
|
217
|
-
return "decompose_task" # Re-try decomposition
|
|
194
|
+
def _handle_planning_failure(state: AgentState) -> Command:
|
|
195
|
+
"""Handles cases where tool search or selection fails by logging the final error message."""
|
|
196
|
+
if messages := state.get("messages"):
|
|
197
|
+
last_message = messages[-1].content
|
|
198
|
+
logger.error(f"Planning failed. Final message: {last_message}")
|
|
218
199
|
else:
|
|
219
|
-
|
|
200
|
+
logger.error("Planning failed with no specific message.")
|
|
201
|
+
return Command(goto=END)
|
|
220
202
|
|
|
221
|
-
workflow
|
|
222
|
-
workflow.
|
|
203
|
+
workflow = StateGraph(AgentState)
|
|
204
|
+
workflow.add_node("search_for_tools", _search_for_tools)
|
|
205
|
+
workflow.add_node("general_search_and_select", _general_search_and_select)
|
|
206
|
+
workflow.add_node("select_tools_for_plan", _select_tools_for_plan)
|
|
207
|
+
workflow.add_node("handle_failure", _handle_planning_failure)
|
|
223
208
|
|
|
224
|
-
workflow.
|
|
225
|
-
workflow.add_edge("handle_planning_failure", END)
|
|
209
|
+
workflow.set_entry_point("search_for_tools")
|
|
226
210
|
|
|
227
211
|
return workflow.compile()
|