universal-mcp-agents 0.1.12__py3-none-any.whl → 0.1.14__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 +2 -0
- universal_mcp/agents/bigtool/__init__.py +1 -1
- universal_mcp/agents/bigtool/agent.py +2 -2
- universal_mcp/agents/bigtool/graph.py +65 -31
- universal_mcp/agents/bigtool/prompts.py +2 -2
- universal_mcp/agents/bigtool/tools.py +18 -4
- universal_mcp/agents/builder/__main__.py +105 -30
- universal_mcp/agents/builder/builder.py +149 -160
- universal_mcp/agents/builder/helper.py +73 -0
- universal_mcp/agents/builder/prompts.py +33 -152
- universal_mcp/agents/builder/state.py +1 -1
- universal_mcp/agents/cli.py +2 -2
- universal_mcp/agents/codeact/agent.py +1 -1
- universal_mcp/agents/codeact/sandbox.py +1 -5
- universal_mcp/agents/codeact0/agent.py +5 -4
- universal_mcp/agents/codeact0/langgraph_agent.py +17 -0
- universal_mcp/agents/codeact0/llm_tool.py +1 -1
- universal_mcp/agents/codeact0/prompts.py +34 -23
- universal_mcp/agents/codeact0/usecases/11-github.yaml +6 -5
- universal_mcp/agents/codeact0/utils.py +42 -63
- universal_mcp/agents/shared/__main__.py +43 -0
- universal_mcp/agents/shared/prompts.py +50 -99
- universal_mcp/agents/shared/tool_node.py +149 -203
- universal_mcp/agents/utils.py +65 -0
- universal_mcp/applications/ui/app.py +2 -2
- {universal_mcp_agents-0.1.12.dist-info → universal_mcp_agents-0.1.14.dist-info}/METADATA +1 -1
- {universal_mcp_agents-0.1.12.dist-info → universal_mcp_agents-0.1.14.dist-info}/RECORD +29 -28
- universal_mcp/agents/codeact0/langgraph_graph.py +0 -17
- universal_mcp/agents/codeact0/legacy_codeact.py +0 -104
- {universal_mcp_agents-0.1.12.dist-info → universal_mcp_agents-0.1.14.dist-info}/WHEEL +0 -0
|
@@ -1,260 +1,206 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
from typing import Annotated, TypedDict
|
|
3
4
|
|
|
4
5
|
from langchain_core.language_models import BaseChatModel
|
|
5
|
-
from langchain_core.messages import AIMessage, AnyMessage
|
|
6
|
+
from langchain_core.messages import AIMessage, AnyMessage
|
|
6
7
|
from langgraph.graph import END, StateGraph
|
|
7
8
|
from langgraph.graph.message import add_messages
|
|
9
|
+
from langgraph.types import Command
|
|
8
10
|
from loguru import logger
|
|
9
11
|
from pydantic import BaseModel, Field
|
|
10
12
|
from universal_mcp.tools.registry import ToolRegistry
|
|
13
|
+
from universal_mcp.types import ToolConfig
|
|
11
14
|
|
|
12
15
|
from universal_mcp.agents.shared.prompts import (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
TASK_DECOMPOSITION_PROMPT,
|
|
16
|
-
TOOL_SEARCH_QUERY_PROMPT,
|
|
16
|
+
APP_SELECTION_PROMPT,
|
|
17
|
+
TOOL_SEARCH_QUERIES_PROMPT,
|
|
17
18
|
TOOL_SELECTION_PROMPT,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
MAX_RETRIES = 1
|
|
21
22
|
|
|
22
|
-
# --- Pydantic Models for Structured LLM Outputs ---
|
|
23
23
|
|
|
24
|
+
class SearchQueries(BaseModel):
|
|
25
|
+
queries: list[str] = Field(description="A list of search queries for finding tools.")
|
|
24
26
|
|
|
25
|
-
class TaskDecomposition(BaseModel):
|
|
26
|
-
sub_tasks: list[str] = Field(description="A list of sub-task descriptions.")
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
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.")
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
class ToolSelection(BaseModel):
|
|
34
33
|
tool_ids: list[str] = Field(description="The IDs of the selected tools.")
|
|
35
34
|
|
|
36
35
|
|
|
37
|
-
# --- LangGraph Agent State ---
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class SubTask(TypedDict, total=False):
|
|
41
|
-
"""Represents a single step in the execution plan."""
|
|
42
|
-
|
|
43
|
-
task: str
|
|
44
|
-
status: str # "pending", "success", "failed"
|
|
45
|
-
app_id: str
|
|
46
|
-
tool_ids: list[str]
|
|
47
|
-
reasoning: str
|
|
48
|
-
|
|
49
|
-
|
|
50
36
|
class AgentState(TypedDict):
|
|
51
37
|
"""The central state of our agent graph."""
|
|
52
38
|
|
|
53
39
|
original_task: str
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
execution_plan: list[SubTask]
|
|
40
|
+
queries: list[str]
|
|
41
|
+
candidate_tools: list[dict]
|
|
42
|
+
execution_plan: ToolConfig
|
|
58
43
|
messages: Annotated[list[AnyMessage], add_messages]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# --- Graph Builder ---
|
|
44
|
+
retry_count: int
|
|
62
45
|
|
|
63
46
|
|
|
64
47
|
def build_tool_node_graph(llm: BaseChatModel, registry: ToolRegistry) -> StateGraph:
|
|
65
|
-
"""Builds
|
|
48
|
+
"""Builds a workflow for tool selection with a retry mechanism."""
|
|
66
49
|
|
|
67
|
-
async def
|
|
68
|
-
"""
|
|
69
|
-
|
|
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
|
+
"""
|
|
70
59
|
task = state["original_task"]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
"sub_tasks": sub_tasks,
|
|
85
|
-
"decomposition_attempts": attempts + 1,
|
|
86
|
-
"messages": [AIMessage(content=f"New plan created with {len(sub_tasks)} steps.")],
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async def _resolve_sub_tasks(state: AgentState) -> AgentState:
|
|
90
|
-
"""Iterates through sub-tasks, providing full plan context to the app selection prompt."""
|
|
91
|
-
sub_tasks = state["sub_tasks"]
|
|
92
|
-
original_task = state["original_task"]
|
|
93
|
-
current_plan = []
|
|
94
|
-
|
|
95
|
-
for i, sub_task in enumerate(sub_tasks):
|
|
96
|
-
task_desc = sub_task["task"]
|
|
97
|
-
logger.info(f"Resolving sub-task: '{task_desc}'")
|
|
98
|
-
|
|
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
|
|
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",
|
|
111
72
|
)
|
|
112
|
-
app_query_response = await llm.with_structured_output(SearchQuery).ainvoke(app_query_prompt)
|
|
113
|
-
app_search_query = app_query_response.query
|
|
114
|
-
logger.info(f"Generated context-aware app search query: '{app_search_query}'")
|
|
115
|
-
|
|
116
|
-
# 3. Search for candidate apps (the rest of the logic is the same)
|
|
117
|
-
candidate_apps = await registry.search_apps(query=app_search_query, limit=5)
|
|
118
|
-
if not candidate_apps:
|
|
119
|
-
logger.error(f"No apps found for query '{app_search_query}' from sub-task: '{task_desc}'")
|
|
120
|
-
return {"failed_sub_task_info": task_desc, "sub_tasks": []}
|
|
121
|
-
|
|
122
|
-
# 4. Generate Action-specific query for finding the tool
|
|
123
|
-
tool_query_prompt = TOOL_SEARCH_QUERY_PROMPT.format(sub_task=task_desc)
|
|
124
|
-
tool_query_response = await llm.with_structured_output(SearchQuery).ainvoke(tool_query_prompt)
|
|
125
|
-
tool_search_query = tool_query_response.query
|
|
126
|
-
logger.info(f"Generated tool search query: '{tool_search_query}'")
|
|
127
|
-
|
|
128
|
-
# 5. Find a suitable tool within the candidate apps
|
|
129
|
-
tool_found = False
|
|
130
|
-
for app in candidate_apps:
|
|
131
|
-
app_id = app["id"]
|
|
132
|
-
logger.info(f"Searching for tools in app '{app_id}' with query '{tool_search_query}'...")
|
|
133
|
-
|
|
134
|
-
found_tools = await registry.search_tools(query=tool_search_query, app_id=app_id, limit=5)
|
|
135
|
-
if not found_tools:
|
|
136
|
-
continue
|
|
137
|
-
|
|
138
|
-
tool_candidates_str = "\n - ".join([f"{tool['name']}: {tool['description']}" for tool in found_tools])
|
|
139
|
-
selection_prompt = TOOL_SELECTION_PROMPT.format(sub_task=task_desc, tool_candidates=tool_candidates_str)
|
|
140
|
-
selection_response = await llm.with_structured_output(ToolSelection).ainvoke(selection_prompt)
|
|
141
|
-
|
|
142
|
-
if selection_response.tool_ids:
|
|
143
|
-
logger.success(f"Found and selected tool(s) {selection_response.tool_ids} in app '{app_id}'.")
|
|
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
|
-
)
|
|
152
|
-
current_plan.append(sub_task)
|
|
153
|
-
tool_found = True
|
|
154
|
-
break
|
|
155
|
-
|
|
156
|
-
if not tool_found:
|
|
157
|
-
logger.error(f"Could not find any suitable tool for sub-task: '{task_desc}'")
|
|
158
|
-
return {"failed_sub_task_info": task_desc, "sub_tasks": []}
|
|
159
|
-
|
|
160
|
-
return {"execution_plan": current_plan, "sub_tasks": []}
|
|
161
|
-
|
|
162
|
-
def _handle_planning_failure(state: AgentState) -> AgentState:
|
|
163
|
-
"""Handles the case where all decomposition attempts have failed."""
|
|
164
|
-
logger.error("Maximum decomposition attempts reached. Planning failed.")
|
|
165
|
-
return {
|
|
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
|
-
]
|
|
171
|
-
}
|
|
172
73
|
|
|
173
|
-
|
|
174
|
-
""
|
|
175
|
-
NEW: Merges steps in the execution plan that use the same app_id.
|
|
176
|
-
It combines their tool_ids into a single unique list.
|
|
177
|
-
"""
|
|
178
|
-
logger.info("Consolidating final execution plan.")
|
|
179
|
-
plan = state["execution_plan"]
|
|
180
|
-
merged_apps: dict[str, SubTask] = {}
|
|
181
|
-
|
|
182
|
-
for step in plan:
|
|
183
|
-
app_id = step["app_id"]
|
|
184
|
-
if app_id not in merged_apps:
|
|
185
|
-
# Store the first occurrence of this app
|
|
186
|
-
merged_apps[app_id] = step.copy()
|
|
187
|
-
merged_apps[app_id]["tool_ids"] = set(step["tool_ids"])
|
|
188
|
-
else:
|
|
189
|
-
# If app already seen, just update its set of tool_ids
|
|
190
|
-
merged_apps[app_id]["tool_ids"].update(step["tool_ids"])
|
|
74
|
+
# Always store queries for potential retry
|
|
75
|
+
update_state = {"queries": queries}
|
|
191
76
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for
|
|
195
|
-
step_data["tool_ids"] = sorted(list(step_data["tool_ids"]))
|
|
196
|
-
final_plan.append(step_data)
|
|
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}
|
|
197
80
|
|
|
198
|
-
|
|
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")
|
|
199
84
|
|
|
200
|
-
|
|
85
|
+
logger.info(f"Found {len(unique_apps)} candidate applications.")
|
|
201
86
|
|
|
202
|
-
|
|
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
|
|
203
91
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
workflow.add_node("handle_planning_failure", _handle_planning_failure)
|
|
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")
|
|
208
95
|
|
|
209
|
-
|
|
96
|
+
logger.success(f"Selected {len(selected_app_ids)} applications: {selected_app_ids}")
|
|
210
97
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return "handle_planning_failure"
|
|
217
|
-
else:
|
|
218
|
-
return "decompose_task" # Re-try decomposition
|
|
219
|
-
else:
|
|
220
|
-
return "resolve_sub_tasks"
|
|
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]
|
|
221
103
|
|
|
222
|
-
|
|
223
|
-
|
|
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")
|
|
224
107
|
|
|
225
|
-
|
|
226
|
-
|
|
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")
|
|
227
111
|
|
|
228
|
-
|
|
112
|
+
async def _general_search_and_select(state: AgentState) -> Command:
|
|
113
|
+
"""
|
|
114
|
+
A retry node that performs a general tool search without app filters.
|
|
115
|
+
"""
|
|
116
|
+
task = 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={"messages": [AIMessage(content="I could not find any relevant tools after extensive searching.")]},
|
|
124
|
+
goto="handle_failure",
|
|
125
|
+
)
|
|
229
126
|
|
|
127
|
+
logger.info(f"--- RETRY {retry_count + 1}/{MAX_RETRIES} ---")
|
|
128
|
+
logger.info("Performing a general tool search without app filters.")
|
|
129
|
+
|
|
130
|
+
general_search_tasks = [
|
|
131
|
+
registry.search_tools(query, distance_threshold=0.85) for query in queries
|
|
132
|
+
]
|
|
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": [AIMessage(content="I could not find any tools for your request, even with a broader search.")],
|
|
143
|
+
"retry_count": retry_count + 1,
|
|
144
|
+
},
|
|
145
|
+
goto="handle_failure",
|
|
146
|
+
)
|
|
230
147
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
148
|
+
logger.success(f"General search found {len(candidate_tools)} candidate tools.")
|
|
149
|
+
return Command(
|
|
150
|
+
update={"candidate_tools": candidate_tools, "retry_count": retry_count + 1},
|
|
151
|
+
goto="select_tools_for_plan",
|
|
152
|
+
)
|
|
234
153
|
|
|
235
|
-
|
|
154
|
+
async def _select_tools_for_plan(state: AgentState) -> Command:
|
|
155
|
+
"""Selects the best tools from the candidates and builds the final execution plan."""
|
|
156
|
+
task = state["original_task"]
|
|
157
|
+
candidate_tools = state["candidate_tools"]
|
|
158
|
+
retry_count = state.get("retry_count", 0)
|
|
159
|
+
logger.info("Starting tool selection from candidate list.")
|
|
160
|
+
|
|
161
|
+
tool_candidates_str = "\n - ".join([f"{tool['id']}: {tool['description']}" for tool in candidate_tools])
|
|
162
|
+
prompt = TOOL_SELECTION_PROMPT.format(task=task, tool_candidates=tool_candidates_str)
|
|
163
|
+
response = await llm.with_structured_output(ToolSelection).ainvoke(prompt)
|
|
164
|
+
selected_tool_ids = response.tool_ids
|
|
165
|
+
|
|
166
|
+
if not selected_tool_ids:
|
|
167
|
+
if retry_count >= MAX_RETRIES:
|
|
168
|
+
logger.error("LLM did not select any tools, even after a retry. Failing.")
|
|
169
|
+
return Command(
|
|
170
|
+
update={"messages": [AIMessage(content="I found potential tools, but could not create a final plan.")]},
|
|
171
|
+
goto="handle_failure",
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
logger.warning("LLM did not select any tools from the current candidate list. Triggering general search.")
|
|
175
|
+
return Command(goto="general_search_and_select")
|
|
236
176
|
|
|
237
|
-
registry = AgentrRegistry()
|
|
238
|
-
llm = load_chat_model("anthropic/claude-4-sonnet-20250514")
|
|
239
177
|
|
|
240
|
-
|
|
178
|
+
logger.success(f"Selected {len(selected_tool_ids)} tools for the final plan: {selected_tool_ids}")
|
|
241
179
|
|
|
242
|
-
|
|
180
|
+
final_plan = defaultdict(list)
|
|
181
|
+
for tool_id in selected_tool_ids:
|
|
182
|
+
if "__" in tool_id:
|
|
183
|
+
app_id, tool_name = tool_id.split("__", 1)
|
|
184
|
+
final_plan[app_id].append(tool_name)
|
|
243
185
|
|
|
244
|
-
|
|
245
|
-
"
|
|
246
|
-
"messages": [HumanMessage(content=task)],
|
|
247
|
-
"decomposition_attempts": 0,
|
|
248
|
-
}
|
|
186
|
+
sorted_final_plan = {app_id: sorted(tools) for app_id, tools in final_plan.items()}
|
|
187
|
+
return Command(update={"execution_plan": sorted_final_plan}, goto=END)
|
|
249
188
|
|
|
250
|
-
|
|
189
|
+
def _handle_planning_failure(state: AgentState) -> Command:
|
|
190
|
+
"""Handles cases where tool search or selection fails by logging the final error message."""
|
|
191
|
+
if messages := state.get("messages"):
|
|
192
|
+
last_message = messages[-1].content
|
|
193
|
+
logger.error(f"Planning failed. Final message: {last_message}")
|
|
194
|
+
else:
|
|
195
|
+
logger.error("Planning failed with no specific message.")
|
|
196
|
+
return Command(goto=END)
|
|
251
197
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
198
|
+
workflow = StateGraph(AgentState)
|
|
199
|
+
workflow.add_node("search_for_tools", _search_for_tools)
|
|
200
|
+
workflow.add_node("general_search_and_select", _general_search_and_select)
|
|
201
|
+
workflow.add_node("select_tools_for_plan", _select_tools_for_plan)
|
|
202
|
+
workflow.add_node("handle_failure", _handle_planning_failure)
|
|
257
203
|
|
|
204
|
+
workflow.set_entry_point("search_for_tools")
|
|
258
205
|
|
|
259
|
-
|
|
260
|
-
asyncio.run(main())
|
|
206
|
+
return workflow.compile()
|
universal_mcp/agents/utils.py
CHANGED
|
@@ -9,6 +9,8 @@ from rich.markdown import Markdown
|
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
from rich.prompt import Prompt
|
|
11
11
|
from rich.table import Table
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
from requests import JSONDecodeError
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class RichCLI:
|
|
@@ -143,3 +145,66 @@ def get_message_text(message: BaseMessage):
|
|
|
143
145
|
logger.error(f"Error getting message text: {e}")
|
|
144
146
|
logger.error(f"Message: {message}")
|
|
145
147
|
raise e
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def filter_retry_on(exc: Exception) -> bool:
|
|
151
|
+
import httpx
|
|
152
|
+
import requests
|
|
153
|
+
|
|
154
|
+
# Transient local/network issues and parsing hiccups
|
|
155
|
+
if isinstance(
|
|
156
|
+
exc,
|
|
157
|
+
(
|
|
158
|
+
TimeoutError,
|
|
159
|
+
ConnectionError,
|
|
160
|
+
JSONDecodeError,
|
|
161
|
+
ValidationError,
|
|
162
|
+
),
|
|
163
|
+
):
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
# httpx transient request-layer errors
|
|
167
|
+
if isinstance(
|
|
168
|
+
exc,
|
|
169
|
+
(
|
|
170
|
+
httpx.TimeoutException,
|
|
171
|
+
httpx.ConnectError,
|
|
172
|
+
httpx.ReadError,
|
|
173
|
+
),
|
|
174
|
+
):
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
if isinstance(exc, (requests.Timeout, requests.ConnectionError)):
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
# HTTP status based retries: 408 (timeout), 429 (rate limit), and 5xx
|
|
181
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
182
|
+
status = exc.response.status_code
|
|
183
|
+
return status == 408 or status == 429 or 500 <= status < 600
|
|
184
|
+
if isinstance(exc, requests.HTTPError):
|
|
185
|
+
if exc.response is None:
|
|
186
|
+
return True
|
|
187
|
+
status = exc.response.status_code
|
|
188
|
+
return status == 408 or status == 429 or 500 <= status < 600
|
|
189
|
+
|
|
190
|
+
if isinstance(
|
|
191
|
+
exc,
|
|
192
|
+
(
|
|
193
|
+
ValueError,
|
|
194
|
+
TypeError,
|
|
195
|
+
ArithmeticError,
|
|
196
|
+
ImportError,
|
|
197
|
+
LookupError,
|
|
198
|
+
NameError,
|
|
199
|
+
SyntaxError,
|
|
200
|
+
RuntimeError,
|
|
201
|
+
ReferenceError,
|
|
202
|
+
StopIteration,
|
|
203
|
+
StopAsyncIteration,
|
|
204
|
+
OSError,
|
|
205
|
+
),
|
|
206
|
+
):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# Default: do not retry unknown exceptions
|
|
210
|
+
return False
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Any, Literal, TypedDict
|
|
3
3
|
|
|
4
|
-
from dotenv import load_dotenv
|
|
5
4
|
import httpx
|
|
6
|
-
from
|
|
5
|
+
from dotenv import load_dotenv
|
|
7
6
|
from markitdown import MarkItDown
|
|
7
|
+
from universal_mcp.applications.application import BaseApplication
|
|
8
8
|
|
|
9
9
|
load_dotenv()
|
|
10
10
|
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
universal_mcp/agents/__init__.py,sha256=
|
|
2
|
-
universal_mcp/agents/base.py,sha256=
|
|
3
|
-
universal_mcp/agents/cli.py,sha256=
|
|
1
|
+
universal_mcp/agents/__init__.py,sha256=kM4mC6Pf6lmaaZF1volo7VtKgA8FDyzb1sNenpB7Ulk,1244
|
|
2
|
+
universal_mcp/agents/base.py,sha256=xQ_zMHTvULaiE4LiE7IfhrLmlBtvJ5zVE9Rq2nJ4Hm4,6976
|
|
3
|
+
universal_mcp/agents/cli.py,sha256=AG9e4iSX3GazT537573YrYT1wSaZYOr42rrYQ7xP3YA,1016
|
|
4
4
|
universal_mcp/agents/hil.py,sha256=_5PCK6q0goGm8qylJq44aSp2MadP-yCPvhOJYKqWLMo,3808
|
|
5
5
|
universal_mcp/agents/llm.py,sha256=hVRwjZs3MHl5_3BWedmurs2Jt1oZDfFX0Zj9F8KH7fk,1787
|
|
6
6
|
universal_mcp/agents/react.py,sha256=8XQvJ0HLVgc-K0qn9Ml48WGcgUGuIKtL67HatlT6Da0,3334
|
|
7
7
|
universal_mcp/agents/simple.py,sha256=NSATg5TWzsRNS7V3LFiDG28WSOCIwCdcC1g7NRwg2nM,2095
|
|
8
|
-
universal_mcp/agents/utils.py,sha256=
|
|
9
|
-
universal_mcp/agents/bigtool/__init__.py,sha256=
|
|
8
|
+
universal_mcp/agents/utils.py,sha256=pZFAqH__csH-gRLlTOnuU8kNix3_t5eC3yIk926FWpQ,6898
|
|
9
|
+
universal_mcp/agents/bigtool/__init__.py,sha256=mZG8dsaCVyKlm82otxtiTA225GIFLUCUUYPEIPF24uw,2299
|
|
10
10
|
universal_mcp/agents/bigtool/__main__.py,sha256=a15OUoqPR938x7eseWtxu0aLX7lRS3nu8k5Ks3giUY4,472
|
|
11
|
-
universal_mcp/agents/bigtool/agent.py,sha256=
|
|
11
|
+
universal_mcp/agents/bigtool/agent.py,sha256=FsWa7S00RHmzrLd2ms_aR4wJ09h_KFJ8DDzFnADKJOQ,251
|
|
12
12
|
universal_mcp/agents/bigtool/context.py,sha256=ny7gd-vvVpUOYAeQbAEUT0A6Vm6Nn2qGywxTzPBzYFg,929
|
|
13
|
-
universal_mcp/agents/bigtool/graph.py,sha256=
|
|
14
|
-
universal_mcp/agents/bigtool/prompts.py,sha256=
|
|
13
|
+
universal_mcp/agents/bigtool/graph.py,sha256=SE1IavwrSciIWsb_miekEh3H46UJWpeIOkcp7CsRG_w,5764
|
|
14
|
+
universal_mcp/agents/bigtool/prompts.py,sha256=Joi5mCzZX63aM_6eBrMOKuNRHjTkceVIibSsGBGqhYE,2041
|
|
15
15
|
universal_mcp/agents/bigtool/state.py,sha256=TQeGZD99okclkoCh5oz-VYIlEsC9yLQyDpnBnm7QCN8,759
|
|
16
|
-
universal_mcp/agents/bigtool/tools.py,sha256=
|
|
17
|
-
universal_mcp/agents/builder/__main__.py,sha256=
|
|
18
|
-
universal_mcp/agents/builder/builder.py,sha256=
|
|
19
|
-
universal_mcp/agents/builder/
|
|
20
|
-
universal_mcp/agents/builder/
|
|
16
|
+
universal_mcp/agents/bigtool/tools.py,sha256=vlydQQnITS7OPaSzyILf3czrgO3_fEB-i7zItKcEWjU,6377
|
|
17
|
+
universal_mcp/agents/builder/__main__.py,sha256=mGw5xHK53qNGgs6q7s2PPPPWozWPDKXD2AZGQTqCDQc,10750
|
|
18
|
+
universal_mcp/agents/builder/builder.py,sha256=uhUOH6S7F2iWDhpNOpfzVUT1VB0MQnmcEkLPYJs65gY,8151
|
|
19
|
+
universal_mcp/agents/builder/helper.py,sha256=G6IoXB433zAwfOGhDXOIGsf_EC9bbOSGN8WPWGSzmrk,2700
|
|
20
|
+
universal_mcp/agents/builder/prompts.py,sha256=k0IfhWZ10zou3I2e3kczxf2zpY2AsPshjeY_h5oQ1KA,2673
|
|
21
|
+
universal_mcp/agents/builder/state.py,sha256=nJrH-ChSpqv-fsXhvP4UkouVdQw0ji376uFM0aJ91D0,921
|
|
21
22
|
universal_mcp/agents/codeact/__init__.py,sha256=rLE8gvOo5H4YSr71DRq76b3RV3uuotxuAy_VnBVaVwk,60
|
|
22
23
|
universal_mcp/agents/codeact/__main__.py,sha256=W2cHXRwH1dZG3ETIkMwUqA_d62K3IctHP-FDZWDjxdw,1067
|
|
23
|
-
universal_mcp/agents/codeact/agent.py,sha256=
|
|
24
|
+
universal_mcp/agents/codeact/agent.py,sha256=sKZWokTHcuL68Y6SNyaaHe6_XkWxaIq36TrNmPJfQto,9762
|
|
24
25
|
universal_mcp/agents/codeact/models.py,sha256=2fdAcF5bxWDpljjEwDEdPBflTMShSPwwncHrphRjsYg,222
|
|
25
26
|
universal_mcp/agents/codeact/prompts.py,sha256=EMI-imnd0Ps0Bd2FOvSqgiicvvtFFu0MF9s93PiC_3k,4493
|
|
26
|
-
universal_mcp/agents/codeact/sandbox.py,sha256=
|
|
27
|
+
universal_mcp/agents/codeact/sandbox.py,sha256=NjN6ISj8psFtHf8V0w24ChJdUMUWkq7OrlbHdzm4wBc,2299
|
|
27
28
|
universal_mcp/agents/codeact/state.py,sha256=WTPfpxDlGRnlr5tZuXMg_KU7GS7TZbnrIKslOvZLbQI,565
|
|
28
29
|
universal_mcp/agents/codeact/utils.py,sha256=JUbT_HYGS_D1BzmzoVpORIe7SGur1KgJguTZ_1tZ4JY,1918
|
|
29
30
|
universal_mcp/agents/codeact0/__init__.py,sha256=rLE8gvOo5H4YSr71DRq76b3RV3uuotxuAy_VnBVaVwk,60
|
|
30
31
|
universal_mcp/agents/codeact0/__main__.py,sha256=V2wLWW9ym3rtiSvPEs-N0Mki7G5dYHzV5dAsAoF-ygQ,1148
|
|
31
|
-
universal_mcp/agents/codeact0/agent.py,sha256=
|
|
32
|
+
universal_mcp/agents/codeact0/agent.py,sha256=WmCHhhGN1q2Q1s14FN2kaNBEIVlYRoMpWJOqQEYjaBo,6538
|
|
32
33
|
universal_mcp/agents/codeact0/config.py,sha256=H-1woj_nhSDwf15F63WYn723y4qlRefXzGxuH81uYF0,2215
|
|
33
|
-
universal_mcp/agents/codeact0/
|
|
34
|
-
universal_mcp/agents/codeact0/
|
|
35
|
-
universal_mcp/agents/codeact0/
|
|
36
|
-
universal_mcp/agents/codeact0/prompts.py,sha256=GTUwnxyyUVdeAoAwo4ODGOlEtZStMkoP_lJEFRbDglY,7324
|
|
34
|
+
universal_mcp/agents/codeact0/langgraph_agent.py,sha256=8-Y3-OOEpY_XLcnjvFX-KyAQA5SzBjya3YBrrV9Qh_8,652
|
|
35
|
+
universal_mcp/agents/codeact0/llm_tool.py,sha256=9xgRylNZo3lya33L3iqS7yipJYaHCeUEEBCfjUp71vc,13821
|
|
36
|
+
universal_mcp/agents/codeact0/prompts.py,sha256=VT3v61JkAvZwjQoxwKR3QsYdkbyd_1E8bIm3oABNfMg,7512
|
|
37
37
|
universal_mcp/agents/codeact0/sandbox.py,sha256=tkrhQoV7sAIT5rtd5kpNYEgDz4y82WSHKpMGE6bqthE,3108
|
|
38
38
|
universal_mcp/agents/codeact0/state.py,sha256=kcoCoz-kd0Ukw1YNGDHNggixCK34KzMl2t6GztakSo4,412
|
|
39
|
-
universal_mcp/agents/codeact0/utils.py,sha256=
|
|
39
|
+
universal_mcp/agents/codeact0/utils.py,sha256=6yeUVsB2CkcrutHjfpMI3KsMJkVpFj3Piag6A2IG0Jc,14733
|
|
40
40
|
universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml,sha256=DiChHW-mNOcaaiec7f_04_A0Xyf9a2ihzXiPA9-Fw1I,239
|
|
41
41
|
universal_mcp/agents/codeact0/usecases/10-reddit2.yaml,sha256=R3vrZZNv_E-m_SuSJ5tSv11HqMomm8Gtxp_LDhkrnAw,618
|
|
42
|
-
universal_mcp/agents/codeact0/usecases/11-github.yaml,sha256=
|
|
42
|
+
universal_mcp/agents/codeact0/usecases/11-github.yaml,sha256=iu4-vlvopPct9vfAseElLXPUAWN5sMVri4oMbDYlA4s,672
|
|
43
43
|
universal_mcp/agents/codeact0/usecases/2-reddit.yaml,sha256=-dstvFMjsuP9UQM3B_G1HBn7ImNIMxSCNbITpfwgwAY,2525
|
|
44
44
|
universal_mcp/agents/codeact0/usecases/2.1-instructions.md,sha256=0gQBY_A3jT_lgKNiu6GdyEYupbX0Xz2unlCoTQwv-Es,5098
|
|
45
45
|
universal_mcp/agents/codeact0/usecases/2.2-instructions.md,sha256=Cx-VkcC55MrgFxlMBMBCD83jEge_yZgBWKwtuK1OPFc,4458
|
|
@@ -55,11 +55,12 @@ universal_mcp/agents/planner/__main__.py,sha256=OfhTfYDZK_ZUfc8sX-Sa6TWk-dNqD2rl
|
|
|
55
55
|
universal_mcp/agents/planner/graph.py,sha256=70hhIoEZOcYojpiyVSCedgYpnmxVP7aqdn8s6VBu-D4,3228
|
|
56
56
|
universal_mcp/agents/planner/prompts.py,sha256=_JoHqiAvswtqCDu90AGUHmfsu8eWE1-_yI4LLn3pqMU,657
|
|
57
57
|
universal_mcp/agents/planner/state.py,sha256=qqyp-jSGsCxe1US-PRLT4-y1sITAcVE6nCMlQLnvop0,278
|
|
58
|
-
universal_mcp/agents/shared/
|
|
59
|
-
universal_mcp/agents/shared/
|
|
58
|
+
universal_mcp/agents/shared/__main__.py,sha256=u6ezelmT7poaMRr5AfKp3uXGOV7zEwj017j_iAeUcZs,1450
|
|
59
|
+
universal_mcp/agents/shared/prompts.py,sha256=-gNyfhrJFtPEG4YeoJRYKmkmRHsI8a5f5ssxk_AWrOI,3760
|
|
60
|
+
universal_mcp/agents/shared/tool_node.py,sha256=syydbbKFMpu9Gg45yGP-kpN5KvD2oSRXWtqo1YpD6gY,9038
|
|
60
61
|
universal_mcp/applications/llm/__init__.py,sha256=xnpxq4Wl_pevvwtSUtEwcty8_d61ywO1V2YnEXyCREY,46
|
|
61
62
|
universal_mcp/applications/llm/app.py,sha256=iNLU6z2LRZc01GfSKvV0vNzT1LhKAjq_UrSJEmjthjw,6032
|
|
62
|
-
universal_mcp/applications/ui/app.py,sha256=
|
|
63
|
-
universal_mcp_agents-0.1.
|
|
64
|
-
universal_mcp_agents-0.1.
|
|
65
|
-
universal_mcp_agents-0.1.
|
|
63
|
+
universal_mcp/applications/ui/app.py,sha256=c7OkZsO2fRtndgAzAQbKu-1xXRuRp9Kjgml57YD2NR4,9459
|
|
64
|
+
universal_mcp_agents-0.1.14.dist-info/METADATA,sha256=6a7VjzkIfQdp6b4UfQVAMKFzg2qE8wszgkocVRgqNok,878
|
|
65
|
+
universal_mcp_agents-0.1.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
66
|
+
universal_mcp_agents-0.1.14.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from langgraph.checkpoint.memory import MemorySaver
|
|
2
|
-
from universal_mcp.agentr.registry import AgentrRegistry
|
|
3
|
-
|
|
4
|
-
from universal_mcp.agents.codeact0.agent import CodeActAgent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
async def agent():
|
|
8
|
-
memory = MemorySaver()
|
|
9
|
-
agent_object = await CodeActAgent(
|
|
10
|
-
name="CodeAct Agent",
|
|
11
|
-
instructions="Be very concise in your answers.",
|
|
12
|
-
model="anthropic:claude-4-sonnet-20250514",
|
|
13
|
-
tools={"google_mail": ["list_messages"]},
|
|
14
|
-
registry=AgentrRegistry(),
|
|
15
|
-
memory=memory,
|
|
16
|
-
)._build_graph()
|
|
17
|
-
return agent_object
|