universal-mcp-agents 0.1.3__py3-none-any.whl → 0.1.4__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/autoagent/graph.py +30 -11
- universal_mcp/agents/autoagent/studio.py +1 -7
- universal_mcp/agents/base.py +55 -9
- universal_mcp/agents/bigtool/__init__.py +3 -1
- universal_mcp/agents/bigtool/graph.py +78 -25
- universal_mcp/agents/bigtool2/__init__.py +3 -1
- universal_mcp/agents/bigtool2/agent.py +2 -1
- universal_mcp/agents/bigtool2/context.py +0 -1
- universal_mcp/agents/bigtool2/graph.py +76 -32
- universal_mcp/agents/bigtoolcache/__init__.py +6 -2
- universal_mcp/agents/bigtoolcache/agent.py +2 -1
- universal_mcp/agents/bigtoolcache/context.py +0 -1
- universal_mcp/agents/bigtoolcache/graph.py +88 -59
- universal_mcp/agents/bigtoolcache/prompts.py +29 -0
- universal_mcp/agents/bigtoolcache/tools_all.txt +956 -0
- universal_mcp/agents/bigtoolcache/tools_important.txt +474 -0
- universal_mcp/agents/builder.py +19 -5
- universal_mcp/agents/codeact/__init__.py +16 -4
- universal_mcp/agents/hil.py +16 -4
- universal_mcp/agents/llm.py +5 -1
- universal_mcp/agents/planner/__init__.py +7 -3
- universal_mcp/agents/planner/__main__.py +3 -1
- universal_mcp/agents/planner/graph.py +3 -1
- universal_mcp/agents/react.py +5 -1
- universal_mcp/agents/shared/tool_node.py +24 -8
- universal_mcp/agents/simple.py +8 -1
- universal_mcp/agents/tools.py +9 -3
- universal_mcp/agents/utils.py +35 -7
- {universal_mcp_agents-0.1.3.dist-info → universal_mcp_agents-0.1.4.dist-info}/METADATA +2 -2
- universal_mcp_agents-0.1.4.dist-info/RECORD +53 -0
- universal_mcp_agents-0.1.3.dist-info/RECORD +0 -51
- {universal_mcp_agents-0.1.3.dist-info → universal_mcp_agents-0.1.4.dist-info}/WHEEL +0 -0
|
@@ -22,7 +22,9 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
22
22
|
tools_list = []
|
|
23
23
|
if app_ids is not None:
|
|
24
24
|
for app_id in app_ids:
|
|
25
|
-
tools_list.extend(
|
|
25
|
+
tools_list.extend(
|
|
26
|
+
await tool_registry.search_tools(query, limit=10, app_id=app_id)
|
|
27
|
+
)
|
|
26
28
|
else:
|
|
27
29
|
tools_list = await tool_registry.search_tools(query, limit=10)
|
|
28
30
|
tools_list = [f"{tool['id']}: {tool['description']}" for tool in tools_list]
|
|
@@ -48,21 +50,33 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
48
50
|
connections = await tool_registry.list_connected_apps()
|
|
49
51
|
connection_ids = set([connection["app_id"] for connection in connections])
|
|
50
52
|
connected_apps = [app["id"] for app in app_ids if app["id"] in connection_ids]
|
|
51
|
-
unconnected_apps = [
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
unconnected_apps = [
|
|
54
|
+
app["id"] for app in app_ids if app["id"] not in connection_ids
|
|
55
|
+
]
|
|
56
|
+
app_id_descriptions = (
|
|
57
|
+
"These are the apps connected to the user's account:\n"
|
|
58
|
+
+ "\n".join([f"{app}" for app in connected_apps])
|
|
54
59
|
)
|
|
55
60
|
if unconnected_apps:
|
|
56
61
|
app_id_descriptions += "\n\nOther (not connected) apps: " + "\n".join(
|
|
57
62
|
[f"{app}" for app in unconnected_apps]
|
|
58
63
|
)
|
|
59
64
|
|
|
60
|
-
system_prompt = system_prompt.format(
|
|
65
|
+
system_prompt = system_prompt.format(
|
|
66
|
+
system_time=datetime.now(tz=UTC).isoformat(), app_ids=app_id_descriptions
|
|
67
|
+
)
|
|
61
68
|
|
|
62
|
-
messages = [
|
|
69
|
+
messages = [
|
|
70
|
+
{"role": "system", "content": system_prompt + "\n" + instructions},
|
|
71
|
+
*state["messages"],
|
|
72
|
+
]
|
|
63
73
|
model = load_chat_model(runtime.context.model)
|
|
64
|
-
loaded_tools = await tool_registry.export_tools(
|
|
65
|
-
|
|
74
|
+
loaded_tools = await tool_registry.export_tools(
|
|
75
|
+
tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN
|
|
76
|
+
)
|
|
77
|
+
model_with_tools = model.bind_tools(
|
|
78
|
+
[search_tools, ask_user, load_tools, *loaded_tools], tool_choice="auto"
|
|
79
|
+
)
|
|
66
80
|
response_raw = model_with_tools.invoke(messages)
|
|
67
81
|
response = cast(AIMessage, response_raw)
|
|
68
82
|
return {"messages": [response]}
|
|
@@ -102,7 +116,8 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
102
116
|
tools = await search_tools.ainvoke(tool_call["args"])
|
|
103
117
|
outputs.append(
|
|
104
118
|
ToolMessage(
|
|
105
|
-
content=json.dumps(tools)
|
|
119
|
+
content=json.dumps(tools)
|
|
120
|
+
+ "\n\nUse the load_tools tool to load the tools you want to use.",
|
|
106
121
|
name=tool_call["name"],
|
|
107
122
|
tool_call_id=tool_call["id"],
|
|
108
123
|
)
|
|
@@ -119,9 +134,13 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
119
134
|
)
|
|
120
135
|
)
|
|
121
136
|
else:
|
|
122
|
-
await tool_registry.export_tools(
|
|
137
|
+
await tool_registry.export_tools(
|
|
138
|
+
[tool_call["name"]], ToolFormat.LANGCHAIN
|
|
139
|
+
)
|
|
123
140
|
try:
|
|
124
|
-
tool_result = await tool_registry.call_tool(
|
|
141
|
+
tool_result = await tool_registry.call_tool(
|
|
142
|
+
tool_call["name"], tool_call["args"]
|
|
143
|
+
)
|
|
125
144
|
outputs.append(
|
|
126
145
|
ToolMessage(
|
|
127
146
|
content=json.dumps(tool_result),
|
|
@@ -8,7 +8,6 @@ tool_registry = AgentrRegistry()
|
|
|
8
8
|
tool_manager = ToolManager()
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
async def main():
|
|
13
12
|
instructions = """
|
|
14
13
|
You are a helpful assistant that can use tools to help the user. If a task requires multiple steps, you should perform separate different searches for different actions. Prefer completing one action before searching for another.
|
|
@@ -16,10 +15,5 @@ async def main():
|
|
|
16
15
|
graph = await build_graph(tool_registry, instructions=instructions)
|
|
17
16
|
return graph
|
|
18
17
|
|
|
19
|
-
graph = asyncio.run(main())
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
18
|
|
|
19
|
+
graph = asyncio.run(main())
|
universal_mcp/agents/base.py
CHANGED
|
@@ -10,7 +10,14 @@ from .utils import RichCLI
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class BaseAgent:
|
|
13
|
-
def __init__(
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
name: str,
|
|
16
|
+
instructions: str,
|
|
17
|
+
model: str,
|
|
18
|
+
memory: BaseCheckpointSaver | None = None,
|
|
19
|
+
**kwargs,
|
|
20
|
+
):
|
|
14
21
|
self.name = name
|
|
15
22
|
self.instructions = instructions
|
|
16
23
|
self.model = model
|
|
@@ -27,12 +34,26 @@ class BaseAgent:
|
|
|
27
34
|
async def _build_graph(self):
|
|
28
35
|
raise NotImplementedError("Subclasses must implement this method")
|
|
29
36
|
|
|
30
|
-
async def stream(self, thread_id: str, user_input: str):
|
|
37
|
+
async def stream(self, thread_id: str, user_input: str, metadata: dict = None):
|
|
31
38
|
await self.ainit()
|
|
32
39
|
aggregate = None
|
|
40
|
+
|
|
41
|
+
run_metadata = {
|
|
42
|
+
"agent_name": self.name,
|
|
43
|
+
"is_background_run": False, # Default to False
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if metadata:
|
|
47
|
+
run_metadata.update(metadata)
|
|
48
|
+
|
|
49
|
+
run_config = {
|
|
50
|
+
"configurable": {"thread_id": thread_id},
|
|
51
|
+
"metadata": run_metadata,
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
async for event, metadata in self._graph.astream(
|
|
34
55
|
{"messages": [{"role": "user", "content": user_input}]},
|
|
35
|
-
config=
|
|
56
|
+
config=run_config,
|
|
36
57
|
context={"system_prompt": self.instructions, "model": self.model},
|
|
37
58
|
stream_mode="messages",
|
|
38
59
|
stream_usage=True,
|
|
@@ -66,12 +87,28 @@ class BaseAgent:
|
|
|
66
87
|
async for event in self.stream(thread_id, user_input):
|
|
67
88
|
stream_updater.update(event.content)
|
|
68
89
|
|
|
69
|
-
async def invoke(
|
|
90
|
+
async def invoke(
|
|
91
|
+
self, user_input: str, thread_id: str = str(uuid4()), metadata: dict = None
|
|
92
|
+
):
|
|
70
93
|
"""Run the agent"""
|
|
71
94
|
await self.ainit()
|
|
95
|
+
|
|
96
|
+
run_metadata = {
|
|
97
|
+
"agent_name": self.name,
|
|
98
|
+
"is_background_run": False, # Default to False
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if metadata:
|
|
102
|
+
run_metadata.update(metadata)
|
|
103
|
+
|
|
104
|
+
run_config = {
|
|
105
|
+
"configurable": {"thread_id": thread_id},
|
|
106
|
+
"metadata": run_metadata,
|
|
107
|
+
}
|
|
108
|
+
|
|
72
109
|
return await self._graph.ainvoke(
|
|
73
110
|
{"messages": [{"role": "user", "content": user_input}]},
|
|
74
|
-
config=
|
|
111
|
+
config=run_config,
|
|
75
112
|
context={"system_prompt": self.instructions, "model": self.model},
|
|
76
113
|
)
|
|
77
114
|
|
|
@@ -85,10 +122,15 @@ class BaseAgent:
|
|
|
85
122
|
# Main loop
|
|
86
123
|
while True:
|
|
87
124
|
try:
|
|
88
|
-
state = self._graph.get_state(
|
|
125
|
+
state = self._graph.get_state(
|
|
126
|
+
config={"configurable": {"thread_id": thread_id}}
|
|
127
|
+
)
|
|
89
128
|
if state.interrupts:
|
|
90
129
|
value = self.cli.handle_interrupt(state.interrupts[0])
|
|
91
|
-
self._graph.invoke(
|
|
130
|
+
self._graph.invoke(
|
|
131
|
+
Command(resume=value),
|
|
132
|
+
config={"configurable": {"thread_id": thread_id}},
|
|
133
|
+
)
|
|
92
134
|
continue
|
|
93
135
|
|
|
94
136
|
user_input = self.cli.get_user_input()
|
|
@@ -99,7 +141,9 @@ class BaseAgent:
|
|
|
99
141
|
if user_input.startswith("/"):
|
|
100
142
|
command = user_input.lower().lstrip("/")
|
|
101
143
|
if command == "about":
|
|
102
|
-
self.cli.display_info(
|
|
144
|
+
self.cli.display_info(
|
|
145
|
+
f"Agent is {self.name}. {self.instructions}"
|
|
146
|
+
)
|
|
103
147
|
continue
|
|
104
148
|
elif command == "exit" or command == "quit" or command == "q":
|
|
105
149
|
self.cli.display_info("Goodbye! 👋")
|
|
@@ -110,7 +154,9 @@ class BaseAgent:
|
|
|
110
154
|
thread_id = str(uuid4())
|
|
111
155
|
continue
|
|
112
156
|
elif command == "help":
|
|
113
|
-
self.cli.display_info(
|
|
157
|
+
self.cli.display_info(
|
|
158
|
+
"Available commands: /about, /exit, /quit, /q, /reset"
|
|
159
|
+
)
|
|
114
160
|
continue
|
|
115
161
|
else:
|
|
116
162
|
self.cli.display_error(f"Unknown command: {command}")
|
|
@@ -27,7 +27,9 @@ class BigToolAgent(BaseAgent):
|
|
|
27
27
|
self.llm = load_chat_model(self.model)
|
|
28
28
|
self.tool_selection_llm = load_chat_model("gemini/gemini-2.0-flash-001")
|
|
29
29
|
|
|
30
|
-
logger.info(
|
|
30
|
+
logger.info(
|
|
31
|
+
f"BigToolAgent '{self.name}' initialized with model '{self.model}'."
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
async def _build_graph(self):
|
|
33
35
|
"""Build the bigtool agent graph using the existing create_agent function."""
|
|
@@ -32,7 +32,9 @@ def build_graph(
|
|
|
32
32
|
logger.info(f"Retrieving tools for task: '{task_query}'")
|
|
33
33
|
try:
|
|
34
34
|
tools_list = await tool_registry.search_tools(task_query, limit=10)
|
|
35
|
-
tool_candidates = [
|
|
35
|
+
tool_candidates = [
|
|
36
|
+
f"{tool['id']}: {tool['description']}" for tool in tools_list
|
|
37
|
+
]
|
|
36
38
|
logger.info(f"Found {len(tool_candidates)} candidate tools.")
|
|
37
39
|
|
|
38
40
|
class ToolSelectionOutput(TypedDict):
|
|
@@ -42,19 +44,28 @@ def build_graph(
|
|
|
42
44
|
app_ids = await tool_registry.list_all_apps()
|
|
43
45
|
connections = await tool_registry.list_connected_apps()
|
|
44
46
|
connection_ids = set([connection["app_id"] for connection in connections])
|
|
45
|
-
connected_apps = [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
connected_apps = [
|
|
48
|
+
app["id"] for app in app_ids if app["id"] in connection_ids
|
|
49
|
+
]
|
|
50
|
+
unconnected_apps = [
|
|
51
|
+
app["id"] for app in app_ids if app["id"] not in connection_ids
|
|
52
|
+
]
|
|
53
|
+
app_id_descriptions = (
|
|
54
|
+
"These are the apps connected to the user's account:\n"
|
|
55
|
+
+ "\n".join([f"{app}" for app in connected_apps])
|
|
49
56
|
)
|
|
50
57
|
if unconnected_apps:
|
|
51
58
|
app_id_descriptions += "\n\nOther (not connected) apps: " + "\n".join(
|
|
52
59
|
[f"{app}" for app in unconnected_apps]
|
|
53
60
|
)
|
|
54
61
|
|
|
55
|
-
response = await model.with_structured_output(
|
|
62
|
+
response = await model.with_structured_output(
|
|
63
|
+
schema=ToolSelectionOutput, method="json_mode"
|
|
64
|
+
).ainvoke(
|
|
56
65
|
SELECT_TOOL_PROMPT.format(
|
|
57
|
-
app_ids=app_id_descriptions,
|
|
66
|
+
app_ids=app_id_descriptions,
|
|
67
|
+
tool_candidates="\n - ".join(tool_candidates),
|
|
68
|
+
task=task_query,
|
|
58
69
|
)
|
|
59
70
|
)
|
|
60
71
|
|
|
@@ -65,15 +76,24 @@ def build_graph(
|
|
|
65
76
|
logger.error(f"Error retrieving tools: {e}")
|
|
66
77
|
return []
|
|
67
78
|
|
|
68
|
-
async def call_model(
|
|
79
|
+
async def call_model(
|
|
80
|
+
state: State, runtime: Runtime[Context]
|
|
81
|
+
) -> Command[Literal["select_tools", "call_tools"]]:
|
|
69
82
|
logger.info("Calling model...")
|
|
70
83
|
try:
|
|
71
|
-
system_message = runtime.context.system_prompt.format(
|
|
72
|
-
|
|
84
|
+
system_message = runtime.context.system_prompt.format(
|
|
85
|
+
system_time=datetime.now(tz=UTC).isoformat()
|
|
86
|
+
)
|
|
87
|
+
messages = [
|
|
88
|
+
{"role": "system", "content": system_message},
|
|
89
|
+
*state["messages"],
|
|
90
|
+
]
|
|
73
91
|
|
|
74
92
|
logger.info(f"Selected tool IDs: {state['selected_tool_ids']}")
|
|
75
93
|
if len(state["selected_tool_ids"]) > 0:
|
|
76
|
-
selected_tools = await tool_registry.export_tools(
|
|
94
|
+
selected_tools = await tool_registry.export_tools(
|
|
95
|
+
tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN
|
|
96
|
+
)
|
|
77
97
|
logger.info(f"Exported {len(selected_tools)} tools for model.")
|
|
78
98
|
else:
|
|
79
99
|
selected_tools = []
|
|
@@ -81,29 +101,43 @@ def build_graph(
|
|
|
81
101
|
model = llm
|
|
82
102
|
if isinstance(model, ChatAnthropic):
|
|
83
103
|
model_with_tools = model.bind_tools(
|
|
84
|
-
[retrieve_tools, *selected_tools],
|
|
104
|
+
[retrieve_tools, *selected_tools],
|
|
105
|
+
tool_choice="auto",
|
|
106
|
+
cache_control={"type": "ephemeral"},
|
|
85
107
|
)
|
|
86
108
|
else:
|
|
87
|
-
model_with_tools = model.bind_tools(
|
|
109
|
+
model_with_tools = model.bind_tools(
|
|
110
|
+
[retrieve_tools, *selected_tools], tool_choice="auto"
|
|
111
|
+
)
|
|
88
112
|
response = cast(AIMessage, await model_with_tools.ainvoke(messages))
|
|
89
113
|
|
|
90
114
|
if response.tool_calls:
|
|
91
|
-
logger.info(
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Model responded with {len(response.tool_calls)} tool calls."
|
|
117
|
+
)
|
|
92
118
|
if len(response.tool_calls) > 1:
|
|
93
|
-
raise Exception(
|
|
119
|
+
raise Exception(
|
|
120
|
+
"Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')"
|
|
121
|
+
)
|
|
94
122
|
tool_call = response.tool_calls[0]
|
|
95
123
|
if tool_call["name"] == retrieve_tools.name:
|
|
96
124
|
logger.info("Model requested to select tools.")
|
|
97
125
|
return Command(goto="select_tools", update={"messages": [response]})
|
|
98
126
|
elif tool_call["name"] not in state["selected_tool_ids"]:
|
|
99
127
|
try:
|
|
100
|
-
await tool_registry.export_tools(
|
|
128
|
+
await tool_registry.export_tools(
|
|
129
|
+
[tool_call["name"]], ToolFormat.LANGCHAIN
|
|
130
|
+
)
|
|
101
131
|
logger.info(
|
|
102
132
|
f"Tool '{tool_call['name']}' not in selected tools, but available. Proceeding to call."
|
|
103
133
|
)
|
|
104
|
-
return Command(
|
|
134
|
+
return Command(
|
|
135
|
+
goto="call_tools", update={"messages": [response]}
|
|
136
|
+
)
|
|
105
137
|
except Exception as e:
|
|
106
|
-
logger.error(
|
|
138
|
+
logger.error(
|
|
139
|
+
f"Unexpected tool call: {tool_call['name']}. Error: {e}"
|
|
140
|
+
)
|
|
107
141
|
raise Exception(
|
|
108
142
|
f"Unexpected tool call: {tool_call['name']}. Available tools: {state['selected_tool_ids']}"
|
|
109
143
|
) from e
|
|
@@ -116,14 +150,24 @@ def build_graph(
|
|
|
116
150
|
logger.error(f"Error in call_model: {e}")
|
|
117
151
|
raise
|
|
118
152
|
|
|
119
|
-
async def select_tools(
|
|
153
|
+
async def select_tools(
|
|
154
|
+
state: State, runtime: Runtime[Context]
|
|
155
|
+
) -> Command[Literal["call_model"]]:
|
|
120
156
|
logger.info("Selecting tools...")
|
|
121
157
|
try:
|
|
122
158
|
tool_call = state["messages"][-1].tool_calls[0]
|
|
123
159
|
selected_tool_names = await retrieve_tools.ainvoke(input=tool_call["args"])
|
|
124
|
-
tool_msg = ToolMessage(
|
|
160
|
+
tool_msg = ToolMessage(
|
|
161
|
+
f"Available tools: {selected_tool_names}", tool_call_id=tool_call["id"]
|
|
162
|
+
)
|
|
125
163
|
logger.info(f"Tools selected: {selected_tool_names}")
|
|
126
|
-
return Command(
|
|
164
|
+
return Command(
|
|
165
|
+
goto="call_model",
|
|
166
|
+
update={
|
|
167
|
+
"messages": [tool_msg],
|
|
168
|
+
"selected_tool_ids": selected_tool_names,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
127
171
|
except Exception as e:
|
|
128
172
|
logger.error(f"Error in select_tools: {e}")
|
|
129
173
|
raise
|
|
@@ -133,10 +177,16 @@ def build_graph(
|
|
|
133
177
|
outputs = []
|
|
134
178
|
recent_tool_ids = []
|
|
135
179
|
for tool_call in state["messages"][-1].tool_calls:
|
|
136
|
-
logger.info(
|
|
180
|
+
logger.info(
|
|
181
|
+
f"Executing tool: {tool_call['name']} with args: {tool_call['args']}"
|
|
182
|
+
)
|
|
137
183
|
try:
|
|
138
|
-
await tool_registry.export_tools(
|
|
139
|
-
|
|
184
|
+
await tool_registry.export_tools(
|
|
185
|
+
[tool_call["name"]], ToolFormat.LANGCHAIN
|
|
186
|
+
)
|
|
187
|
+
tool_result = await tool_registry.call_tool(
|
|
188
|
+
tool_call["name"], tool_call["args"]
|
|
189
|
+
)
|
|
140
190
|
logger.info(f"Tool '{tool_call['name']}' executed successfully.")
|
|
141
191
|
outputs.append(
|
|
142
192
|
ToolMessage(
|
|
@@ -155,7 +205,10 @@ def build_graph(
|
|
|
155
205
|
tool_call_id=tool_call["id"],
|
|
156
206
|
)
|
|
157
207
|
)
|
|
158
|
-
return Command(
|
|
208
|
+
return Command(
|
|
209
|
+
goto="call_model",
|
|
210
|
+
update={"messages": outputs, "selected_tool_ids": recent_tool_ids},
|
|
211
|
+
)
|
|
159
212
|
|
|
160
213
|
builder = StateGraph(State, context_schema=Context)
|
|
161
214
|
|
|
@@ -27,7 +27,9 @@ class BigToolAgent2(BaseAgent):
|
|
|
27
27
|
self.llm = load_chat_model(self.model)
|
|
28
28
|
self.recursion_limit = kwargs.get("recursion_limit", 10)
|
|
29
29
|
|
|
30
|
-
logger.info(
|
|
30
|
+
logger.info(
|
|
31
|
+
f"BigToolAgent '{self.name}' initialized with model '{self.model}'."
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
async def _build_graph(self):
|
|
33
35
|
"""Build the bigtool agent graph using the existing create_agent function."""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from universal_mcp.agents.bigtool2 import BigToolAgent2
|
|
2
2
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
async def agent():
|
|
5
6
|
agent_object = await BigToolAgent2(
|
|
6
7
|
name="BigTool Agent 2",
|
|
@@ -8,4 +9,4 @@ async def agent():
|
|
|
8
9
|
model="anthropic/claude-4-sonnet-20250514",
|
|
9
10
|
registry=AgentrRegistry(),
|
|
10
11
|
)._build_graph()
|
|
11
|
-
return agent_object
|
|
12
|
+
return agent_object
|
|
@@ -17,11 +17,7 @@ from universal_mcp.tools.registry import ToolRegistry
|
|
|
17
17
|
from universal_mcp.types import ToolFormat
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
def build_graph(
|
|
22
|
-
tool_registry: ToolRegistry,
|
|
23
|
-
llm: BaseChatModel
|
|
24
|
-
):
|
|
20
|
+
def build_graph(tool_registry: ToolRegistry, llm: BaseChatModel):
|
|
25
21
|
@tool
|
|
26
22
|
async def search_tools(queries: list[str]) -> str:
|
|
27
23
|
"""Search tools for a given list of queries
|
|
@@ -33,12 +29,18 @@ def build_graph(
|
|
|
33
29
|
app_ids = await tool_registry.list_all_apps()
|
|
34
30
|
connections = await tool_registry.list_connected_apps()
|
|
35
31
|
connection_ids = set([connection["app_id"] for connection in connections])
|
|
36
|
-
connected_apps = [
|
|
37
|
-
|
|
32
|
+
connected_apps = [
|
|
33
|
+
app["id"] for app in app_ids if app["id"] in connection_ids
|
|
34
|
+
]
|
|
35
|
+
unconnected_apps = [
|
|
36
|
+
app["id"] for app in app_ids if app["id"] not in connection_ids
|
|
37
|
+
]
|
|
38
38
|
app_tools = {}
|
|
39
39
|
for task_query in queries:
|
|
40
40
|
tools_list = await tool_registry.search_tools(task_query, limit=40)
|
|
41
|
-
tool_candidates = [
|
|
41
|
+
tool_candidates = [
|
|
42
|
+
f"{tool['id']}: {tool['description']}" for tool in tools_list
|
|
43
|
+
]
|
|
42
44
|
for tool in tool_candidates:
|
|
43
45
|
app = tool.split("__")[0]
|
|
44
46
|
if app not in app_tools:
|
|
@@ -49,65 +51,94 @@ def build_graph(
|
|
|
49
51
|
app_tools[app].append(tool)
|
|
50
52
|
for app in app_tools:
|
|
51
53
|
app_status = "connected" if app in connected_apps else "NOT connected"
|
|
52
|
-
all_tool_candidates +=
|
|
54
|
+
all_tool_candidates += (
|
|
55
|
+
f"Tools from {app} (status: {app_status} by user):\n"
|
|
56
|
+
)
|
|
53
57
|
for tool in app_tools[app]:
|
|
54
58
|
all_tool_candidates += f" - {tool}\n"
|
|
55
59
|
all_tool_candidates += "\n"
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
|
|
58
61
|
return all_tool_candidates
|
|
59
62
|
except Exception as e:
|
|
60
63
|
logger.error(f"Error retrieving tools: {e}")
|
|
61
64
|
return "Error: " + str(e)
|
|
62
|
-
|
|
65
|
+
|
|
63
66
|
@tool
|
|
64
67
|
async def load_tools(tool_ids: list[str]) -> list[str]:
|
|
65
68
|
"""Load the tools for the given tool ids. Returns the tool ids."""
|
|
66
69
|
return tool_ids
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
async def call_model(
|
|
72
|
+
state: State, runtime: Runtime[Context]
|
|
73
|
+
) -> Command[Literal["select_tools", "call_tools"]]:
|
|
70
74
|
logger.info("Calling model...")
|
|
71
75
|
try:
|
|
72
|
-
system_message = runtime.context.system_prompt.format(
|
|
73
|
-
|
|
76
|
+
system_message = runtime.context.system_prompt.format(
|
|
77
|
+
system_time=datetime.now(tz=UTC).isoformat()
|
|
78
|
+
)
|
|
79
|
+
messages = [
|
|
80
|
+
{"role": "system", "content": system_message},
|
|
81
|
+
*state["messages"],
|
|
82
|
+
]
|
|
74
83
|
|
|
75
84
|
logger.info(f"Selected tool IDs: {state['selected_tool_ids']}")
|
|
76
85
|
if len(state["selected_tool_ids"]) > 0:
|
|
77
|
-
selected_tools = await tool_registry.export_tools(
|
|
86
|
+
selected_tools = await tool_registry.export_tools(
|
|
87
|
+
tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN
|
|
88
|
+
)
|
|
78
89
|
logger.info(f"Exported {len(selected_tools)} tools for model.")
|
|
79
90
|
else:
|
|
80
91
|
selected_tools = []
|
|
81
92
|
|
|
82
93
|
model = llm
|
|
83
94
|
|
|
84
|
-
model_with_tools = model.bind_tools(
|
|
95
|
+
model_with_tools = model.bind_tools(
|
|
96
|
+
[search_tools, load_tools, *selected_tools], tool_choice="auto"
|
|
97
|
+
)
|
|
85
98
|
response = cast(AIMessage, await model_with_tools.ainvoke(messages))
|
|
86
99
|
|
|
87
100
|
if response.tool_calls:
|
|
88
|
-
logger.info(
|
|
101
|
+
logger.info(
|
|
102
|
+
f"Model responded with {len(response.tool_calls)} tool calls."
|
|
103
|
+
)
|
|
89
104
|
if len(response.tool_calls) > 1:
|
|
90
|
-
raise Exception(
|
|
105
|
+
raise Exception(
|
|
106
|
+
"Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')"
|
|
107
|
+
)
|
|
91
108
|
tool_call = response.tool_calls[0]
|
|
92
109
|
if tool_call["name"] == search_tools.name:
|
|
93
110
|
logger.info("Model requested to select tools.")
|
|
94
111
|
return Command(goto="select_tools", update={"messages": [response]})
|
|
95
112
|
elif tool_call["name"] == load_tools.name:
|
|
96
113
|
logger.info("Model requested to load tools.")
|
|
97
|
-
tool_msg = ToolMessage(
|
|
114
|
+
tool_msg = ToolMessage(
|
|
115
|
+
f"Loaded tools.", tool_call_id=tool_call["id"]
|
|
116
|
+
)
|
|
98
117
|
selected_tool_ids = tool_call["args"]["tool_ids"]
|
|
99
118
|
logger.info(f"Loaded tools: {selected_tool_ids}")
|
|
100
|
-
return Command(
|
|
119
|
+
return Command(
|
|
120
|
+
goto="call_model",
|
|
121
|
+
update={
|
|
122
|
+
"messages": [response, tool_msg],
|
|
123
|
+
"selected_tool_ids": selected_tool_ids,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
101
126
|
|
|
102
127
|
elif tool_call["name"] not in state["selected_tool_ids"]:
|
|
103
128
|
try:
|
|
104
|
-
await tool_registry.export_tools(
|
|
129
|
+
await tool_registry.export_tools(
|
|
130
|
+
[tool_call["name"]], ToolFormat.LANGCHAIN
|
|
131
|
+
)
|
|
105
132
|
logger.info(
|
|
106
133
|
f"Tool '{tool_call['name']}' not in selected tools, but available. Proceeding to call."
|
|
107
134
|
)
|
|
108
|
-
return Command(
|
|
135
|
+
return Command(
|
|
136
|
+
goto="call_tools", update={"messages": [response]}
|
|
137
|
+
)
|
|
109
138
|
except Exception as e:
|
|
110
|
-
logger.error(
|
|
139
|
+
logger.error(
|
|
140
|
+
f"Unexpected tool call: {tool_call['name']}. Error: {e}"
|
|
141
|
+
)
|
|
111
142
|
raise Exception(
|
|
112
143
|
f"Unexpected tool call: {tool_call['name']}. Available tools: {state['selected_tool_ids']}"
|
|
113
144
|
) from e
|
|
@@ -120,12 +151,16 @@ def build_graph(
|
|
|
120
151
|
logger.error(f"Error in call_model: {e}")
|
|
121
152
|
raise
|
|
122
153
|
|
|
123
|
-
async def select_tools(
|
|
154
|
+
async def select_tools(
|
|
155
|
+
state: State, runtime: Runtime[Context]
|
|
156
|
+
) -> Command[Literal["call_model"]]:
|
|
124
157
|
logger.info("Selecting tools...")
|
|
125
158
|
try:
|
|
126
159
|
tool_call = state["messages"][-1].tool_calls[0]
|
|
127
|
-
searched_tools= await search_tools.ainvoke(input=tool_call["args"])
|
|
128
|
-
tool_msg = ToolMessage(
|
|
160
|
+
searched_tools = await search_tools.ainvoke(input=tool_call["args"])
|
|
161
|
+
tool_msg = ToolMessage(
|
|
162
|
+
f"Available tools: {searched_tools}", tool_call_id=tool_call["id"]
|
|
163
|
+
)
|
|
129
164
|
return Command(goto="call_model", update={"messages": [tool_msg]})
|
|
130
165
|
except Exception as e:
|
|
131
166
|
logger.error(f"Error in select_tools: {e}")
|
|
@@ -136,10 +171,16 @@ def build_graph(
|
|
|
136
171
|
outputs = []
|
|
137
172
|
recent_tool_ids = []
|
|
138
173
|
for tool_call in state["messages"][-1].tool_calls:
|
|
139
|
-
logger.info(
|
|
174
|
+
logger.info(
|
|
175
|
+
f"Executing tool: {tool_call['name']} with args: {tool_call['args']}"
|
|
176
|
+
)
|
|
140
177
|
try:
|
|
141
|
-
await tool_registry.export_tools(
|
|
142
|
-
|
|
178
|
+
await tool_registry.export_tools(
|
|
179
|
+
[tool_call["name"]], ToolFormat.LANGCHAIN
|
|
180
|
+
)
|
|
181
|
+
tool_result = await tool_registry.call_tool(
|
|
182
|
+
tool_call["name"], tool_call["args"]
|
|
183
|
+
)
|
|
143
184
|
logger.info(f"Tool '{tool_call['name']}' executed successfully.")
|
|
144
185
|
outputs.append(
|
|
145
186
|
ToolMessage(
|
|
@@ -158,7 +199,10 @@ def build_graph(
|
|
|
158
199
|
tool_call_id=tool_call["id"],
|
|
159
200
|
)
|
|
160
201
|
)
|
|
161
|
-
return Command(
|
|
202
|
+
return Command(
|
|
203
|
+
goto="call_model",
|
|
204
|
+
update={"messages": outputs, "selected_tool_ids": recent_tool_ids},
|
|
205
|
+
)
|
|
162
206
|
|
|
163
207
|
builder = StateGraph(State, context_schema=Context)
|
|
164
208
|
|