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