universal-mcp-agents 0.1.19rc1__py3-none-any.whl → 0.1.24rc3__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.
- universal_mcp/agents/__init__.py +15 -16
- universal_mcp/agents/base.py +46 -35
- universal_mcp/agents/bigtool/state.py +1 -1
- universal_mcp/agents/cli.py +2 -5
- universal_mcp/agents/codeact0/__init__.py +2 -3
- universal_mcp/agents/codeact0/__main__.py +4 -7
- universal_mcp/agents/codeact0/agent.py +444 -96
- universal_mcp/agents/codeact0/langgraph_agent.py +1 -1
- universal_mcp/agents/codeact0/llm_tool.py +2 -254
- universal_mcp/agents/codeact0/prompts.py +247 -137
- universal_mcp/agents/codeact0/sandbox.py +52 -18
- universal_mcp/agents/codeact0/state.py +26 -6
- universal_mcp/agents/codeact0/tools.py +400 -74
- universal_mcp/agents/codeact0/utils.py +175 -11
- universal_mcp/agents/codeact00/__init__.py +3 -0
- universal_mcp/agents/{unified → codeact00}/__main__.py +4 -6
- universal_mcp/agents/codeact00/agent.py +578 -0
- universal_mcp/agents/codeact00/config.py +77 -0
- universal_mcp/agents/{unified → codeact00}/langgraph_agent.py +2 -2
- universal_mcp/agents/{unified → codeact00}/llm_tool.py +1 -1
- universal_mcp/agents/codeact00/prompts.py +364 -0
- universal_mcp/agents/{unified → codeact00}/sandbox.py +52 -18
- universal_mcp/agents/codeact00/state.py +66 -0
- universal_mcp/agents/codeact00/tools.py +525 -0
- universal_mcp/agents/codeact00/utils.py +678 -0
- universal_mcp/agents/codeact01/__init__.py +3 -0
- universal_mcp/agents/{codeact → codeact01}/__main__.py +4 -11
- universal_mcp/agents/codeact01/agent.py +413 -0
- universal_mcp/agents/codeact01/config.py +77 -0
- universal_mcp/agents/codeact01/langgraph_agent.py +14 -0
- universal_mcp/agents/codeact01/llm_tool.py +25 -0
- universal_mcp/agents/codeact01/prompts.py +246 -0
- universal_mcp/agents/codeact01/sandbox.py +162 -0
- universal_mcp/agents/{unified → codeact01}/state.py +26 -10
- universal_mcp/agents/codeact01/tools.py +648 -0
- universal_mcp/agents/{unified → codeact01}/utils.py +175 -11
- universal_mcp/agents/llm.py +14 -4
- universal_mcp/agents/react.py +3 -3
- universal_mcp/agents/sandbox.py +124 -69
- universal_mcp/applications/llm/app.py +76 -24
- {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/METADATA +6 -5
- universal_mcp_agents-0.1.24rc3.dist-info/RECORD +66 -0
- universal_mcp/agents/codeact/__init__.py +0 -3
- universal_mcp/agents/codeact/agent.py +0 -240
- universal_mcp/agents/codeact/models.py +0 -11
- universal_mcp/agents/codeact/prompts.py +0 -82
- universal_mcp/agents/codeact/sandbox.py +0 -85
- universal_mcp/agents/codeact/state.py +0 -11
- universal_mcp/agents/codeact/utils.py +0 -68
- universal_mcp/agents/codeact0/playbook_agent.py +0 -355
- universal_mcp/agents/unified/README.md +0 -45
- universal_mcp/agents/unified/__init__.py +0 -3
- universal_mcp/agents/unified/agent.py +0 -289
- universal_mcp/agents/unified/prompts.py +0 -192
- universal_mcp/agents/unified/tools.py +0 -188
- universal_mcp_agents-0.1.19rc1.dist-info/RECORD +0 -64
- {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/WHEEL +0 -0
|
@@ -1,37 +1,46 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import uuid
|
|
3
5
|
from typing import Literal, cast
|
|
4
6
|
|
|
5
|
-
from
|
|
6
|
-
from langchain_core.
|
|
7
|
-
from langchain_core.tools import tool as create_tool
|
|
7
|
+
from langchain_anthropic import ChatAnthropic
|
|
8
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
8
9
|
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
9
10
|
from langgraph.graph import START, StateGraph
|
|
10
|
-
from langgraph.types import Command, RetryPolicy
|
|
11
|
+
from langgraph.types import Command, RetryPolicy, StreamWriter
|
|
11
12
|
from universal_mcp.tools.registry import ToolRegistry
|
|
12
|
-
from universal_mcp.types import
|
|
13
|
+
from universal_mcp.types import ToolFormat
|
|
13
14
|
|
|
14
15
|
from universal_mcp.agents.base import BaseAgent
|
|
15
|
-
from universal_mcp.agents.codeact0.llm_tool import
|
|
16
|
+
from universal_mcp.agents.codeact0.llm_tool import smart_print
|
|
16
17
|
from universal_mcp.agents.codeact0.prompts import (
|
|
18
|
+
AGENT_BUILDER_GENERATING_PROMPT,
|
|
19
|
+
AGENT_BUILDER_META_PROMPT,
|
|
20
|
+
AGENT_BUILDER_PLANNING_PROMPT,
|
|
21
|
+
build_tool_definitions,
|
|
17
22
|
create_default_prompt,
|
|
18
23
|
)
|
|
19
|
-
from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
|
|
20
|
-
from universal_mcp.agents.codeact0.state import CodeActState
|
|
21
|
-
from universal_mcp.agents.codeact0.
|
|
24
|
+
from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
|
|
25
|
+
from universal_mcp.agents.codeact0.state import AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan, CodeActState
|
|
26
|
+
from universal_mcp.agents.codeact0.tools import (
|
|
27
|
+
create_meta_tools,
|
|
28
|
+
enter_agent_builder_mode,
|
|
29
|
+
)
|
|
30
|
+
from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string, strip_thinking, extract_plan_parameters
|
|
22
31
|
from universal_mcp.agents.llm import load_chat_model
|
|
23
|
-
from universal_mcp.agents.utils import filter_retry_on
|
|
32
|
+
from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
|
|
24
33
|
|
|
25
34
|
|
|
26
|
-
class
|
|
35
|
+
class CodeActPlaybookAgent(BaseAgent):
|
|
27
36
|
def __init__(
|
|
28
37
|
self,
|
|
29
38
|
name: str,
|
|
30
39
|
instructions: str,
|
|
31
40
|
model: str,
|
|
32
41
|
memory: BaseCheckpointSaver | None = None,
|
|
33
|
-
tools: ToolConfig | None = None,
|
|
34
42
|
registry: ToolRegistry | None = None,
|
|
43
|
+
agent_builder_registry: object | None = None,
|
|
35
44
|
sandbox_timeout: int = 20,
|
|
36
45
|
**kwargs,
|
|
37
46
|
):
|
|
@@ -42,103 +51,442 @@ class CodeActAgent(BaseAgent):
|
|
|
42
51
|
memory=memory,
|
|
43
52
|
**kwargs,
|
|
44
53
|
)
|
|
45
|
-
self.model_instance = load_chat_model(model
|
|
46
|
-
self.
|
|
54
|
+
self.model_instance = load_chat_model(model)
|
|
55
|
+
self.agent_builder_model_instance = load_chat_model("anthropic:claude-sonnet-4-5-20250929", thinking=False)
|
|
47
56
|
self.registry = registry
|
|
57
|
+
self.agent_builder_registry = agent_builder_registry
|
|
58
|
+
self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
|
|
59
|
+
|
|
60
|
+
self.tools_config = self.agent.tools if self.agent else {}
|
|
48
61
|
self.eval_fn = eval_unsafe
|
|
49
62
|
self.sandbox_timeout = sandbox_timeout
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
self.default_tools_config = {
|
|
64
|
+
"llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
|
|
65
|
+
}
|
|
66
|
+
self.final_instructions = ""
|
|
67
|
+
self.tools_context = {}
|
|
68
|
+
self.eval_mode = kwargs.get("eval_mode", False)
|
|
69
|
+
|
|
70
|
+
async def _build_graph(self): # noqa: PLR0915
|
|
71
|
+
"""Build the graph for the CodeAct Playbook Agent."""
|
|
72
|
+
meta_tools = create_meta_tools(self.registry)
|
|
73
|
+
self.additional_tools = [
|
|
74
|
+
smart_print,
|
|
75
|
+
meta_tools["web_search"],
|
|
76
|
+
meta_tools["read_file"],
|
|
77
|
+
meta_tools["save_file"],
|
|
78
|
+
meta_tools["upload_file"],
|
|
79
|
+
]
|
|
80
|
+
|
|
62
81
|
if self.tools_config:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
|
|
67
|
-
additional_tools = [smart_print, data_extractor, ai_classify, call_llm]
|
|
68
|
-
additional_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in additional_tools]
|
|
69
|
-
self.instructions, self.tools_context = create_default_prompt(
|
|
70
|
-
exported_tools, additional_tools, self.instructions
|
|
71
|
-
)
|
|
82
|
+
await self.registry.load_tools(self.tools_config) # Load provided tools
|
|
83
|
+
if self.default_tools_config:
|
|
84
|
+
await self.registry.load_tools(self.default_tools_config) # Load default tools
|
|
72
85
|
|
|
73
|
-
def call_model(state: CodeActState) -> Command[Literal["
|
|
74
|
-
|
|
86
|
+
async def call_model(state: CodeActState) -> Command[Literal["execute_tools"]]:
|
|
87
|
+
"""This node now only ever binds the four meta-tools to the LLM."""
|
|
88
|
+
messages = build_anthropic_cache_message(self.final_instructions) + state["messages"]
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
agent_facing_tools = [
|
|
91
|
+
execute_ipython_cell,
|
|
92
|
+
enter_agent_builder_mode,
|
|
93
|
+
meta_tools["search_functions"],
|
|
94
|
+
meta_tools["load_functions"],
|
|
95
|
+
]
|
|
79
96
|
|
|
97
|
+
if isinstance(self.model_instance, ChatAnthropic):
|
|
98
|
+
model_with_tools = self.model_instance.bind_tools(
|
|
99
|
+
tools=agent_facing_tools,
|
|
100
|
+
tool_choice="auto",
|
|
101
|
+
cache_control={"type": "ephemeral", "ttl": "1h"},
|
|
102
|
+
)
|
|
103
|
+
if isinstance(messages[-1].content, str):
|
|
104
|
+
pass
|
|
105
|
+
else:
|
|
106
|
+
last = copy.deepcopy(messages[-1])
|
|
107
|
+
last.content[-1]["cache_control"] = {"type": "ephemeral", "ttl": "5m"}
|
|
108
|
+
messages[-1] = last
|
|
109
|
+
else:
|
|
110
|
+
model_with_tools = self.model_instance.bind_tools(
|
|
111
|
+
tools=agent_facing_tools,
|
|
112
|
+
tool_choice="auto",
|
|
113
|
+
)
|
|
114
|
+
response = cast(AIMessage, await model_with_tools.ainvoke(messages))
|
|
80
115
|
if response.tool_calls:
|
|
81
|
-
|
|
82
|
-
raise Exception("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
|
|
83
|
-
if response.tool_calls[0]["name"] != "execute_ipython_cell":
|
|
84
|
-
raise Exception(
|
|
85
|
-
f"Unexpected tool call: {response.tool_calls[0]['name']}. Expected 'execute_ipython_cell'."
|
|
86
|
-
)
|
|
87
|
-
if (
|
|
88
|
-
response.tool_calls[0]["args"].get("snippet") is None
|
|
89
|
-
or not response.tool_calls[0]["args"]["snippet"].strip()
|
|
90
|
-
):
|
|
91
|
-
raise Exception("Tool call 'execute_ipython_cell' requires a non-empty 'snippet' argument.")
|
|
92
|
-
return Command(goto="sandbox", update={"messages": [response]})
|
|
116
|
+
return Command(goto="execute_tools", update={"messages": [response]})
|
|
93
117
|
else:
|
|
94
|
-
return Command(update={"messages": [response]})
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
return Command(update={"messages": [response], "model_with_tools": model_with_tools})
|
|
119
|
+
|
|
120
|
+
async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "agent_builder"]]:
|
|
121
|
+
"""Execute tool calls"""
|
|
122
|
+
last_message = state["messages"][-1]
|
|
123
|
+
tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
|
|
124
|
+
|
|
125
|
+
tool_messages = []
|
|
126
|
+
new_tool_ids = []
|
|
127
|
+
tool_result = ""
|
|
128
|
+
ask_user = False
|
|
129
|
+
ai_msg = ""
|
|
130
|
+
effective_previous_add_context = state.get("add_context", {})
|
|
131
|
+
effective_existing_context = state.get("context", {})
|
|
132
|
+
# logging.info(f"Initial new_tool_ids_for_context: {new_tool_ids_for_context}")
|
|
133
|
+
|
|
134
|
+
for tool_call in tool_calls:
|
|
135
|
+
tool_name = tool_call["name"]
|
|
136
|
+
tool_args = tool_call["args"]
|
|
137
|
+
try:
|
|
138
|
+
if tool_name == "enter_agent_builder_mode":
|
|
139
|
+
tool_message = ToolMessage(
|
|
140
|
+
content=json.dumps("Entered Agent Builder Mode."),
|
|
141
|
+
name=tool_call["name"],
|
|
142
|
+
tool_call_id=tool_call["id"],
|
|
143
|
+
)
|
|
144
|
+
return Command(
|
|
145
|
+
goto="agent_builder",
|
|
146
|
+
update={
|
|
147
|
+
"agent_builder_mode": "planning",
|
|
148
|
+
"messages": [tool_message],
|
|
149
|
+
}, # Entered Agent Builder mode
|
|
150
|
+
)
|
|
151
|
+
elif tool_name == "execute_ipython_cell":
|
|
152
|
+
code = tool_call["args"]["snippet"]
|
|
153
|
+
output, new_context, new_add_context = await handle_execute_ipython_cell(
|
|
154
|
+
code,
|
|
155
|
+
self.tools_context, # Uses the dynamically updated context
|
|
156
|
+
self.eval_fn,
|
|
157
|
+
effective_previous_add_context,
|
|
158
|
+
effective_existing_context,
|
|
159
|
+
)
|
|
160
|
+
effective_existing_context = new_context
|
|
161
|
+
effective_previous_add_context = new_add_context
|
|
162
|
+
tool_result = output
|
|
163
|
+
elif tool_name == "load_functions":
|
|
164
|
+
# The tool now does all the work of validation and formatting.
|
|
165
|
+
tool_result, new_context_for_sandbox, valid_tools, unconnected_links = await meta_tools[
|
|
166
|
+
"load_functions"
|
|
167
|
+
].ainvoke(tool_args)
|
|
168
|
+
# We still need to update the sandbox context for `execute_ipython_cell`
|
|
169
|
+
new_tool_ids.extend(valid_tools)
|
|
170
|
+
if new_tool_ids:
|
|
171
|
+
self.tools_context.update(new_context_for_sandbox)
|
|
172
|
+
if unconnected_links:
|
|
173
|
+
ask_user = True
|
|
174
|
+
ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {unconnected_links} "
|
|
175
|
+
|
|
176
|
+
elif tool_name == "search_functions":
|
|
177
|
+
tool_result = await meta_tools["search_functions"].ainvoke(tool_args)
|
|
178
|
+
else:
|
|
179
|
+
raise Exception(
|
|
180
|
+
f"Unexpected tool call: {tool_call['name']}. "
|
|
181
|
+
"tool calls must be one of 'enter_agent_builder_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'. For using functions, call them in code using 'execute_ipython_cell'."
|
|
182
|
+
)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
tool_result = str(e)
|
|
185
|
+
|
|
186
|
+
tool_message = ToolMessage(
|
|
187
|
+
content=json.dumps(tool_result),
|
|
188
|
+
name=tool_call["name"],
|
|
189
|
+
tool_call_id=tool_call["id"],
|
|
190
|
+
)
|
|
191
|
+
tool_messages.append(tool_message)
|
|
192
|
+
|
|
193
|
+
if ask_user:
|
|
194
|
+
tool_messages.append(AIMessage(content=ai_msg))
|
|
195
|
+
return Command(
|
|
196
|
+
update={
|
|
197
|
+
"messages": tool_messages,
|
|
198
|
+
"selected_tool_ids": new_tool_ids,
|
|
199
|
+
"context": effective_existing_context,
|
|
200
|
+
"add_context": effective_previous_add_context,
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return Command(
|
|
205
|
+
goto="call_model",
|
|
206
|
+
update={
|
|
207
|
+
"messages": tool_messages,
|
|
208
|
+
"selected_tool_ids": new_tool_ids,
|
|
209
|
+
"context": effective_existing_context,
|
|
210
|
+
"add_context": effective_previous_add_context,
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def agent_builder(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
|
|
215
|
+
agent_builder_mode = state.get("agent_builder_mode")
|
|
216
|
+
if agent_builder_mode == "planning":
|
|
217
|
+
plan_id = str(uuid.uuid4())
|
|
218
|
+
writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": bool(self.agent)}})
|
|
219
|
+
planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT + self.preloaded_defs
|
|
220
|
+
messages = [{"role": "system", "content": planning_instructions}] + strip_thinking(state["messages"])
|
|
221
|
+
|
|
222
|
+
model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
|
|
223
|
+
AgentBuilderPlan
|
|
224
|
+
)
|
|
225
|
+
response = await model_with_structured_output.ainvoke(messages)
|
|
226
|
+
plan = cast(AgentBuilderPlan, response)
|
|
227
|
+
|
|
228
|
+
writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
|
|
229
|
+
ai_msg = AIMessage(
|
|
230
|
+
content=json.dumps(plan.model_dump()),
|
|
231
|
+
additional_kwargs={
|
|
232
|
+
"type": "planning",
|
|
233
|
+
"plan": plan.steps,
|
|
234
|
+
"update": bool(self.agent),
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if self.eval_mode:
|
|
239
|
+
mock_user_message = HumanMessage(content="yes, this is great")
|
|
240
|
+
return Command(
|
|
241
|
+
goto="agent_builder",
|
|
242
|
+
update={
|
|
243
|
+
"messages": [ai_msg, mock_user_message],
|
|
244
|
+
"agent_builder_mode": "generating",
|
|
245
|
+
"plan": plan.steps,
|
|
246
|
+
},
|
|
247
|
+
)
|
|
124
248
|
|
|
125
249
|
return Command(
|
|
126
|
-
goto="call_model",
|
|
127
250
|
update={
|
|
128
|
-
"messages": [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
251
|
+
"messages": [ai_msg],
|
|
252
|
+
"agent_builder_mode": "confirming",
|
|
253
|
+
"plan": plan.steps,
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
elif agent_builder_mode == "confirming":
|
|
258
|
+
# Deterministic routing based on three exact button inputs from UI
|
|
259
|
+
user_text = ""
|
|
260
|
+
for m in reversed(state["messages"]):
|
|
261
|
+
try:
|
|
262
|
+
if getattr(m, "type", "") in {"human", "user"}:
|
|
263
|
+
user_text = (get_message_text(m) or "").strip()
|
|
264
|
+
if user_text:
|
|
265
|
+
break
|
|
266
|
+
except Exception:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
t = user_text.lower()
|
|
270
|
+
if t == "yes, this is great":
|
|
271
|
+
self.meta_id = str(uuid.uuid4())
|
|
272
|
+
name, description = None, None
|
|
273
|
+
if self.agent:
|
|
274
|
+
# Update flow: use existing name/description and do not re-generate
|
|
275
|
+
name = getattr(self.agent, "name", None)
|
|
276
|
+
description = getattr(self.agent, "description", None)
|
|
277
|
+
writer(
|
|
278
|
+
{
|
|
279
|
+
"type": "custom",
|
|
280
|
+
id: self.meta_id,
|
|
281
|
+
"name": "generating",
|
|
282
|
+
"data": {
|
|
283
|
+
"update": True,
|
|
284
|
+
"name": name,
|
|
285
|
+
"description": description,
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
writer({"type": "custom", id: self.meta_id, "name": "generating", "data": {"update": False}})
|
|
291
|
+
|
|
292
|
+
meta_instructions = self.instructions + AGENT_BUILDER_META_PROMPT
|
|
293
|
+
messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
|
|
294
|
+
|
|
295
|
+
model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
|
|
296
|
+
AgentBuilderMeta
|
|
297
|
+
)
|
|
298
|
+
meta_response = await model_with_structured_output.ainvoke(messages)
|
|
299
|
+
meta = cast(AgentBuilderMeta, meta_response)
|
|
300
|
+
name, description = meta.name, meta.description
|
|
301
|
+
|
|
302
|
+
# Emit intermediary UI update with created name/description
|
|
303
|
+
writer(
|
|
304
|
+
{
|
|
305
|
+
"type": "custom",
|
|
306
|
+
id: self.meta_id,
|
|
307
|
+
"name": "generating",
|
|
308
|
+
"data": {"update": False, "name": name, "description": description},
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return Command(
|
|
313
|
+
goto="agent_builder",
|
|
314
|
+
update={
|
|
315
|
+
"agent_builder_mode": "generating",
|
|
316
|
+
"agent_name": name,
|
|
317
|
+
"agent_description": description,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
if t == "i would like to modify the plan":
|
|
321
|
+
prompt_ai = AIMessage(
|
|
322
|
+
content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.",
|
|
323
|
+
additional_kwargs={"stream": "true"},
|
|
324
|
+
)
|
|
325
|
+
return Command(update={"agent_builder_mode": "planning", "messages": [prompt_ai]})
|
|
326
|
+
if t == "let's do something else":
|
|
327
|
+
return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
|
|
328
|
+
|
|
329
|
+
# Fallback safe default
|
|
330
|
+
return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
|
|
331
|
+
|
|
332
|
+
elif agent_builder_mode == "generating":
|
|
333
|
+
generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT + self.preloaded_defs
|
|
334
|
+
messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
|
|
335
|
+
|
|
336
|
+
model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
|
|
337
|
+
AgentBuilderCode
|
|
338
|
+
)
|
|
339
|
+
response = await model_with_structured_output.ainvoke(messages)
|
|
340
|
+
func_code = cast(AgentBuilderCode, response).code
|
|
341
|
+
|
|
342
|
+
# Extract function name (handle both regular and async functions)
|
|
343
|
+
match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
|
|
344
|
+
if match:
|
|
345
|
+
function_name = match.group(1)
|
|
346
|
+
else:
|
|
347
|
+
function_name = "generated_agent"
|
|
348
|
+
|
|
349
|
+
# Use generated metadata if available
|
|
350
|
+
final_name = state.get("agent_name") or function_name
|
|
351
|
+
final_description = state.get("agent_description") or f"Generated agent: {function_name}"
|
|
352
|
+
add_context = state.get("add_context", {})
|
|
353
|
+
if "functions" not in add_context:
|
|
354
|
+
add_context["functions"] = []
|
|
355
|
+
add_context["functions"].append(func_code)
|
|
356
|
+
|
|
357
|
+
# Save or update an Agent using the helper registry
|
|
358
|
+
try:
|
|
359
|
+
if not self.agent_builder_registry:
|
|
360
|
+
raise ValueError("AgentBuilder registry is not configured")
|
|
361
|
+
|
|
362
|
+
plan_params = extract_plan_parameters(state["plan"])
|
|
363
|
+
|
|
364
|
+
# Build instructions payload embedding the plan and function code
|
|
365
|
+
instructions_payload = {
|
|
366
|
+
"plan": state["plan"],
|
|
367
|
+
"script": func_code,
|
|
368
|
+
"params": plan_params,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Convert tool ids list to dict
|
|
372
|
+
tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
|
|
373
|
+
|
|
374
|
+
res = self.agent_builder_registry.upsert_agent(
|
|
375
|
+
name=final_name,
|
|
376
|
+
description=final_description,
|
|
377
|
+
instructions=instructions_payload,
|
|
378
|
+
tools=tool_dict,
|
|
379
|
+
)
|
|
380
|
+
except Exception:
|
|
381
|
+
# In case of error, add the code to the exit message content
|
|
382
|
+
|
|
383
|
+
mock_exit_tool_call = {"name": "exit_agent_builder_mode", "args": {}, "id": "exit_builder_1"}
|
|
384
|
+
|
|
385
|
+
# Create a minimal assistant message to maintain flow
|
|
386
|
+
mock_assistant_message = AIMessage(
|
|
387
|
+
content=json.dumps(response.model_dump()),
|
|
388
|
+
tool_calls=[mock_exit_tool_call],
|
|
389
|
+
additional_kwargs={
|
|
390
|
+
"type": "generating",
|
|
391
|
+
"id": "ignore",
|
|
392
|
+
"update": bool(self.agent),
|
|
393
|
+
"name": final_name.replace(" ", "_"),
|
|
394
|
+
"description": final_description,
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
mock_exit_tool_response = ToolMessage(
|
|
398
|
+
content=json.dumps(
|
|
399
|
+
f"An error occurred. Displaying the function code:\n\n{func_code}\nFinal Name: {final_name}\nDescription: {final_description}"
|
|
400
|
+
),
|
|
401
|
+
name="exit_agent_builder_mode",
|
|
402
|
+
tool_call_id="exit_builder_1",
|
|
403
|
+
)
|
|
404
|
+
if self.eval_mode:
|
|
405
|
+
human_msg = HumanMessage(content="Call the generated agent function (without redeclaring it) and check whether it works as expected")
|
|
406
|
+
return Command(goto="call_model", update={"messages": [mock_assistant_message, mock_exit_tool_response, human_msg], "agent_builder_mode": "normal", "add_context": add_context})
|
|
407
|
+
else:
|
|
408
|
+
return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "agent_builder_mode": "normal", "add_context": add_context})
|
|
409
|
+
|
|
410
|
+
writer(
|
|
411
|
+
{
|
|
412
|
+
"type": "custom",
|
|
413
|
+
id: self.meta_id,
|
|
414
|
+
"name": "generating",
|
|
415
|
+
"data": {
|
|
416
|
+
"id": str(res.id),
|
|
417
|
+
"update": bool(self.agent),
|
|
418
|
+
"name": final_name,
|
|
419
|
+
"description": final_description,
|
|
420
|
+
"add_context": add_context,
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
mock_exit_tool_call = {"name": "exit_agent_builder_mode", "args": {}, "id": "exit_builder_1"}
|
|
425
|
+
mock_assistant_message = AIMessage(
|
|
426
|
+
content=json.dumps(response.model_dump()),
|
|
427
|
+
tool_calls=[mock_exit_tool_call],
|
|
428
|
+
additional_kwargs={
|
|
429
|
+
"type": "generating",
|
|
430
|
+
"id": str(res.id),
|
|
431
|
+
"update": bool(self.agent),
|
|
432
|
+
"name": final_name.replace(" ", "_"),
|
|
433
|
+
"description": final_description,
|
|
137
434
|
},
|
|
138
435
|
)
|
|
139
436
|
|
|
437
|
+
mock_exit_tool_response = ToolMessage(
|
|
438
|
+
content=json.dumps(
|
|
439
|
+
"Exited Agent Builder Mode. Enter this mode again if you need to modify the saved agent."
|
|
440
|
+
),
|
|
441
|
+
name="exit_agent_builder_mode",
|
|
442
|
+
tool_call_id="exit_builder_1",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return Command(
|
|
446
|
+
update={
|
|
447
|
+
"messages": [mock_assistant_message, mock_exit_tool_response],
|
|
448
|
+
"agent_builder_mode": "normal",
|
|
449
|
+
"add_context": add_context,
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
async def route_entry(state: CodeActState) -> Command[Literal["call_model", "agent_builder", "execute_tools"]]:
|
|
454
|
+
"""Route to either normal mode or agent builder creation"""
|
|
455
|
+
pre_tools = await self.registry.export_tools(format=ToolFormat.NATIVE)
|
|
456
|
+
|
|
457
|
+
# Create the initial system prompt and tools_context in one go
|
|
458
|
+
self.final_instructions, self.tools_context = create_default_prompt(
|
|
459
|
+
pre_tools,
|
|
460
|
+
self.additional_tools,
|
|
461
|
+
self.instructions,
|
|
462
|
+
await get_connected_apps_string(self.registry),
|
|
463
|
+
self.agent,
|
|
464
|
+
is_initial_prompt=True,
|
|
465
|
+
)
|
|
466
|
+
self.preloaded_defs, _ = build_tool_definitions(pre_tools)
|
|
467
|
+
self.preloaded_defs = "\n".join(self.preloaded_defs)
|
|
468
|
+
await self.registry.load_tools(state["selected_tool_ids"])
|
|
469
|
+
exported_tools = await self.registry.export_tools(
|
|
470
|
+
state["selected_tool_ids"], ToolFormat.NATIVE
|
|
471
|
+
) # Get definition for only the new tools
|
|
472
|
+
_, loaded_tools_context = build_tool_definitions(exported_tools)
|
|
473
|
+
self.tools_context.update(loaded_tools_context)
|
|
474
|
+
|
|
475
|
+
if (
|
|
476
|
+
len(state["messages"]) == 1 and self.agent
|
|
477
|
+
): # Inject the agent's script function into add_context for execution
|
|
478
|
+
script = self.agent.instructions.get("script")
|
|
479
|
+
add_context = {"functions": [script]}
|
|
480
|
+
return Command(goto="call_model", update={"add_context": add_context})
|
|
481
|
+
|
|
482
|
+
if state.get("agent_builder_mode") in ["planning", "confirming", "generating"]:
|
|
483
|
+
return Command(goto="agent_builder")
|
|
484
|
+
return Command(goto="call_model")
|
|
485
|
+
|
|
140
486
|
agent = StateGraph(state_schema=CodeActState)
|
|
141
487
|
agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
|
|
142
|
-
agent.add_node(
|
|
143
|
-
agent.
|
|
488
|
+
agent.add_node(agent_builder)
|
|
489
|
+
agent.add_node(execute_tools)
|
|
490
|
+
agent.add_node(route_entry)
|
|
491
|
+
agent.add_edge(START, "route_entry")
|
|
144
492
|
return agent.compile(checkpointer=self.memory)
|