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.
- universal_mcp/agents/__init__.py +5 -9
- universal_mcp/agents/base.py +33 -29
- universal_mcp/agents/cli.py +0 -3
- universal_mcp/agents/codeact0/__init__.py +2 -3
- universal_mcp/agents/codeact0/__main__.py +2 -2
- universal_mcp/agents/codeact0/agent.py +256 -87
- universal_mcp/agents/codeact0/langgraph_agent.py +1 -1
- universal_mcp/agents/codeact0/llm_tool.py +2 -254
- universal_mcp/agents/codeact0/prompts.py +88 -104
- universal_mcp/agents/codeact0/sandbox.py +31 -1
- universal_mcp/agents/codeact0/state.py +14 -3
- universal_mcp/agents/codeact0/tools.py +189 -83
- universal_mcp/agents/codeact0/utils.py +8 -0
- universal_mcp/applications/llm/app.py +2 -2
- {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.21.dist-info}/METADATA +1 -1
- universal_mcp_agents-0.1.21.dist-info/RECORD +44 -0
- universal_mcp/agents/codeact/__init__.py +0 -3
- universal_mcp/agents/codeact/__main__.py +0 -33
- 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/__main__.py +0 -28
- universal_mcp/agents/unified/agent.py +0 -289
- universal_mcp/agents/unified/langgraph_agent.py +0 -14
- universal_mcp/agents/unified/llm_tool.py +0 -25
- universal_mcp/agents/unified/prompts.py +0 -192
- universal_mcp/agents/unified/sandbox.py +0 -101
- universal_mcp/agents/unified/state.py +0 -42
- universal_mcp/agents/unified/tools.py +0 -188
- universal_mcp/agents/unified/utils.py +0 -388
- universal_mcp_agents-0.1.19rc1.dist-info/RECORD +0 -64
- {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.21.dist-info}/WHEEL +0 -0
universal_mcp/agents/__init__.py
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
|
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"
|
universal_mcp/agents/base.py
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Send a final finished message
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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":
|
|
120
|
+
"recursion_limit": 50,
|
|
117
121
|
"configurable": {"thread_id": thread_id},
|
|
118
122
|
"metadata": run_metadata,
|
|
119
123
|
"run_id": thread_id,
|
universal_mcp/agents/cli.py
CHANGED
|
@@ -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)
|
|
@@ -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
|
|
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 =
|
|
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
|
|
2
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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["
|
|
74
|
-
messages = [{"role": "system", "content": self.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
143
|
-
agent.
|
|
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)
|