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.

Files changed (49) hide show
  1. universal_mcp/agents/__init__.py +1 -1
  2. universal_mcp/agents/base.py +3 -0
  3. universal_mcp/agents/bigtool/__init__.py +1 -1
  4. universal_mcp/agents/bigtool/__main__.py +4 -3
  5. universal_mcp/agents/bigtool/agent.py +3 -2
  6. universal_mcp/agents/bigtool/graph.py +68 -31
  7. universal_mcp/agents/bigtool/prompts.py +2 -2
  8. universal_mcp/agents/bigtool/tools.py +17 -4
  9. universal_mcp/agents/builder/__main__.py +129 -28
  10. universal_mcp/agents/builder/builder.py +149 -161
  11. universal_mcp/agents/builder/helper.py +71 -0
  12. universal_mcp/agents/builder/prompts.py +94 -160
  13. universal_mcp/agents/codeact0/__init__.py +2 -1
  14. universal_mcp/agents/codeact0/agent.py +13 -5
  15. universal_mcp/agents/codeact0/langgraph_agent.py +14 -0
  16. universal_mcp/agents/codeact0/llm_tool.py +1 -2
  17. universal_mcp/agents/codeact0/playbook_agent.py +353 -0
  18. universal_mcp/agents/codeact0/prompts.py +126 -41
  19. universal_mcp/agents/codeact0/sandbox.py +43 -32
  20. universal_mcp/agents/codeact0/state.py +27 -3
  21. universal_mcp/agents/codeact0/tools.py +180 -0
  22. universal_mcp/agents/codeact0/utils.py +89 -75
  23. universal_mcp/agents/shared/__main__.py +44 -0
  24. universal_mcp/agents/shared/prompts.py +49 -98
  25. universal_mcp/agents/shared/tool_node.py +160 -176
  26. universal_mcp/agents/utils.py +71 -0
  27. universal_mcp/applications/ui/app.py +2 -2
  28. {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/METADATA +3 -3
  29. universal_mcp_agents-0.1.15.dist-info/RECORD +50 -0
  30. universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
  31. universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
  32. universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -13
  33. universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
  34. universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
  35. universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
  36. universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
  37. universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
  38. universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
  39. universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
  40. universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
  41. universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
  42. universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
  43. universal_mcp/agents/planner/__init__.py +0 -51
  44. universal_mcp/agents/planner/__main__.py +0 -28
  45. universal_mcp/agents/planner/graph.py +0 -85
  46. universal_mcp/agents/planner/prompts.py +0 -14
  47. universal_mcp/agents/planner/state.py +0 -11
  48. universal_mcp_agents-0.1.13.dist-info/RECORD +0 -63
  49. {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/WHEEL +0 -0
@@ -1,132 +1,83 @@
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.
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. **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.
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 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."
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 perplexity web search."
20
- - **CORRECT DECOMPOSITION:**
21
- - "Perform a web search using Perplexity to find the best restaurants in Goa."
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 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:**
44
+ **YOUR SEARCH QUERIES (as a list of strings):**
67
45
  """
68
46
 
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.
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. 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.
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
- **FAILED SUB-TASK FROM PREVIOUS PLAN:**
107
- "{failed_sub_task}"
60
+ **AVAILABLE APPS:**
61
+ {app_candidates}
108
62
 
109
- **YOUR NEW, REVISED DECOMPOSITION (as a list of strings):**
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 specific sub-task.
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 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
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
- **SUB-TASK:**
126
- "{sub_task}"
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
- APP_SEARCH_QUERY_PROMPT,
13
- REVISE_DECOMPOSITION_PROMPT,
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
- MAX_DECOMPOSITION_ATTEMPTS = 2
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
- class SearchQuery(BaseModel):
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
- # --- LangGraph Agent State ---
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
- task: str
43
- status: str # "pending", "success", "failed"
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
- class AgentState(TypedDict):
50
- """The central state of our agent graph."""
74
+ # Always store queries for potential retry
75
+ update_state = {"queries": queries}
51
76
 
52
- original_task: str
53
- decomposition_attempts: int
54
- failed_sub_task_info: str # To inform re-decomposition
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
- # --- Graph Builder ---
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
- def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGraph:
64
- """Builds the adaptive LangGraph workflow for tool selection."""
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
- async def _decompose_task(state: AgentState) -> AgentState:
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
- if attempts > 0 and failed_info:
73
- logger.warning(f"Revising decomposition. Attempt {attempts + 1}.")
74
- prompt = REVISE_DECOMPOSITION_PROMPT.format(task=task, failed_sub_task=failed_info)
75
- else:
76
- logger.info("Performing initial task decomposition.")
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
- def _consolidate_plan(state: AgentState) -> AgentState:
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
- NEW: Merges steps in the execution plan that use the same app_id.
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
- logger.info("Consolidating final execution plan.")
178
- plan = state["execution_plan"]
179
- merged_apps: dict[str, SubTask] = {}
180
-
181
- for step in plan:
182
- app_id = step["app_id"]
183
- if app_id not in merged_apps:
184
- # Store the first occurrence of this app
185
- merged_apps[app_id] = step.copy()
186
- merged_apps[app_id]["tool_ids"] = set(step["tool_ids"])
187
- else:
188
- # If app already seen, just update its set of tool_ids
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
- # Convert the merged dictionary back to a list of SubTasks
192
- final_plan = []
193
- for app_id, step_data in merged_apps.items():
194
- step_data["tool_ids"] = sorted(list(step_data["tool_ids"]))
195
- final_plan.append(step_data)
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
- return {"execution_plan": final_plan}
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
- # --- Graph Definition ---
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
- workflow = StateGraph(AgentState)
183
+ logger.success(f"Selected {len(selected_tool_ids)} tools for the final plan: {selected_tool_ids}")
202
184
 
203
- workflow.add_node("decompose_task", _decompose_task)
204
- workflow.add_node("resolve_sub_tasks", _resolve_sub_tasks)
205
- workflow.add_node("consolidate_plan", _consolidate_plan) # NEW NODE
206
- workflow.add_node("handle_planning_failure", _handle_planning_failure)
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
- workflow.set_entry_point("decompose_task")
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 should_continue(state: AgentState):
211
- if not state.get("sub_tasks"): # Resolution failed or succeeded
212
- if state.get("execution_plan"):
213
- return "consolidate_plan" # MODIFIED: Go to consolidate on success
214
- elif state["decomposition_attempts"] >= MAX_DECOMPOSITION_ATTEMPTS:
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
- return "resolve_sub_tasks"
200
+ logger.error("Planning failed with no specific message.")
201
+ return Command(goto=END)
220
202
 
221
- workflow.add_conditional_edges("decompose_task", lambda s: "resolve_sub_tasks")
222
- workflow.add_conditional_edges("resolve_sub_tasks", should_continue)
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.add_edge("consolidate_plan", END) # NEW EDGE
225
- workflow.add_edge("handle_planning_failure", END)
209
+ workflow.set_entry_point("search_for_tools")
226
210
 
227
211
  return workflow.compile()