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.

Files changed (32) hide show
  1. universal_mcp/agents/autoagent/graph.py +30 -11
  2. universal_mcp/agents/autoagent/studio.py +1 -7
  3. universal_mcp/agents/base.py +55 -9
  4. universal_mcp/agents/bigtool/__init__.py +3 -1
  5. universal_mcp/agents/bigtool/graph.py +78 -25
  6. universal_mcp/agents/bigtool2/__init__.py +3 -1
  7. universal_mcp/agents/bigtool2/agent.py +2 -1
  8. universal_mcp/agents/bigtool2/context.py +0 -1
  9. universal_mcp/agents/bigtool2/graph.py +76 -32
  10. universal_mcp/agents/bigtoolcache/__init__.py +6 -2
  11. universal_mcp/agents/bigtoolcache/agent.py +2 -1
  12. universal_mcp/agents/bigtoolcache/context.py +0 -1
  13. universal_mcp/agents/bigtoolcache/graph.py +88 -59
  14. universal_mcp/agents/bigtoolcache/prompts.py +29 -0
  15. universal_mcp/agents/bigtoolcache/tools_all.txt +956 -0
  16. universal_mcp/agents/bigtoolcache/tools_important.txt +474 -0
  17. universal_mcp/agents/builder.py +19 -5
  18. universal_mcp/agents/codeact/__init__.py +16 -4
  19. universal_mcp/agents/hil.py +16 -4
  20. universal_mcp/agents/llm.py +5 -1
  21. universal_mcp/agents/planner/__init__.py +7 -3
  22. universal_mcp/agents/planner/__main__.py +3 -1
  23. universal_mcp/agents/planner/graph.py +3 -1
  24. universal_mcp/agents/react.py +5 -1
  25. universal_mcp/agents/shared/tool_node.py +24 -8
  26. universal_mcp/agents/simple.py +8 -1
  27. universal_mcp/agents/tools.py +9 -3
  28. universal_mcp/agents/utils.py +35 -7
  29. {universal_mcp_agents-0.1.3.dist-info → universal_mcp_agents-0.1.4.dist-info}/METADATA +2 -2
  30. universal_mcp_agents-0.1.4.dist-info/RECORD +53 -0
  31. universal_mcp_agents-0.1.3.dist-info/RECORD +0 -51
  32. {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(await tool_registry.search_tools(query, limit=10, app_id=app_id))
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 = [app["id"] for app in app_ids if app["id"] not in connection_ids]
52
- app_id_descriptions = "These are the apps connected to the user's account:\n" + "\n".join(
53
- [f"{app}" for app in connected_apps]
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(system_time=datetime.now(tz=UTC).isoformat(), app_ids=app_id_descriptions)
65
+ system_prompt = system_prompt.format(
66
+ system_time=datetime.now(tz=UTC).isoformat(), app_ids=app_id_descriptions
67
+ )
61
68
 
62
- messages = [{"role": "system", "content": system_prompt + "\n" + instructions}, *state["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(tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN)
65
- model_with_tools = model.bind_tools([search_tools, ask_user, load_tools, *loaded_tools], tool_choice="auto")
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) + "\n\nUse the load_tools tool to load the tools you want to use.",
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([tool_call["name"]], ToolFormat.LANGCHAIN)
137
+ await tool_registry.export_tools(
138
+ [tool_call["name"]], ToolFormat.LANGCHAIN
139
+ )
123
140
  try:
124
- tool_result = await tool_registry.call_tool(tool_call["name"], tool_call["args"])
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())
@@ -10,7 +10,14 @@ from .utils import RichCLI
10
10
 
11
11
 
12
12
  class BaseAgent:
13
- def __init__(self, name: str, instructions: str, model: str, memory: BaseCheckpointSaver | None = None, **kwargs):
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={"configurable": {"thread_id": thread_id}},
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(self, user_input: str, thread_id: str = str(uuid4())):
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={"configurable": {"thread_id": thread_id}},
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(config={"configurable": {"thread_id": thread_id}})
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(Command(resume=value), config={"configurable": {"thread_id": thread_id}})
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(f"Agent is {self.name}. {self.instructions}")
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("Available commands: /about, /exit, /quit, /q, /reset")
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(f"BigToolAgent '{self.name}' initialized with model '{self.model}'.")
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 = [f"{tool['id']}: {tool['description']}" for tool in tools_list]
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 = [app["id"] for app in app_ids if app["id"] in connection_ids]
46
- unconnected_apps = [app["id"] for app in app_ids if app["id"] not in connection_ids]
47
- app_id_descriptions = "These are the apps connected to the user's account:\n" + "\n".join(
48
- [f"{app}" for app in connected_apps]
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(schema=ToolSelectionOutput, method="json_mode").ainvoke(
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, tool_candidates="\n - ".join(tool_candidates), task=task_query
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(state: State, runtime: Runtime[Context]) -> Command[Literal["select_tools", "call_tools"]]:
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(system_time=datetime.now(tz=UTC).isoformat())
72
- messages = [{"role": "system", "content": system_message}, *state["messages"]]
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(tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN)
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], tool_choice="auto", cache_control={"type": "ephemeral"}
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([retrieve_tools, *selected_tools], tool_choice="auto")
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(f"Model responded with {len(response.tool_calls)} tool calls.")
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("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
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([tool_call["name"]], ToolFormat.LANGCHAIN)
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(goto="call_tools", update={"messages": [response]})
134
+ return Command(
135
+ goto="call_tools", update={"messages": [response]}
136
+ )
105
137
  except Exception as e:
106
- logger.error(f"Unexpected tool call: {tool_call['name']}. Error: {e}")
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(state: State, runtime: Runtime[Context]) -> Command[Literal["call_model"]]:
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(f"Available tools: {selected_tool_names}", tool_call_id=tool_call["id"])
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(goto="call_model", update={"messages": [tool_msg], "selected_tool_ids": selected_tool_names})
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(f"Executing tool: {tool_call['name']} with args: {tool_call['args']}")
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([tool_call["name"]], ToolFormat.LANGCHAIN)
139
- tool_result = await tool_registry.call_tool(tool_call["name"], tool_call["args"])
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(goto="call_model", update={"messages": outputs, "selected_tool_ids": recent_tool_ids})
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(f"BigToolAgent '{self.name}' initialized with model '{self.model}'.")
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
@@ -30,4 +30,3 @@ class Context:
30
30
  "This is to prevent infinite recursion."
31
31
  },
32
32
  )
33
-
@@ -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 = [app["id"] for app in app_ids if app["id"] in connection_ids]
37
- unconnected_apps = [app["id"] for app in app_ids if app["id"] not in connection_ids]
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 = [f"{tool['id']}: {tool['description']}" for tool in tools_list]
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 += f"Tools from {app} (status: {app_status} by user):\n"
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
- async def call_model(state: State, runtime: Runtime[Context]) -> Command[Literal["select_tools", "call_tools"]]:
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(system_time=datetime.now(tz=UTC).isoformat())
73
- messages = [{"role": "system", "content": system_message}, *state["messages"]]
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(tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN)
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([search_tools, load_tools, *selected_tools], tool_choice="auto")
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(f"Model responded with {len(response.tool_calls)} tool calls.")
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("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
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(f"Loaded tools.", tool_call_id=tool_call["id"])
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(goto="call_model", update={ "messages": [response, tool_msg], "selected_tool_ids": selected_tool_ids})
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([tool_call["name"]], ToolFormat.LANGCHAIN)
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(goto="call_tools", update={"messages": [response]})
135
+ return Command(
136
+ goto="call_tools", update={"messages": [response]}
137
+ )
109
138
  except Exception as e:
110
- logger.error(f"Unexpected tool call: {tool_call['name']}. Error: {e}")
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(state: State, runtime: Runtime[Context]) -> Command[Literal["call_model"]]:
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(f"Available tools: {searched_tools}", tool_call_id=tool_call["id"])
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(f"Executing tool: {tool_call['name']} with args: {tool_call['args']}")
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([tool_call["name"]], ToolFormat.LANGCHAIN)
142
- tool_result = await tool_registry.call_tool(tool_call["name"], tool_call["args"])
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(goto="call_model", update={"messages": outputs, "selected_tool_ids": recent_tool_ids})
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