universal-mcp-agents 0.1.23rc1__tar.gz → 0.1.23rc3__tar.gz

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 (65) hide show
  1. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/PKG-INFO +3 -3
  2. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/pyproject.toml +3 -3
  3. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/__init__.py +2 -2
  4. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/base.py +2 -2
  5. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/cli.py +1 -1
  6. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/__main__.py +2 -5
  7. universal_mcp_agents-0.1.23rc3/src/universal_mcp/agents/codeact0/agent.py +406 -0
  8. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/prompts.py +58 -43
  9. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/state.py +13 -13
  10. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/tools.py +41 -8
  11. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/utils.py +26 -5
  12. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/uv.lock +3 -3
  13. universal_mcp_agents-0.1.23rc1/src/universal_mcp/agents/codeact0/agent.py +0 -365
  14. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/.github/workflows/evals.yml +0 -0
  15. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/.github/workflows/lint.yml +0 -0
  16. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/.github/workflows/release-please.yml +0 -0
  17. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/.github/workflows/tests.yml +0 -0
  18. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/.gitignore +0 -0
  19. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/.pre-commit-config.yaml +0 -0
  20. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/GEMINI.md +0 -0
  21. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/PROMPTS.md +0 -0
  22. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/README.md +0 -0
  23. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/bump_and_release.sh +0 -0
  24. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/__init__.py +0 -0
  25. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/dataset.py +0 -0
  26. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/datasets/codeact.jsonl +0 -0
  27. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/datasets/exact.jsonl +0 -0
  28. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/datasets/tasks.jsonl +0 -0
  29. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/evaluators.py +0 -0
  30. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/prompts.py +0 -0
  31. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/run.py +0 -0
  32. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/evals/utils.py +0 -0
  33. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/tests/test_agents.py +0 -0
  34. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/__init__.py +0 -0
  35. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/__main__.py +0 -0
  36. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/agent.py +0 -0
  37. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/context.py +0 -0
  38. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/graph.py +0 -0
  39. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/prompts.py +0 -0
  40. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/state.py +0 -0
  41. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/bigtool/tools.py +0 -0
  42. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/builder/__main__.py +0 -0
  43. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/builder/builder.py +0 -0
  44. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/builder/helper.py +0 -0
  45. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/builder/prompts.py +0 -0
  46. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/builder/state.py +0 -0
  47. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/__init__.py +0 -0
  48. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/config.py +0 -0
  49. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/langgraph_agent.py +0 -0
  50. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/llm_tool.py +0 -0
  51. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/codeact0/sandbox.py +0 -0
  52. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/hil.py +0 -0
  53. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/llm.py +0 -0
  54. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/react.py +0 -0
  55. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/sandbox.py +0 -0
  56. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/shared/__main__.py +0 -0
  57. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/shared/prompts.py +0 -0
  58. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/shared/tool_node.py +0 -0
  59. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/simple.py +0 -0
  60. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/agents/utils.py +0 -0
  61. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/applications/filesystem/__init__.py +0 -0
  62. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/applications/filesystem/app.py +0 -0
  63. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/applications/llm/__init__.py +0 -0
  64. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/applications/llm/app.py +0 -0
  65. {universal_mcp_agents-0.1.23rc1 → universal_mcp_agents-0.1.23rc3}/src/universal_mcp/applications/ui/app.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-agents
3
- Version: 0.1.23rc1
3
+ Version: 0.1.23rc3
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/universal-mcp/applications
6
6
  Project-URL: Repository, https://github.com/universal-mcp/applications
@@ -12,8 +12,8 @@ Requires-Dist: langchain-google-genai>=2.1.10
12
12
  Requires-Dist: langchain-openai>=0.3.32
13
13
  Requires-Dist: langgraph>=0.6.6
14
14
  Requires-Dist: typer>=0.17.4
15
- Requires-Dist: universal-mcp-applications>=0.1.24
16
- Requires-Dist: universal-mcp>=0.1.24rc25
15
+ Requires-Dist: universal-mcp-applications>=0.1.25
16
+ Requires-Dist: universal-mcp>=0.1.24rc26
17
17
  Provides-Extra: dev
18
18
  Requires-Dist: pre-commit; extra == 'dev'
19
19
  Requires-Dist: ruff; extra == 'dev'
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "universal-mcp-agents"
9
- version = "0.1.23-rc1"
9
+ version = "0.1.23-rc3"
10
10
  description = "Add your description here"
11
11
  readme = "README.md"
12
12
  authors = [
@@ -19,8 +19,8 @@ dependencies = [
19
19
  "langchain-openai>=0.3.32",
20
20
  "langgraph>=0.6.6",
21
21
  "typer>=0.17.4",
22
- "universal-mcp>=0.1.24rc25",
23
- "universal-mcp-applications>=0.1.24",
22
+ "universal-mcp>=0.1.24rc26",
23
+ "universal-mcp-applications>=0.1.25",
24
24
  ]
25
25
 
26
26
  [project.license]
@@ -9,7 +9,7 @@ from universal_mcp.agents.simple import SimpleAgent
9
9
 
10
10
 
11
11
  def get_agent(
12
- agent_name: Literal["react", "simple", "builder", "bigtool", "codeact-script", "codeact-repl"],
12
+ agent_name: Literal["react", "simple", "builder", "bigtool", "codeact-repl"],
13
13
  ):
14
14
  if agent_name == "react":
15
15
  return ReactAgent
@@ -23,7 +23,7 @@ def get_agent(
23
23
  return CodeActPlaybookAgent
24
24
  else:
25
25
  raise ValueError(
26
- f"Unknown agent: {agent_name}. Possible values: react, simple, builder, bigtool, codeact-script, codeact-repl"
26
+ f"Unknown agent: {agent_name}. Possible values: react, simple, builder, bigtool, codeact-repl"
27
27
  )
28
28
 
29
29
 
@@ -66,9 +66,9 @@ class BaseAgent:
66
66
  ):
67
67
  if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2:
68
68
  payload, meta_dict = meta
69
- is_playbook = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "playbook"
69
+ is_agent_builder = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "agent_builder"
70
70
  additional_kwargs = getattr(payload, "additional_kwargs", {}) or {}
71
- if is_playbook and not additional_kwargs.get("stream"):
71
+ if is_agent_builder and not additional_kwargs.get("stream"):
72
72
  continue
73
73
  if isinstance(payload, AIMessageChunk):
74
74
  last_ai_chunk = payload
@@ -25,7 +25,7 @@ def run(name: str = "react"):
25
25
  client = AgentrClient()
26
26
  params = {
27
27
  "instructions": "You are a helpful assistant",
28
- "model": "anthropic/claude-sonnet-4-20250514",
28
+ "model": "azure/gpt-4.1",
29
29
  "registry": AgentrRegistry(client=client),
30
30
  "memory": MemorySaver(),
31
31
  }
@@ -13,15 +13,12 @@ async def main():
13
13
  agent = CodeActPlaybookAgent(
14
14
  name="CodeAct Agent",
15
15
  instructions="Be very concise in your answers.",
16
- model="anthropic:claude-4-sonnet-20250514",
17
- tools={"google_mail": ["list_messages"]},
16
+ model="azure/gpt-4.1",
18
17
  registry=AgentrRegistry(),
19
18
  memory=memory,
20
19
  )
21
20
  print("Starting agent...")
22
- result = await agent.invoke(
23
- user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
24
- )
21
+ result = await agent.invoke(user_input="load all the tools of reddit which can be used to search subreddit")
25
22
  print(messages_to_list(result["messages"]))
26
23
 
27
24
 
@@ -0,0 +1,406 @@
1
+ import copy
2
+ import json
3
+ import re
4
+ import uuid
5
+ from typing import Literal, cast
6
+
7
+ from langchain_anthropic import ChatAnthropic
8
+ from langchain_core.messages import AIMessage, ToolMessage
9
+ from langchain_core.tools import StructuredTool
10
+ from langgraph.checkpoint.base import BaseCheckpointSaver
11
+ from langgraph.graph import START, StateGraph
12
+ from langgraph.types import Command, RetryPolicy, StreamWriter
13
+ from universal_mcp.tools.registry import ToolRegistry
14
+ from universal_mcp.types import ToolFormat
15
+
16
+ from universal_mcp.agents.base import BaseAgent
17
+ from universal_mcp.agents.codeact0.llm_tool import smart_print
18
+ from universal_mcp.agents.codeact0.prompts import (
19
+ AGENT_BUILDER_GENERATING_PROMPT,
20
+ AGENT_BUILDER_META_PROMPT,
21
+ AGENT_BUILDER_PLANNING_PROMPT,
22
+ create_default_prompt,
23
+ )
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 CodeActState, AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan
26
+ from universal_mcp.agents.codeact0.tools import (
27
+ create_meta_tools,
28
+ enter_agent_builder_mode,
29
+ get_valid_tools,
30
+ )
31
+ from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string
32
+ from universal_mcp.agents.llm import load_chat_model
33
+ from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
34
+
35
+
36
+ class CodeActPlaybookAgent(BaseAgent):
37
+ def __init__(
38
+ self,
39
+ name: str,
40
+ instructions: str,
41
+ model: str,
42
+ memory: BaseCheckpointSaver | None = None,
43
+ registry: ToolRegistry | None = None,
44
+ agent_builder_registry: object | None = None,
45
+ sandbox_timeout: int = 20,
46
+ **kwargs,
47
+ ):
48
+ super().__init__(
49
+ name=name,
50
+ instructions=instructions,
51
+ model=model,
52
+ memory=memory,
53
+ **kwargs,
54
+ )
55
+ self.model_instance = load_chat_model(model)
56
+ self.agent_builder_model_instance = load_chat_model("azure/gpt-4.1")
57
+ self.registry = registry
58
+ self.agent_builder_registry = agent_builder_registry
59
+ self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
60
+ self.tools_config = self.agent.tools if self.agent else {}
61
+ self.eval_fn = eval_unsafe
62
+ self.sandbox_timeout = sandbox_timeout
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.exported_tools = []
69
+
70
+ async def _build_graph(self):
71
+ meta_tools = create_meta_tools(self.registry)
72
+ additional_tools = [smart_print, meta_tools["web_search"]]
73
+ self.additional_tools = [
74
+ t if isinstance(t, StructuredTool) else StructuredTool.from_function(t) for t in additional_tools
75
+ ]
76
+
77
+ if self.tools_config:
78
+ if isinstance(self.tools_config, dict):
79
+ self.tools_config = [
80
+ f"{provider}__{tool}" for provider, tools in self.tools_config.items() for tool in tools
81
+ ]
82
+ if not self.registry:
83
+ raise ValueError("Tools are configured but no registry is provided")
84
+ await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
85
+
86
+ await self.registry.export_tools(self.default_tools_config, ToolFormat.LANGCHAIN)
87
+
88
+ async def call_model(state: CodeActState) -> Command[Literal["execute_tools"]]:
89
+ """This node now only ever binds the four meta-tools to the LLM."""
90
+ messages = build_anthropic_cache_message(self.final_instructions) + state["messages"]
91
+
92
+ agent_facing_tools = [
93
+ execute_ipython_cell,
94
+ enter_agent_builder_mode,
95
+ meta_tools["search_functions"],
96
+ meta_tools["load_functions"],
97
+ ]
98
+
99
+ if isinstance(self.model_instance, ChatAnthropic):
100
+ model_with_tools = self.model_instance.bind_tools(
101
+ tools=agent_facing_tools,
102
+ tool_choice="auto",
103
+ cache_control={"type": "ephemeral", "ttl": "1h"},
104
+ )
105
+ if isinstance(messages[-1].content, str):
106
+ pass
107
+ else:
108
+ last = copy.deepcopy(messages[-1])
109
+ last.content[-1]["cache_control"] = {"type": "ephemeral", "ttl": "5m"}
110
+ messages[-1] = last
111
+ else:
112
+ model_with_tools = self.model_instance.bind_tools(
113
+ tools=agent_facing_tools,
114
+ tool_choice="auto",
115
+ )
116
+
117
+ response = cast(AIMessage, model_with_tools.invoke(messages))
118
+ if response.tool_calls:
119
+ return Command(goto="execute_tools", update={"messages": [response]})
120
+ else:
121
+ return Command(update={"messages": [response], "model_with_tools": model_with_tools})
122
+
123
+ async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "agent_builder"]]:
124
+ """Execute tool calls"""
125
+ last_message = state["messages"][-1]
126
+ tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
127
+
128
+ tool_messages = []
129
+ new_tool_ids = []
130
+ tool_result = ""
131
+ ask_user = False
132
+ ai_msg = ""
133
+ effective_previous_add_context = state.get("add_context", {})
134
+ effective_existing_context = state.get("context", {})
135
+ # logging.info(f"Initial new_tool_ids_for_context: {new_tool_ids_for_context}")
136
+
137
+ for tool_call in tool_calls:
138
+ tool_name = tool_call["name"]
139
+ tool_args = tool_call["args"]
140
+ try:
141
+ if tool_name == "enter_agent_builder_mode":
142
+ tool_message = ToolMessage(
143
+ content=json.dumps("Entered Agent Builder Mode."),
144
+ name=tool_call["name"],
145
+ tool_call_id=tool_call["id"],
146
+ )
147
+ return Command(
148
+ goto="agent_builder",
149
+ update={"agent_builder_mode": "planning", "messages": [tool_message]}, # 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["load_functions"].ainvoke(tool_args)
166
+ # We still need to update the sandbox context for `execute_ipython_cell`
167
+ new_tool_ids.extend(valid_tools)
168
+ if new_tool_ids:
169
+ self.tools_context.update(new_context_for_sandbox)
170
+ if unconnected_links:
171
+ ask_user = True
172
+ 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} "
173
+
174
+ elif tool_name == "search_functions":
175
+ tool_result = await meta_tools["search_functions"].ainvoke(tool_args)
176
+ else:
177
+ raise Exception(
178
+ f"Unexpected tool call: {tool_call['name']}. "
179
+ "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'."
180
+ )
181
+ except Exception as e:
182
+ tool_result = str(e)
183
+
184
+ tool_message = ToolMessage(
185
+ content=json.dumps(tool_result),
186
+ name=tool_call["name"],
187
+ tool_call_id=tool_call["id"],
188
+ )
189
+ tool_messages.append(tool_message)
190
+
191
+ if ask_user:
192
+ tool_messages.append(AIMessage(content=ai_msg))
193
+ return Command(
194
+ update={
195
+ "messages": tool_messages,
196
+ "selected_tool_ids": new_tool_ids,
197
+ "context": effective_existing_context,
198
+ "add_context": effective_previous_add_context,
199
+ }
200
+ )
201
+
202
+ return Command(
203
+ goto="call_model",
204
+ update={
205
+ "messages": tool_messages,
206
+ "selected_tool_ids": new_tool_ids,
207
+ "context": effective_existing_context,
208
+ "add_context": effective_previous_add_context,
209
+ },
210
+ )
211
+
212
+ def agent_builder(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
213
+ agent_builder_mode = state.get("agent_builder_mode")
214
+ if agent_builder_mode == "planning":
215
+ plan_id = str(uuid.uuid4())
216
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": bool(self.agent)}})
217
+ planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT
218
+ messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
219
+
220
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(AgentBuilderPlan)
221
+ response = model_with_structured_output.invoke(messages)
222
+ plan = cast(AgentBuilderPlan, response)
223
+
224
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
225
+ return Command(
226
+ update={
227
+ "messages": [
228
+ AIMessage(
229
+ content=json.dumps(plan.model_dump()),
230
+ additional_kwargs={
231
+ "type": "planning",
232
+ "plan": plan.steps,
233
+ "update": bool(self.agent),
234
+ },
235
+ )
236
+ ],
237
+ "agent_builder_mode": "confirming",
238
+ "plan": plan.steps,
239
+ }
240
+ )
241
+
242
+ elif agent_builder_mode == "confirming":
243
+ # Deterministic routing based on three exact button inputs from UI
244
+ user_text = ""
245
+ for m in reversed(state["messages"]):
246
+ try:
247
+ if getattr(m, "type", "") in {"human", "user"}:
248
+ user_text = (get_message_text(m) or "").strip()
249
+ if user_text:
250
+ break
251
+ except Exception:
252
+ continue
253
+
254
+ t = user_text.lower()
255
+ if t == "yes, this is great":
256
+ self.meta_id = str(uuid.uuid4())
257
+ name, description = None, None
258
+ if self.agent:
259
+ # Update flow: use existing name/description and do not re-generate
260
+ name = getattr(self.agent, "name", None)
261
+ description = getattr(self.agent, "description", None)
262
+ writer(
263
+ {
264
+ "type": "custom",
265
+ id: self.meta_id,
266
+ "name": "generating",
267
+ "data": {
268
+ "update": True,
269
+ "name": name,
270
+ "description": description,
271
+ },
272
+ }
273
+ )
274
+ else:
275
+ writer({"type": "custom", id: self.meta_id, "name": "generating", "data": {"update": False}})
276
+
277
+ meta_instructions = self.instructions + AGENT_BUILDER_META_PROMPT
278
+ messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
279
+
280
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(AgentBuilderMeta)
281
+ meta_response = model_with_structured_output.invoke(messages)
282
+ meta = cast(AgentBuilderMeta, meta_response)
283
+ name, description = meta.name, meta.description
284
+
285
+ # Emit intermediary UI update with created name/description
286
+ writer(
287
+ {
288
+ "type": "custom",
289
+ id: self.meta_id,
290
+ "name": "generating",
291
+ "data": {"update": False, "name": name, "description": description},
292
+ }
293
+ )
294
+
295
+ return Command(
296
+ goto="agent_builder",
297
+ update={
298
+ "agent_builder_mode": "generating",
299
+ "agent_name": name,
300
+ "agent_description": description,
301
+ },
302
+ )
303
+ if t == "i would like to modify the plan":
304
+ prompt_ai = AIMessage(
305
+ content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.",
306
+ additional_kwargs={"stream": "true"},
307
+ )
308
+ return Command(update={"agent_builder_mode": "planning", "messages": [prompt_ai]})
309
+ if t == "let's do something else":
310
+ return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
311
+
312
+ # Fallback safe default
313
+ return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
314
+
315
+ elif agent_builder_mode == "generating":
316
+ generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT
317
+ messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
318
+
319
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(AgentBuilderCode)
320
+ response = model_with_structured_output.invoke(messages)
321
+ func_code = cast(AgentBuilderCode, response).code
322
+
323
+ # Extract function name (handle both regular and async functions)
324
+ match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
325
+ if match:
326
+ function_name = match.group(1)
327
+ else:
328
+ function_name = "generated_agent"
329
+
330
+ # Use generated metadata if available
331
+ final_name = state.get("agent_name") or function_name
332
+ final_description = state.get("agent_description") or f"Generated agent: {function_name}"
333
+
334
+ # Save or update an Agent using the helper registry
335
+ try:
336
+ if not self.agent_builder_registry:
337
+ raise ValueError("AgentBuilder registry is not configured")
338
+
339
+ # Build instructions payload embedding the plan and function code
340
+ instructions_payload = {
341
+ "plan": state["plan"],
342
+ "script": func_code,
343
+ }
344
+
345
+ # Convert tool ids list to dict
346
+ tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
347
+
348
+ res = self.agent_builder_registry.upsert_agent(
349
+ name=final_name,
350
+ description=final_description,
351
+ instructions=instructions_payload,
352
+ tools=tool_dict,
353
+ )
354
+ except Exception as e:
355
+ raise e
356
+
357
+ writer(
358
+ {
359
+ "type": "custom",
360
+ id: self.meta_id,
361
+ "name": "generating",
362
+ "data": {
363
+ "id": str(res.id),
364
+ "update": bool(self.agent),
365
+ "name": final_name,
366
+ "description": final_description,
367
+ },
368
+ }
369
+ )
370
+ mock_assistant_message = AIMessage(
371
+ content=json.dumps(response.model_dump()),
372
+ additional_kwargs={
373
+ "type": "generating",
374
+ "id": str(res.id),
375
+ "update": bool(self.agent),
376
+ "name": final_name,
377
+ "description": final_description,
378
+ },
379
+ )
380
+
381
+ return Command(update={"messages": [mock_assistant_message], "agent_builder_mode": "normal"})
382
+
383
+ async def route_entry(state: CodeActState) -> Literal["call_model", "agent_builder"]:
384
+ """Route to either normal mode or agent builder creation"""
385
+ all_tools = await self.registry.export_tools(state["selected_tool_ids"], ToolFormat.LANGCHAIN)
386
+ # print(all_tools)
387
+
388
+ # Create the initial system prompt and tools_context in one go
389
+ self.final_instructions, self.tools_context = create_default_prompt(
390
+ all_tools,
391
+ self.additional_tools,
392
+ self.instructions,
393
+ await get_connected_apps_string(self.registry),
394
+ self.agent,
395
+ is_initial_prompt=True,
396
+ )
397
+ if state.get("agent_builder_mode") in ["planning", "confirming", "generating"]:
398
+ return "agent_builder"
399
+ return "call_model"
400
+
401
+ agent = StateGraph(state_schema=CodeActState)
402
+ agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
403
+ agent.add_node(agent_builder)
404
+ agent.add_node(execute_tools)
405
+ agent.add_conditional_edges(START, route_entry)
406
+ return agent.compile(checkpointer=self.memory)