universal-mcp-agents 0.1.9__py3-none-any.whl → 0.1.11__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 +9 -9
  2. universal_mcp/agents/base.py +13 -18
  3. universal_mcp/agents/bigtool2/__init__.py +6 -7
  4. universal_mcp/agents/bigtool2/__main__.py +2 -4
  5. universal_mcp/agents/bigtool2/agent.py +1 -0
  6. universal_mcp/agents/bigtool2/graph.py +48 -184
  7. universal_mcp/agents/bigtool2/meta_tools.py +120 -0
  8. universal_mcp/agents/bigtoolcache/__init__.py +31 -22
  9. universal_mcp/agents/bigtoolcache/__main__.py +1 -4
  10. universal_mcp/agents/bigtoolcache/agent.py +1 -3
  11. universal_mcp/agents/bigtoolcache/graph.py +101 -191
  12. universal_mcp/agents/bigtoolcache/prompts.py +7 -31
  13. universal_mcp/agents/bigtoolcache/tools.py +141 -0
  14. universal_mcp/agents/builder.py +10 -20
  15. universal_mcp/agents/cli.py +1 -2
  16. universal_mcp/agents/codeact/__init__.py +1 -1
  17. universal_mcp/agents/codeact/__main__.py +15 -5
  18. universal_mcp/agents/codeact/agent.py +67 -100
  19. universal_mcp/agents/codeact/prompts.py +32 -42
  20. universal_mcp/agents/codeact/sandbox.py +30 -39
  21. universal_mcp/agents/codeact/state.py +3 -6
  22. universal_mcp/agents/codeact/utils.py +12 -5
  23. universal_mcp/agents/hil.py +1 -6
  24. universal_mcp/agents/planner/__init__.py +1 -3
  25. universal_mcp/agents/planner/graph.py +1 -3
  26. universal_mcp/agents/react.py +14 -6
  27. universal_mcp/agents/shared/prompts.py +3 -3
  28. universal_mcp/agents/shared/tool_node.py +47 -47
  29. universal_mcp/agents/simple.py +2 -1
  30. universal_mcp/agents/utils.py +4 -15
  31. universal_mcp/applications/ui/app.py +5 -15
  32. {universal_mcp_agents-0.1.9.dist-info → universal_mcp_agents-0.1.11.dist-info}/METADATA +2 -1
  33. universal_mcp_agents-0.1.11.dist-info/RECORD +42 -0
  34. universal_mcp/agents/autoagent/__init__.py +0 -30
  35. universal_mcp/agents/autoagent/__main__.py +0 -25
  36. universal_mcp/agents/autoagent/context.py +0 -26
  37. universal_mcp/agents/autoagent/graph.py +0 -170
  38. universal_mcp/agents/autoagent/prompts.py +0 -9
  39. universal_mcp/agents/autoagent/state.py +0 -27
  40. universal_mcp/agents/autoagent/utils.py +0 -13
  41. universal_mcp/agents/bigtool/__init__.py +0 -58
  42. universal_mcp/agents/bigtool/__main__.py +0 -23
  43. universal_mcp/agents/bigtool/graph.py +0 -210
  44. universal_mcp/agents/bigtool/prompts.py +0 -31
  45. universal_mcp/agents/bigtool/state.py +0 -27
  46. universal_mcp/agents/bigtoolcache/tools_all.txt +0 -956
  47. universal_mcp/agents/bigtoolcache/tools_important.txt +0 -474
  48. universal_mcp_agents-0.1.9.dist-info/RECORD +0 -54
  49. {universal_mcp_agents-0.1.9.dist-info → universal_mcp_agents-0.1.11.dist-info}/WHEEL +0 -0
@@ -1,170 +0,0 @@
1
- import json
2
- from datetime import UTC, datetime
3
- from typing import cast
4
-
5
- from langchain_core.messages import AIMessage, ToolMessage
6
- from langchain_core.tools import tool
7
- from langgraph.graph import END, START, StateGraph
8
- from langgraph.runtime import Runtime
9
- from universal_mcp.tools.registry import ToolRegistry
10
- from universal_mcp.types import ToolFormat
11
-
12
- from universal_mcp.agents.autoagent.context import Context
13
- from universal_mcp.agents.autoagent.prompts import SYSTEM_PROMPT
14
- from universal_mcp.agents.autoagent.state import State
15
- from universal_mcp.agents.llm import load_chat_model
16
-
17
-
18
- async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
19
- @tool()
20
- async def search_tools(query: str, app_ids: list[str] | None = None) -> list[str]:
21
- """Retrieve tools using a search query and a list of app ids. Use multiple times if you require tools for different queries."""
22
- tools_list = []
23
- if app_ids is not None:
24
- for app_id in app_ids:
25
- tools_list.extend(
26
- await tool_registry.search_tools(query, limit=10, app_id=app_id)
27
- )
28
- else:
29
- tools_list = await tool_registry.search_tools(query, limit=10)
30
- tools_list = [f"{tool['id']}: {tool['description']}" for tool in tools_list]
31
- return tools_list
32
-
33
- @tool()
34
- async def ask_user(question: str) -> str:
35
- """Ask the user a question. Use this tool to ask the user for any missing information for performing a task, or when you have multiple apps to choose from for performing a task."""
36
- full_question = question
37
- return f"ASKING_USER: {full_question}"
38
-
39
- @tool()
40
- async def load_tools(tools: list[str]) -> list[str]:
41
- """Choose the tools you want to use by passing their tool ids. Loads the tools for the chosen tools and returns the tool ids."""
42
- return tools
43
-
44
- async def call_model(
45
- state: State,
46
- runtime: Runtime[Context],
47
- ):
48
- system_prompt = SYSTEM_PROMPT
49
- app_ids = await tool_registry.list_all_apps()
50
- connections = await tool_registry.list_connected_apps()
51
- connection_ids = set([connection["app_id"] for connection in connections])
52
- connected_apps = [app["id"] for app in app_ids if app["id"] in connection_ids]
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])
59
- )
60
- if unconnected_apps:
61
- app_id_descriptions += "\n\nOther (not connected) apps: " + "\n".join(
62
- [f"{app}" for app in unconnected_apps]
63
- )
64
-
65
- system_prompt = system_prompt.format(
66
- system_time=datetime.now(tz=UTC).isoformat(), app_ids=app_id_descriptions
67
- )
68
-
69
- messages = [
70
- {"role": "system", "content": system_prompt + "\n" + instructions},
71
- *state["messages"],
72
- ]
73
- model = load_chat_model(runtime.context.model)
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
- )
80
- response_raw = model_with_tools.invoke(messages)
81
- response = cast(AIMessage, response_raw)
82
- return {"messages": [response]}
83
-
84
- # Define the conditional edge that determines whether to continue or not
85
- def should_continue(state: State):
86
- messages = state["messages"]
87
- last_message = messages[-1]
88
- # If there is no function call, then we finish
89
- if not last_message.tool_calls:
90
- return END
91
- else:
92
- return "tools"
93
-
94
- def tool_router(state: State):
95
- last_message = state["messages"][-1]
96
- if isinstance(last_message, ToolMessage) and last_message.name == ask_user.name:
97
- return END
98
- else:
99
- return "agent"
100
-
101
- async def tool_node(state: State):
102
- outputs = []
103
- tool_ids = state["selected_tool_ids"]
104
- for tool_call in state["messages"][-1].tool_calls:
105
- if tool_call["name"] == ask_user.name:
106
- outputs.append(
107
- ToolMessage(
108
- content=json.dumps(
109
- "The user has been asked the question, and the run will wait for the user's response."
110
- ),
111
- name=tool_call["name"],
112
- tool_call_id=tool_call["id"],
113
- )
114
- )
115
- elif tool_call["name"] == search_tools.name:
116
- tools = await search_tools.ainvoke(tool_call["args"])
117
- outputs.append(
118
- ToolMessage(
119
- content=json.dumps(tools)
120
- + "\n\nUse the load_tools tool to load the tools you want to use.",
121
- name=tool_call["name"],
122
- tool_call_id=tool_call["id"],
123
- )
124
- )
125
-
126
- elif tool_call["name"] == load_tools.name:
127
- tool_ids = await load_tools.ainvoke(tool_call["args"])
128
-
129
- outputs.append(
130
- ToolMessage(
131
- content=json.dumps(tool_ids),
132
- name=tool_call["name"],
133
- tool_call_id=tool_call["id"],
134
- )
135
- )
136
- else:
137
- await tool_registry.export_tools(
138
- [tool_call["name"]], ToolFormat.LANGCHAIN
139
- )
140
- try:
141
- tool_result = await tool_registry.call_tool(
142
- tool_call["name"], tool_call["args"]
143
- )
144
- outputs.append(
145
- ToolMessage(
146
- content=json.dumps(tool_result),
147
- name=tool_call["name"],
148
- tool_call_id=tool_call["id"],
149
- )
150
- )
151
- except Exception as e:
152
- outputs.append(
153
- ToolMessage(
154
- content=json.dumps("Error: " + str(e)),
155
- name=tool_call["name"],
156
- tool_call_id=tool_call["id"],
157
- )
158
- )
159
- return {"messages": outputs, "selected_tool_ids": tool_ids}
160
-
161
- builder = StateGraph(State, context_schema=Context)
162
-
163
- builder.add_node("agent", call_model)
164
- builder.add_node("tools", tool_node)
165
-
166
- builder.add_edge(START, "agent")
167
- builder.add_conditional_edges("agent", should_continue)
168
- builder.add_conditional_edges("tools", tool_router)
169
-
170
- return builder
@@ -1,9 +0,0 @@
1
- """Default prompts used by the agent."""
2
-
3
- SYSTEM_PROMPT = """You are a helpful AI assistant. When you lack tools for any task you should use the `search_tools` function to unlock relevant tools. Whenever you need to ask the user for any information, or choose between multiple different applications, you can ask the user using the `ask_user` function.
4
-
5
- System time: {system_time}
6
- These are the list of apps available to you:
7
- {app_ids}
8
- Note that when multiple apps seem relevant for a task, you MUST ask the user to choose the app. Prefer connected apps over unconnected apps while breaking a tie. If more than one relevant app (or none of the relevant apps) are connected, you must ask the user to choose the app. In case the user asks you to use an app that is not connected, call the apps tools normally. You will be provided a link for connection that you should pass on to the user.
9
- """
@@ -1,27 +0,0 @@
1
- from typing import Annotated
2
-
3
- from langgraph.prebuilt.chat_agent_executor import AgentState
4
-
5
-
6
- def _enqueue(left: list, right: list) -> list:
7
- """Treat left as a FIFO queue, append new items from right (preserve order),
8
- keep items unique, and cap total size to 20 (drop oldest items)."""
9
- max_size = 30
10
- preferred_size = 20
11
- if len(right) > preferred_size:
12
- preferred_size = min(max_size, len(right))
13
- queue = list(left or [])
14
-
15
- for item in right[:preferred_size] or []:
16
- if item in queue:
17
- queue.remove(item)
18
- queue.append(item)
19
-
20
- if len(queue) > preferred_size:
21
- queue = queue[-preferred_size:]
22
-
23
- return queue
24
-
25
-
26
- class State(AgentState):
27
- selected_tool_ids: Annotated[list[str], _enqueue]
@@ -1,13 +0,0 @@
1
- from langchain_core.messages import BaseMessage
2
-
3
-
4
- def get_message_text(msg: BaseMessage) -> str:
5
- """Get the text content of a message."""
6
- content = msg.content
7
- if isinstance(content, str):
8
- return content
9
- elif isinstance(content, dict):
10
- return content.get("text", "")
11
- else:
12
- txts = [c if isinstance(c, str) else (c.get("text") or "") for c in content]
13
- return "".join(txts).strip()
@@ -1,58 +0,0 @@
1
- from langgraph.checkpoint.base import BaseCheckpointSaver
2
- from universal_mcp.logger import logger
3
- from universal_mcp.tools.registry import ToolRegistry
4
-
5
- from universal_mcp.agents.base import BaseAgent
6
- from universal_mcp.agents.llm import load_chat_model
7
-
8
- from .graph import build_graph
9
- from .prompts import SYSTEM_PROMPT
10
-
11
-
12
- class BigToolAgent(BaseAgent):
13
- def __init__(
14
- self,
15
- name: str,
16
- instructions: str,
17
- model: str,
18
- registry: ToolRegistry,
19
- memory: BaseCheckpointSaver | None = None,
20
- **kwargs,
21
- ):
22
- super().__init__(name, instructions, model, memory, **kwargs)
23
- self.registry = registry
24
- self.llm = load_chat_model(self.model)
25
-
26
- logger.info(
27
- f"BigToolAgent '{self.name}' initialized with model '{self.model}'."
28
- )
29
-
30
- def _build_system_message(self):
31
- return SYSTEM_PROMPT.format(
32
- name=self.name,
33
- instructions=self.instructions,
34
- )
35
-
36
- async def _build_graph(self):
37
- """Build the bigtool agent graph using the existing create_agent function."""
38
- logger.info(f"Building graph for BigToolAgent '{self.name}'...")
39
- try:
40
- graph_builder = build_graph(
41
- tool_registry=self.registry,
42
- llm=self.llm,
43
- system_prompt=self._build_system_message(),
44
- )
45
-
46
- compiled_graph = graph_builder.compile(checkpointer=self.memory)
47
- logger.info("Graph built and compiled successfully.")
48
- return compiled_graph
49
- except Exception as e:
50
- logger.error(f"Error building graph for BigToolAgent '{self.name}': {e}")
51
- raise
52
-
53
- @property
54
- def graph(self):
55
- return self._graph
56
-
57
-
58
- __all__ = ["BigToolAgent"]
@@ -1,23 +0,0 @@
1
- import asyncio
2
-
3
- from loguru import logger
4
- from universal_mcp.agentr.registry import AgentrRegistry
5
-
6
- from universal_mcp.agents.bigtool import BigToolAgent
7
- from universal_mcp.agents.utils import messages_to_list
8
-
9
-
10
- async def main():
11
- agent = BigToolAgent(
12
- name="bigtool",
13
- instructions="You are a helpful assistant that can use tools to help the user.",
14
- model="azure/gpt-4.1",
15
- registry=AgentrRegistry(),
16
- )
17
- await agent.ainit()
18
- output = await agent.invoke(
19
- user_input="Send an email to manoj@agentr.dev")
20
- logger.info(messages_to_list(output["messages"]))
21
-
22
- if __name__ == "__main__":
23
- asyncio.run(main())
@@ -1,210 +0,0 @@
1
- import json
2
- from typing import Literal, TypedDict, cast
3
-
4
- from langchain_core.language_models import BaseChatModel
5
- from langchain_core.messages import AIMessage, ToolMessage
6
- from langchain_core.tools import tool
7
- from langgraph.graph import StateGraph
8
- from langgraph.types import Command
9
- from universal_mcp.logger import logger
10
- from universal_mcp.tools.registry import ToolRegistry
11
- from universal_mcp.types import ToolFormat
12
-
13
- from universal_mcp.agents.bigtool.state import State
14
-
15
- from .prompts import SELECT_TOOL_PROMPT
16
-
17
-
18
- def build_graph(
19
- tool_registry: ToolRegistry,
20
- llm: BaseChatModel,
21
- system_prompt: str,
22
- ):
23
- @tool
24
- async def retrieve_tools(task_query: str) -> list[str]:
25
- """Retrieve tools for a given task.
26
- Task query should be atomic (doable with a single tool).
27
- For tasks requiring multiple tools, call this tool multiple times for each subtask."""
28
- logger.info(f"Retrieving tools for task: '{task_query}'")
29
- try:
30
- tools_list = await tool_registry.search_tools(task_query, limit=10)
31
- tool_candidates = [
32
- f"{tool['id']}: {tool['description']}" for tool in tools_list
33
- ]
34
- logger.info(f"Found {len(tool_candidates)} candidate tools.")
35
-
36
- class ToolSelectionOutput(TypedDict):
37
- tool_names: list[str]
38
-
39
- model = llm
40
- app_ids = await tool_registry.list_all_apps()
41
- connections = await tool_registry.list_connected_apps()
42
- connection_ids = set([connection["app_id"] for connection in connections])
43
- connected_apps = [
44
- app["id"] for app in app_ids if app["id"] in connection_ids
45
- ]
46
- unconnected_apps = [
47
- app["id"] for app in app_ids if app["id"] not in connection_ids
48
- ]
49
- app_id_descriptions = (
50
- "These are the apps connected to the user's account:\n"
51
- + "\n".join([f"{app}" for app in connected_apps])
52
- )
53
- if unconnected_apps:
54
- app_id_descriptions += "\n\nOther (not connected) apps: " + "\n".join(
55
- [f"{app}" for app in unconnected_apps]
56
- )
57
-
58
- response = await model.with_structured_output(
59
- schema=ToolSelectionOutput, method="json_mode"
60
- ).ainvoke(
61
- SELECT_TOOL_PROMPT.format(
62
- app_ids=app_id_descriptions,
63
- tool_candidates="\n - ".join(tool_candidates),
64
- task=task_query,
65
- )
66
- )
67
-
68
- selected_tool_names = cast(ToolSelectionOutput, response)["tool_names"]
69
- logger.info(f"Selected tools: {selected_tool_names}")
70
- return selected_tool_names
71
- except Exception as e:
72
- logger.error(f"Error retrieving tools: {e}")
73
- return []
74
-
75
-
76
- async def call_model(
77
- state: State
78
- ) -> Command[Literal["select_tools", "call_tools"]]:
79
- logger.info("Calling model...")
80
- try:
81
- messages = [
82
- {"role": "system", "content": system_prompt},
83
- *state["messages"],
84
- ]
85
-
86
- logger.info(f"Selected tool IDs: {state['selected_tool_ids']}")
87
- if len(state["selected_tool_ids"]) > 0:
88
- selected_tools = await tool_registry.export_tools(
89
- tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN
90
- )
91
- logger.info(f"Exported {len(selected_tools)} tools for model.")
92
- else:
93
- selected_tools = []
94
-
95
- model_with_tools = llm.bind_tools(
96
- [retrieve_tools, *selected_tools], tool_choice="auto"
97
- )
98
-
99
-
100
- response = await model_with_tools.ainvoke(messages)
101
- cast(AIMessage, response)
102
- logger.debug(f"Response: {response}")
103
-
104
-
105
- if response.tool_calls:
106
- logger.info(
107
- f"Model responded with {len(response.tool_calls)} tool calls."
108
- )
109
- if len(response.tool_calls) > 1:
110
- raise Exception(
111
- "Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')"
112
- )
113
- tool_call = response.tool_calls[0]
114
- if tool_call["name"] == retrieve_tools.name:
115
- logger.info("Model requested to select tools.")
116
- return Command(goto="select_tools", update={"messages": [response]})
117
- elif tool_call["name"] not in state["selected_tool_ids"]:
118
- try:
119
- await tool_registry.export_tools(
120
- [tool_call["name"]], ToolFormat.LANGCHAIN
121
- )
122
- logger.info(
123
- f"Tool '{tool_call['name']}' not in selected tools, but available. Proceeding to call."
124
- )
125
- return Command(
126
- goto="call_tools", update={"messages": [response]}
127
- )
128
- except Exception as e:
129
- logger.error(
130
- f"Unexpected tool call: {tool_call['name']}. Error: {e}"
131
- )
132
- raise Exception(
133
- f"Unexpected tool call: {tool_call['name']}. Available tools: {state['selected_tool_ids']}"
134
- ) from e
135
- logger.info(f"Proceeding to call tool: {tool_call['name']}")
136
- return Command(goto="call_tools", update={"messages": [response]})
137
- else:
138
- logger.info("Model responded with a message, ending execution.")
139
- return Command(update={"messages": [response]})
140
- except Exception as e:
141
- logger.error(f"Error in call_model: {e}")
142
- raise
143
-
144
- async def select_tools(
145
- state: State
146
- ) -> Command[Literal["call_model"]]:
147
- logger.info("Selecting tools...")
148
- try:
149
- tool_call = state["messages"][-1].tool_calls[0]
150
- selected_tool_names = await retrieve_tools.ainvoke(input=tool_call["args"])
151
- tool_msg = ToolMessage(
152
- f"Available tools: {selected_tool_names}", tool_call_id=tool_call["id"]
153
- )
154
- logger.info(f"Tools selected: {selected_tool_names}")
155
- return Command(
156
- goto="call_model",
157
- update={
158
- "messages": [tool_msg],
159
- "selected_tool_ids": selected_tool_names,
160
- },
161
- )
162
- except Exception as e:
163
- logger.error(f"Error in select_tools: {e}")
164
- raise
165
-
166
- async def call_tools(state: State) -> Command[Literal["call_model"]]:
167
- logger.info("Calling tools...")
168
- outputs = []
169
- recent_tool_ids = []
170
- for tool_call in state["messages"][-1].tool_calls:
171
- logger.info(
172
- f"Executing tool: {tool_call['name']} with args: {tool_call['args']}"
173
- )
174
- try:
175
- await tool_registry.export_tools(
176
- [tool_call["name"]], ToolFormat.LANGCHAIN
177
- )
178
- tool_result = await tool_registry.call_tool(
179
- tool_call["name"], tool_call["args"]
180
- )
181
- logger.info(f"Tool '{tool_call['name']}' executed successfully.")
182
- outputs.append(
183
- ToolMessage(
184
- content=json.dumps(tool_result),
185
- name=tool_call["name"],
186
- tool_call_id=tool_call["id"],
187
- )
188
- )
189
- recent_tool_ids.append(tool_call["name"])
190
- except Exception as e:
191
- logger.error(f"Error executing tool '{tool_call['name']}': {e}")
192
- outputs.append(
193
- ToolMessage(
194
- content=json.dumps("Error: " + str(e)),
195
- name=tool_call["name"],
196
- tool_call_id=tool_call["id"],
197
- )
198
- )
199
- return Command(
200
- goto="call_model",
201
- update={"messages": outputs, "selected_tool_ids": recent_tool_ids},
202
- )
203
-
204
- builder = StateGraph(State)
205
-
206
- builder.add_node(call_model)
207
- builder.add_node(select_tools)
208
- builder.add_node(call_tools)
209
- builder.set_entry_point("call_model")
210
- return builder
@@ -1,31 +0,0 @@
1
- """Default prompts used by the agent."""
2
-
3
- SYSTEM_PROMPT = """You are {name}, a helpful AI assistant.
4
-
5
- **Core Directives:**
6
- 1. **Always Use Tools for Tasks:** For any user request that requires an action (e.g., sending an email, searching for information, creating an event), you MUST use a tool. Do not answer from your own knowledge or refuse a task if a tool might exist for it.
7
- 2. **First Step is ALWAYS `retrieve_tools`:** Before you can use any other tool, you MUST first call the `retrieve_tools` function to find the right tool for the user's request. This is your mandatory first action.
8
- 3. **Strictly Follow the Process:** Your only job in your first turn is to analyze the user's request and call `retrieve_tools` with a concise query describing the core task. Do not engage in conversation.
9
-
10
- When multiple tools are available for the same task, you must ask the user.
11
-
12
- {instructions}
13
- """
14
-
15
- SELECT_TOOL_PROMPT = """You are an AI assistant that helps the user perform tasks using various apps (each app has multiple tools).
16
- You will be provided with a task and a list of tools which might be relevant for this task.
17
-
18
- Your goal is to select the most appropriate tool for the given task.
19
- <task>
20
- {task}
21
- </task>
22
-
23
- These are the list of apps available to you:
24
- {app_ids}
25
- Note that when multiple apps seem relevant for a task, prefer connected apps over unconnected apps while breaking a tie. If more than one relevant app (or none of the relevant apps) are connected, you must choose both apps tools. In case the user specifically asks you to use an app that is not connected, select the tool.
26
-
27
- <tool_candidates>
28
- - {tool_candidates}
29
- </tool_candidates>
30
-
31
- """
@@ -1,27 +0,0 @@
1
- from typing import Annotated
2
-
3
- from langgraph.prebuilt.chat_agent_executor import AgentState
4
-
5
-
6
- def _enqueue(left: list, right: list) -> list:
7
- """Treat left as a FIFO queue, append new items from right (preserve order),
8
- keep items unique, and cap total size to 20 (drop oldest items)."""
9
- max_size = 30
10
- preferred_size = 20
11
- if len(right) > preferred_size:
12
- preferred_size = min(max_size, len(right))
13
- queue = list(left or [])
14
-
15
- for item in right[:preferred_size] or []:
16
- if item in queue:
17
- queue.remove(item)
18
- queue.append(item)
19
-
20
- if len(queue) > preferred_size:
21
- queue = queue[-preferred_size:]
22
-
23
- return queue
24
-
25
-
26
- class State(AgentState):
27
- selected_tool_ids: Annotated[list[str], _enqueue]