universal-mcp-agents 0.1.19rc1__py3-none-any.whl → 0.1.21__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 (38) hide show
  1. universal_mcp/agents/__init__.py +5 -9
  2. universal_mcp/agents/base.py +33 -29
  3. universal_mcp/agents/cli.py +0 -3
  4. universal_mcp/agents/codeact0/__init__.py +2 -3
  5. universal_mcp/agents/codeact0/__main__.py +2 -2
  6. universal_mcp/agents/codeact0/agent.py +256 -87
  7. universal_mcp/agents/codeact0/langgraph_agent.py +1 -1
  8. universal_mcp/agents/codeact0/llm_tool.py +2 -254
  9. universal_mcp/agents/codeact0/prompts.py +88 -104
  10. universal_mcp/agents/codeact0/sandbox.py +31 -1
  11. universal_mcp/agents/codeact0/state.py +14 -3
  12. universal_mcp/agents/codeact0/tools.py +189 -83
  13. universal_mcp/agents/codeact0/utils.py +8 -0
  14. universal_mcp/applications/llm/app.py +2 -2
  15. {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.21.dist-info}/METADATA +1 -1
  16. universal_mcp_agents-0.1.21.dist-info/RECORD +44 -0
  17. universal_mcp/agents/codeact/__init__.py +0 -3
  18. universal_mcp/agents/codeact/__main__.py +0 -33
  19. universal_mcp/agents/codeact/agent.py +0 -240
  20. universal_mcp/agents/codeact/models.py +0 -11
  21. universal_mcp/agents/codeact/prompts.py +0 -82
  22. universal_mcp/agents/codeact/sandbox.py +0 -85
  23. universal_mcp/agents/codeact/state.py +0 -11
  24. universal_mcp/agents/codeact/utils.py +0 -68
  25. universal_mcp/agents/codeact0/playbook_agent.py +0 -355
  26. universal_mcp/agents/unified/README.md +0 -45
  27. universal_mcp/agents/unified/__init__.py +0 -3
  28. universal_mcp/agents/unified/__main__.py +0 -28
  29. universal_mcp/agents/unified/agent.py +0 -289
  30. universal_mcp/agents/unified/langgraph_agent.py +0 -14
  31. universal_mcp/agents/unified/llm_tool.py +0 -25
  32. universal_mcp/agents/unified/prompts.py +0 -192
  33. universal_mcp/agents/unified/sandbox.py +0 -101
  34. universal_mcp/agents/unified/state.py +0 -42
  35. universal_mcp/agents/unified/tools.py +0 -188
  36. universal_mcp/agents/unified/utils.py +0 -388
  37. universal_mcp_agents-0.1.19rc1.dist-info/RECORD +0 -64
  38. {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.21.dist-info}/WHEEL +0 -0
@@ -3,14 +3,14 @@ from typing import Literal
3
3
  from universal_mcp.agents.base import BaseAgent
4
4
  from universal_mcp.agents.bigtool import BigToolAgent
5
5
  from universal_mcp.agents.builder.builder import BuilderAgent
6
- from universal_mcp.agents.codeact import CodeActAgent as CodeActScript
7
- from universal_mcp.agents.codeact0 import CodeActPlaybookAgent as CodeActRepl
6
+ from universal_mcp.agents.codeact0 import CodeActPlaybookAgent
8
7
  from universal_mcp.agents.react import ReactAgent
9
8
  from universal_mcp.agents.simple import SimpleAgent
10
- from universal_mcp.agents.unified import UnifiedAgent
11
9
 
12
10
 
13
- def get_agent(agent_name: Literal["react", "simple", "builder", "bigtool", "codeact-script", "codeact-repl"]):
11
+ def get_agent(
12
+ agent_name: Literal["react", "simple", "builder", "bigtool", "codeact-script", "codeact-repl"],
13
+ ):
14
14
  if agent_name == "react":
15
15
  return ReactAgent
16
16
  elif agent_name == "simple":
@@ -19,12 +19,8 @@ def get_agent(agent_name: Literal["react", "simple", "builder", "bigtool", "code
19
19
  return BuilderAgent
20
20
  elif agent_name == "bigtool":
21
21
  return BigToolAgent
22
- elif agent_name == "codeact-script":
23
- return CodeActScript
24
22
  elif agent_name == "codeact-repl":
25
- return CodeActRepl
26
- elif agent_name == "unified":
27
- return UnifiedAgent
23
+ return CodeActPlaybookAgent
28
24
  else:
29
25
  raise ValueError(
30
26
  f"Unknown agent: {agent_name}. Possible values: react, simple, builder, bigtool, codeact-script, codeact-repl"
@@ -49,44 +49,48 @@ class BaseAgent:
49
49
  run_metadata.update(metadata)
50
50
 
51
51
  run_config = {
52
+ "recursion_limit": 50,
52
53
  "configurable": {"thread_id": thread_id},
53
54
  "metadata": run_metadata,
55
+ "run_id": thread_id,
56
+ "run_name": self.name,
54
57
  }
55
58
 
59
+ last_ai_chunk = None
56
60
  async for event, meta in self._graph.astream(
57
61
  {"messages": [{"role": "user", "content": user_input}]},
58
62
  config=run_config,
59
63
  context={"system_prompt": self.instructions, "model": self.model},
60
- stream_mode="messages",
64
+ stream_mode=["messages", "custom"],
61
65
  stream_usage=True,
62
66
  ):
63
- # Only forward assistant token chunks that are not tool-related.
64
- type_ = type(event)
65
- tags = meta.get("tags", []) if isinstance(meta, dict) else []
66
- is_quiet = isinstance(tags, list) and ("quiet" in tags)
67
- if is_quiet:
68
- continue
69
- # Handle different types of messages
70
- if type_ == AIMessageChunk:
71
- # Accumulate billing and aggregate message
72
- aggregate = event if aggregate is None else aggregate + event
73
- # Ignore intermeddite finish messages
74
- if "finish_reason" in event.response_metadata:
75
- # Got LLM finish reason ignore it
76
- logger.debug(
77
- f"Finish event: {event}, reason: {event.response_metadata['finish_reason']}, Metadata: {meta}"
78
- )
79
- pass
80
- else:
81
- logger.debug(f"Event: {event}, Metadata: {meta}")
82
- yield event
83
- # Send a final finished message
84
- # The last event would be finish
85
- event = cast(AIMessageChunk, event)
86
- event.usage_metadata = aggregate.usage_metadata
87
- logger.debug(f"Usage metadata: {event.usage_metadata}")
88
- event.content = "" # Clear the message since it would have already been streamed above
89
- yield event
67
+ if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2:
68
+ payload, meta_dict = meta
69
+ is_playbook = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "playbook"
70
+ additional_kwargs = getattr(payload, "additional_kwargs", {}) or {}
71
+ if is_playbook and not additional_kwargs.get("stream"):
72
+ continue
73
+ if isinstance(payload, AIMessageChunk):
74
+ last_ai_chunk = payload
75
+ aggregate = payload if aggregate is None else aggregate + payload
76
+ if "finish_reason" in payload.response_metadata:
77
+ logger.debug(
78
+ f"Finish event: {payload}, reason: {payload.response_metadata['finish_reason']}, Metadata: {meta_dict}"
79
+ )
80
+ pass
81
+ logger.debug(f"Event: {payload}, Metadata: {meta_dict}")
82
+ yield payload
83
+
84
+ if event == "custom":
85
+ yield meta
86
+
87
+ # Send a final finished message if we saw any AI chunks (to carry usage)
88
+ if last_ai_chunk is not None and aggregate is not None:
89
+ event = cast(AIMessageChunk, last_ai_chunk)
90
+ event.usage_metadata = aggregate.usage_metadata
91
+ logger.debug(f"Usage metadata: {event.usage_metadata}")
92
+ event.content = "" # Clear the message since it would have already been streamed above
93
+ yield event
90
94
 
91
95
  async def stream_interactive(self, thread_id: str, user_input: str):
92
96
  await self.ainit()
@@ -113,7 +117,7 @@ class BaseAgent:
113
117
  run_metadata.update(metadata)
114
118
 
115
119
  run_config = {
116
- "recursion_limit": 25,
120
+ "recursion_limit": 50,
117
121
  "configurable": {"thread_id": thread_id},
118
122
  "metadata": run_metadata,
119
123
  "run_id": thread_id,
@@ -28,9 +28,6 @@ def run(name: str = "react"):
28
28
  "model": "anthropic/claude-sonnet-4-20250514",
29
29
  "registry": AgentrRegistry(client=client),
30
30
  "memory": MemorySaver(),
31
- "tools": {
32
- "google_mail": ["send_email"],
33
- },
34
31
  }
35
32
  agent_cls = get_agent(name)
36
33
  agent = agent_cls(name=name, **params)
@@ -1,4 +1,3 @@
1
- from .agent import CodeActAgent
2
- from .playbook_agent import CodeActPlaybookAgent
1
+ from .agent import CodeActPlaybookAgent
3
2
 
4
- __all__ = ["CodeActAgent", "CodeActPlaybookAgent"]
3
+ __all__ = ["CodeActPlaybookAgent"]
@@ -4,13 +4,13 @@ from langgraph.checkpoint.memory import MemorySaver
4
4
  from rich import print
5
5
  from universal_mcp.agentr.registry import AgentrRegistry
6
6
 
7
- from universal_mcp.agents.codeact0.agent import CodeActAgent
7
+ from universal_mcp.agents.codeact0.agent import CodeActPlaybookAgent
8
8
  from universal_mcp.agents.utils import messages_to_list
9
9
 
10
10
 
11
11
  async def main():
12
12
  memory = MemorySaver()
13
- agent = CodeActAgent(
13
+ agent = CodeActPlaybookAgent(
14
14
  name="CodeAct Agent",
15
15
  instructions="Be very concise in your answers.",
16
16
  model="anthropic:claude-4-sonnet-20250514",
@@ -1,29 +1,36 @@
1
- import inspect
2
- from collections.abc import Callable
1
+ import json
2
+ import re
3
3
  from typing import Literal, cast
4
+ import uuid
4
5
 
5
6
  from langchain_core.messages import AIMessage, ToolMessage
6
7
  from langchain_core.tools import StructuredTool
7
- from langchain_core.tools import tool as create_tool
8
8
  from langgraph.checkpoint.base import BaseCheckpointSaver
9
9
  from langgraph.graph import START, StateGraph
10
- from langgraph.types import Command, RetryPolicy
10
+ from langgraph.types import Command, RetryPolicy, StreamWriter
11
11
  from universal_mcp.tools.registry import ToolRegistry
12
12
  from universal_mcp.types import ToolConfig, ToolFormat
13
13
 
14
14
  from universal_mcp.agents.base import BaseAgent
15
- from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
15
+ from universal_mcp.agents.codeact0.llm_tool import smart_print
16
16
  from universal_mcp.agents.codeact0.prompts import (
17
+ PLAYBOOK_GENERATING_PROMPT,
18
+ PLAYBOOK_PLANNING_PROMPT,
17
19
  create_default_prompt,
18
20
  )
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.utils import inject_context, smart_truncate
21
+ from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
22
+ from universal_mcp.agents.codeact0.state import CodeActState, PlaybookCode, PlaybookPlan
23
+ from universal_mcp.agents.codeact0.tools import (
24
+ create_meta_tools,
25
+ enter_playbook_mode,
26
+ get_valid_tools,
27
+ )
28
+ from universal_mcp.agents.codeact0.utils import add_tools
22
29
  from universal_mcp.agents.llm import load_chat_model
23
- from universal_mcp.agents.utils import filter_retry_on
30
+ from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
24
31
 
25
32
 
26
- class CodeActAgent(BaseAgent):
33
+ class CodeActPlaybookAgent(BaseAgent):
27
34
  def __init__(
28
35
  self,
29
36
  name: str,
@@ -32,6 +39,7 @@ class CodeActAgent(BaseAgent):
32
39
  memory: BaseCheckpointSaver | None = None,
33
40
  tools: ToolConfig | None = None,
34
41
  registry: ToolRegistry | None = None,
42
+ playbook_registry: object | None = None,
35
43
  sandbox_timeout: int = 20,
36
44
  **kwargs,
37
45
  ):
@@ -42,103 +50,264 @@ class CodeActAgent(BaseAgent):
42
50
  memory=memory,
43
51
  **kwargs,
44
52
  )
45
- self.model_instance = load_chat_model(model, thinking=True)
53
+ self.model_instance = load_chat_model(model)
54
+ self.playbook_model_instance = load_chat_model("azure/gpt-4.1")
46
55
  self.tools_config = tools or {}
47
56
  self.registry = registry
57
+ self.playbook_registry = playbook_registry
58
+ self.playbook = playbook_registry.get_agent() if playbook_registry else None
48
59
  self.eval_fn = eval_unsafe
49
60
  self.sandbox_timeout = sandbox_timeout
50
- self.processed_tools: list[StructuredTool | Callable] = []
51
-
52
- # TODO(manoj): Use toolformat native instead of langchain
53
- # TODO(manoj, later): Add better sandboxing
54
- # Old Nishant TODO s:
55
- # - Make codeact faster by calling upto API call (this done but should be tested)
56
- # - Add support for async eval_fn
57
- # - Throw Error if code snippet is too long (> 1000 characters) and suggest to split it into smaller parts
58
- # - Multiple models from config
61
+ self.default_tools = {
62
+ "llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
63
+ "markitdown": ["convert_to_markdown"],
64
+ }
65
+ add_tools(self.tools_config, self.default_tools)
59
66
 
60
67
  async def _build_graph(self):
61
- exported_tools = []
68
+ meta_tools = create_meta_tools(self.registry)
69
+ additional_tools = [smart_print, meta_tools["web_search"]]
70
+ self.additional_tools = [
71
+ t if isinstance(t, StructuredTool) else StructuredTool.from_function(t) for t in additional_tools
72
+ ]
62
73
  if self.tools_config:
74
+ # Convert dict format to list format if needed
75
+ if isinstance(self.tools_config, dict):
76
+ self.tools_config = [
77
+ f"{provider}__{tool}" for provider, tools in self.tools_config.items() for tool in tools
78
+ ]
63
79
  if not self.registry:
64
80
  raise ValueError("Tools are configured but no registry is provided")
65
- # Langchain tools are fine
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
- )
72
81
 
73
- def call_model(state: CodeActState) -> Command[Literal["sandbox"]]:
74
- messages = [{"role": "system", "content": self.instructions}] + state["messages"]
82
+ async def call_model(state: CodeActState) -> Command[Literal["execute_tools"]]:
83
+ messages = [{"role": "system", "content": self.final_instructions}] + state["messages"]
75
84
 
76
85
  # Run the model and potentially loop for reflection
77
- model_with_tools = self.model_instance.bind_tools(tools=[execute_ipython_cell], tool_choice="auto")
86
+ model_with_tools = self.model_instance.bind_tools(
87
+ tools=[
88
+ execute_ipython_cell,
89
+ enter_playbook_mode,
90
+ meta_tools["search_functions"],
91
+ meta_tools["load_functions"],
92
+ ],
93
+ tool_choice="auto",
94
+ )
78
95
  response = cast(AIMessage, model_with_tools.invoke(messages))
79
-
80
96
  if response.tool_calls:
81
- if len(response.tool_calls) > 1:
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]})
97
+ return Command(goto="execute_tools", update={"messages": [response]})
93
98
  else:
94
- return Command(update={"messages": [response]})
95
-
96
- # If eval_fn is a async, we define async node function.
97
- if inspect.iscoroutinefunction(self.eval_fn):
98
- raise ValueError("eval_fn must be a synchronous function, not a coroutine.")
99
- # async def sandbox(state: StateSchema):
100
- # existing_context = state.get("context", {})
101
- # context = {**existing_context, **tools_context}
102
- # # Execute the script in the sandbox
103
- # output, new_vars = await eval_fn(state["script"], context)
104
- # new_context = {**existing_context, **new_vars}
105
- # return {
106
- # "messages": [{"role": "user", "content": output}],
107
- # "context": new_context,
108
- # }
109
- else:
110
-
111
- def sandbox(state: CodeActState) -> Command[Literal["call_model"]]:
112
- tool_call = state["messages"][-1].tool_calls[0] # type: ignore
113
- code = tool_call["args"]["snippet"]
114
- previous_add_context = state.get("add_context", {})
115
- add_context = inject_context(previous_add_context, self.tools_context)
116
- existing_context = state.get("context", {})
117
- context = {**existing_context, **add_context}
118
- # Execute the script in the sandbox
119
-
120
- output, new_context, new_add_context = self.eval_fn(
121
- code, context, previous_add_context, 180
122
- ) # default timeout 3 min
123
- output = smart_truncate(output)
99
+ return Command(update={"messages": [response], "model_with_tools": model_with_tools})
100
+
101
+ async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "playbook"]]:
102
+ """Execute tool calls"""
103
+ last_message = state["messages"][-1]
104
+ tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
105
+
106
+ tool_messages = []
107
+ new_tool_ids = []
108
+ ask_user = False
109
+ ai_msg = ""
110
+ tool_result = ""
111
+ effective_previous_add_context = state.get("add_context", {})
112
+ effective_existing_context = state.get("context", {})
113
+
114
+ for tool_call in tool_calls:
115
+ try:
116
+ if tool_call["name"] == "enter_playbook_mode":
117
+ tool_message = ToolMessage(
118
+ content=json.dumps("Entered Playbook Mode."),
119
+ name=tool_call["name"],
120
+ tool_call_id=tool_call["id"],
121
+ )
122
+ return Command(
123
+ goto="playbook",
124
+ update={"playbook_mode": "planning", "messages": [tool_message]}, # Entered Playbook mode
125
+ )
126
+ elif tool_call["name"] == "execute_ipython_cell":
127
+ code = tool_call["args"]["snippet"]
128
+ output, new_context, new_add_context = await handle_execute_ipython_cell(
129
+ code,
130
+ self.tools_context,
131
+ self.eval_fn,
132
+ effective_previous_add_context,
133
+ effective_existing_context,
134
+ )
135
+ effective_existing_context = new_context
136
+ effective_previous_add_context = new_add_context
137
+ tool_result = output
138
+ elif tool_call["name"] == "load_functions": # Handle load_functions separately
139
+ valid_tools, unconnected_links = await get_valid_tools(
140
+ tool_ids=tool_call["args"]["tool_ids"], registry=self.registry
141
+ )
142
+ new_tool_ids.extend(valid_tools)
143
+ # Create tool message response
144
+ tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
145
+ links = "\n".join(unconnected_links)
146
+ if links:
147
+ ask_user = True
148
+ ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {links} "
149
+ elif tool_call["name"] == "search_functions":
150
+ tool_result = await meta_tools["search_functions"].ainvoke(tool_call["args"])
151
+ else:
152
+ raise Exception(
153
+ f"Unexpected tool call: {tool_call['name']}. "
154
+ "tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'"
155
+ )
156
+ except Exception as e:
157
+ tool_result = str(e)
124
158
 
159
+ tool_message = ToolMessage(
160
+ content=json.dumps(tool_result),
161
+ name=tool_call["name"],
162
+ tool_call_id=tool_call["id"],
163
+ )
164
+ tool_messages.append(tool_message)
165
+
166
+ if new_tool_ids:
167
+ self.tools_config.extend(new_tool_ids)
168
+ self.exported_tools = await self.registry.export_tools(new_tool_ids, ToolFormat.LANGCHAIN)
169
+ self.final_instructions, self.tools_context = create_default_prompt(
170
+ self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
171
+ )
172
+ if ask_user:
173
+ tool_messages.append(AIMessage(content=ai_msg))
125
174
  return Command(
126
- goto="call_model",
127
175
  update={
128
- "messages": [
129
- ToolMessage(
130
- content=output,
131
- name=tool_call["name"],
132
- tool_call_id=tool_call["id"],
133
- )
134
- ],
135
- "context": new_context,
136
- "add_context": new_add_context,
137
- },
176
+ "messages": tool_messages,
177
+ "selected_tool_ids": new_tool_ids,
178
+ "context": effective_existing_context,
179
+ "add_context": effective_previous_add_context,
180
+ }
138
181
  )
139
182
 
183
+ return Command(
184
+ goto="call_model",
185
+ update={
186
+ "messages": tool_messages,
187
+ "selected_tool_ids": new_tool_ids,
188
+ "context": effective_existing_context,
189
+ "add_context": effective_previous_add_context,
190
+ },
191
+ )
192
+
193
+ def playbook(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
194
+ playbook_mode = state.get("playbook_mode")
195
+ if playbook_mode == "planning":
196
+ plan_id = str(uuid.uuid4())
197
+ writer({
198
+ "type": "custom",
199
+ id: plan_id,
200
+ "name": "planning",
201
+ "data": {"update": bool(self.playbook)}
202
+ })
203
+ planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
204
+ messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
205
+
206
+ model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookPlan)
207
+ response = model_with_structured_output.invoke(messages)
208
+ plan = cast(PlaybookPlan, response)
209
+
210
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
211
+ return Command(update={"messages": [AIMessage(content=json.dumps(plan.dict()), additional_kwargs={"type": "planning", "plan": plan.steps, "update": bool(self.playbook)})], "playbook_mode": "confirming", "plan": plan.steps})
212
+
213
+ elif playbook_mode == "confirming":
214
+ # Deterministic routing based on three exact button inputs from UI
215
+ user_text = ""
216
+ for m in reversed(state["messages"]):
217
+ try:
218
+ if getattr(m, "type", "") in {"human", "user"}:
219
+ user_text = (get_message_text(m) or "").strip()
220
+ if user_text:
221
+ break
222
+ except Exception:
223
+ continue
224
+
225
+ t = user_text.lower()
226
+ if t == "yes, this is great":
227
+ return Command(goto="playbook", update={"playbook_mode": "generating"})
228
+ if t == "i would like to modify the plan":
229
+ prompt_ai = AIMessage(content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.", additional_kwargs={"stream": "true"})
230
+ return Command(update={"playbook_mode": "planning", "messages": [prompt_ai]})
231
+ if t == "let's do something else":
232
+ return Command(goto="call_model", update={"playbook_mode": "inactive"})
233
+
234
+ # Fallback safe default
235
+ return Command(goto="call_model", update={"playbook_mode": "inactive"})
236
+
237
+ elif playbook_mode == "generating":
238
+ generate_id = str(uuid.uuid4())
239
+ writer({
240
+ "type": "custom",
241
+ id: generate_id,
242
+ "name": "generating",
243
+ "data": {"update": bool(self.playbook)}
244
+ })
245
+ generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
246
+ messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
247
+
248
+ model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookCode)
249
+ response = model_with_structured_output.invoke(messages)
250
+ func_code = cast(PlaybookCode, response).code
251
+
252
+ # Extract function name (handle both regular and async functions)
253
+ match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
254
+ if match:
255
+ function_name = match.group(1)
256
+ else:
257
+ function_name = "generated_playbook"
258
+
259
+ # Save or update an Agent using the helper registry
260
+ try:
261
+ if not self.playbook_registry:
262
+ raise ValueError("Playbook registry is not configured")
263
+
264
+ # Build instructions payload embedding the plan and function code
265
+ instructions_payload = {
266
+ "playbookPlan": state["plan"],
267
+ "playbookScript": func_code,
268
+ }
269
+
270
+ # Convert tool ids list to dict
271
+ tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
272
+
273
+ res = self.playbook_registry.upsert_agent(
274
+ name=function_name,
275
+ description=f"Generated playbook: {function_name}",
276
+ instructions=instructions_payload,
277
+ tools=tool_dict,
278
+ visibility="private",
279
+ )
280
+ except Exception as e:
281
+ raise e
282
+
283
+ writer({
284
+ "type": "custom",
285
+ id: generate_id,
286
+ "name": "generating",
287
+ "data": {"id": str(res.id), "update": bool(self.playbook)}
288
+ })
289
+ mock_assistant_message = AIMessage(content=json.dumps(response.dict()), additional_kwargs={"type": "generating", "id": str(res.id), "update": bool(self.playbook)})
290
+
291
+ return Command(
292
+ update={"messages": [mock_assistant_message], "playbook_mode": "normal"}
293
+ )
294
+
295
+ async def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
296
+ """Route to either normal mode or playbook creation"""
297
+ self.exported_tools = []
298
+ self.tools_config.extend(state.get("selected_tool_ids", []))
299
+ self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
300
+ self.final_instructions, self.tools_context = create_default_prompt(
301
+ self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
302
+ )
303
+ if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
304
+ return "playbook"
305
+ return "call_model"
306
+
140
307
  agent = StateGraph(state_schema=CodeActState)
141
308
  agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
142
- agent.add_node(sandbox)
143
- agent.add_edge(START, "call_model")
309
+ agent.add_node(playbook)
310
+ agent.add_node(execute_tools)
311
+ agent.add_conditional_edges(START, route_entry)
312
+ # agent.add_edge(START, "call_model")
144
313
  return agent.compile(checkpointer=self.memory)
@@ -1,6 +1,6 @@
1
1
  from universal_mcp.agentr.registry import AgentrRegistry
2
2
 
3
- from universal_mcp.agents.codeact0.playbook_agent import CodeActPlaybookAgent
3
+ from universal_mcp.agents.codeact0 import CodeActPlaybookAgent
4
4
 
5
5
 
6
6
  async def agent():