universal-mcp-agents 0.1.13__py3-none-any.whl → 0.1.15__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 +1 -1
- universal_mcp/agents/base.py +3 -0
- universal_mcp/agents/bigtool/__init__.py +1 -1
- universal_mcp/agents/bigtool/__main__.py +4 -3
- universal_mcp/agents/bigtool/agent.py +3 -2
- universal_mcp/agents/bigtool/graph.py +68 -31
- universal_mcp/agents/bigtool/prompts.py +2 -2
- universal_mcp/agents/bigtool/tools.py +17 -4
- universal_mcp/agents/builder/__main__.py +129 -28
- universal_mcp/agents/builder/builder.py +149 -161
- universal_mcp/agents/builder/helper.py +71 -0
- universal_mcp/agents/builder/prompts.py +94 -160
- universal_mcp/agents/codeact0/__init__.py +2 -1
- universal_mcp/agents/codeact0/agent.py +13 -5
- universal_mcp/agents/codeact0/langgraph_agent.py +14 -0
- universal_mcp/agents/codeact0/llm_tool.py +1 -2
- universal_mcp/agents/codeact0/playbook_agent.py +353 -0
- universal_mcp/agents/codeact0/prompts.py +126 -41
- universal_mcp/agents/codeact0/sandbox.py +43 -32
- universal_mcp/agents/codeact0/state.py +27 -3
- universal_mcp/agents/codeact0/tools.py +180 -0
- universal_mcp/agents/codeact0/utils.py +89 -75
- universal_mcp/agents/shared/__main__.py +44 -0
- universal_mcp/agents/shared/prompts.py +49 -98
- universal_mcp/agents/shared/tool_node.py +160 -176
- universal_mcp/agents/utils.py +71 -0
- universal_mcp/applications/ui/app.py +2 -2
- {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/METADATA +3 -3
- universal_mcp_agents-0.1.15.dist-info/RECORD +50 -0
- universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
- universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -13
- universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
- universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
- universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
- universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
- universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
- universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
- universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
- universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
- universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
- universal_mcp/agents/planner/__init__.py +0 -51
- universal_mcp/agents/planner/__main__.py +0 -28
- universal_mcp/agents/planner/graph.py +0 -85
- universal_mcp/agents/planner/prompts.py +0 -14
- universal_mcp/agents/planner/state.py +0 -11
- universal_mcp_agents-0.1.13.dist-info/RECORD +0 -63
- {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal, cast
|
|
8
|
+
|
|
9
|
+
from langchain_core.messages import AIMessage, RemoveMessage, ToolMessage
|
|
10
|
+
from langchain_core.tools import StructuredTool
|
|
11
|
+
from langchain_core.tools import tool as create_tool
|
|
12
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
13
|
+
from langgraph.graph import START, StateGraph
|
|
14
|
+
from langgraph.types import Command, RetryPolicy
|
|
15
|
+
from universal_mcp.tools.registry import ToolRegistry
|
|
16
|
+
from universal_mcp.types import ToolFormat, ToolConfig
|
|
17
|
+
|
|
18
|
+
from universal_mcp.agents.base import BaseAgent
|
|
19
|
+
from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
|
|
20
|
+
from universal_mcp.agents.codeact0.prompts import (
|
|
21
|
+
create_default_prompt,
|
|
22
|
+
)
|
|
23
|
+
from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
|
|
24
|
+
from universal_mcp.agents.codeact0.state import CodeActState
|
|
25
|
+
from universal_mcp.agents.codeact0.tools import create_meta_tools, enter_playbook_mode, exit_playbook_mode, get_valid_tools
|
|
26
|
+
from universal_mcp.agents.codeact0.utils import inject_context, smart_truncate
|
|
27
|
+
from universal_mcp.agents.llm import load_chat_model
|
|
28
|
+
from universal_mcp.agents.utils import filter_retry_on, get_message_text
|
|
29
|
+
|
|
30
|
+
PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
|
|
31
|
+
|
|
32
|
+
TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function. Do not include the searching and loading of tools. Assume that the tools have already been loaded.
|
|
33
|
+
|
|
34
|
+
Your plan should:
|
|
35
|
+
1. Identify the key steps in the workflow
|
|
36
|
+
2. Mark user-specific variables that should become the main playbook function parameters using `variable_name` syntax. Intermediate variables should not be highlighted using ``
|
|
37
|
+
3. Keep the logic generic and reusable
|
|
38
|
+
4. Be clear and concise
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```
|
|
42
|
+
1. Connect to database using `db_connection_string`
|
|
43
|
+
2. Query user data for `user_id`
|
|
44
|
+
3. Process results and calculate `metric_name`
|
|
45
|
+
4. Send notification to `email_address`
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Now create a plan based on the conversation history. Enclose it between ``` and ```. Ask the user if the plan is okay."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
PLAYBOOK_CONFIRMING_PROMPT = """Now, you are tasked with confirming the playbook plan. Return True if the user is happy with the plan, False otherwise. Do not say anything else in your response. The user response will be the last message in the chain.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function. Return the function in Python code.
|
|
56
|
+
Do not include any other text in your response.
|
|
57
|
+
The function should be a single, complete piece of code that can be executed independently, based on previously executed code snippets that executed correctly.
|
|
58
|
+
The parameters of the function should be the same as the final confirmed playbook plan.
|
|
59
|
+
Do not include anything other than python code in your response
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CodeActPlaybookAgent(BaseAgent):
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
name: str,
|
|
67
|
+
instructions: str,
|
|
68
|
+
model: str,
|
|
69
|
+
memory: BaseCheckpointSaver | None = None,
|
|
70
|
+
tools: ToolConfig | None = None,
|
|
71
|
+
registry: ToolRegistry | None = None,
|
|
72
|
+
sandbox_timeout: int = 20,
|
|
73
|
+
**kwargs,
|
|
74
|
+
):
|
|
75
|
+
super().__init__(
|
|
76
|
+
name=name,
|
|
77
|
+
instructions=instructions,
|
|
78
|
+
model=model,
|
|
79
|
+
memory=memory,
|
|
80
|
+
**kwargs,
|
|
81
|
+
)
|
|
82
|
+
self.model_instance = load_chat_model(model, thinking=True)
|
|
83
|
+
self.tools_config = tools or []
|
|
84
|
+
self.registry = registry
|
|
85
|
+
self.eval_fn = eval_unsafe
|
|
86
|
+
self.sandbox_timeout = sandbox_timeout
|
|
87
|
+
self.processed_tools: list[StructuredTool | Callable] = []
|
|
88
|
+
|
|
89
|
+
async def _build_graph(self):
|
|
90
|
+
self.exported_tools = []
|
|
91
|
+
if self.tools_config:
|
|
92
|
+
# Convert dict format to list format if needed
|
|
93
|
+
if isinstance(self.tools_config, dict):
|
|
94
|
+
self.tools_config = [
|
|
95
|
+
f"{provider}__{tool}"
|
|
96
|
+
for provider, tools in self.tools_config.items()
|
|
97
|
+
for tool in tools
|
|
98
|
+
]
|
|
99
|
+
if not self.registry:
|
|
100
|
+
raise ValueError("Tools are configured but no registry is provided")
|
|
101
|
+
# Langchain tools are fine
|
|
102
|
+
self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
|
|
103
|
+
meta_tools = create_meta_tools(self.registry)
|
|
104
|
+
await self.registry.export_tools(["exa__search_with_filters"], ToolFormat.LANGCHAIN)
|
|
105
|
+
additional_tools = [smart_print, data_extractor, ai_classify, call_llm, meta_tools["web_search"]]
|
|
106
|
+
self.additional_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in additional_tools]
|
|
107
|
+
self.final_instructions, self.tools_context = create_default_prompt(
|
|
108
|
+
self.exported_tools, self.additional_tools, self.instructions
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def call_model(state: CodeActState) -> Command[Literal["sandbox", "execute_tools"]]:
|
|
112
|
+
messages = [{"role": "system", "content": self.final_instructions}] + state["messages"]
|
|
113
|
+
|
|
114
|
+
# Run the model and potentially loop for reflection
|
|
115
|
+
model_with_tools = self.model_instance.bind_tools(
|
|
116
|
+
tools=[
|
|
117
|
+
execute_ipython_cell,
|
|
118
|
+
enter_playbook_mode,
|
|
119
|
+
meta_tools["search_functions"],
|
|
120
|
+
meta_tools["load_functions"],
|
|
121
|
+
],
|
|
122
|
+
tool_choice="auto",
|
|
123
|
+
)
|
|
124
|
+
response = cast(AIMessage, model_with_tools.invoke(messages))
|
|
125
|
+
if response.tool_calls:
|
|
126
|
+
return Command(goto="execute_tools", update={"messages": [response]})
|
|
127
|
+
else:
|
|
128
|
+
return Command(update={"messages": [response], "model_with_tools": model_with_tools})
|
|
129
|
+
|
|
130
|
+
# if response.tool_calls:
|
|
131
|
+
# if len(response.tool_calls) > 1:
|
|
132
|
+
# raise Exception("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
|
|
133
|
+
# if response.tool_calls[0]["name"] == "enter_playbook_mode":
|
|
134
|
+
# return Command(goto="playbook", update = {"playbook_mode": "planning"})
|
|
135
|
+
# if response.tool_calls[0]["name"] != "execute_ipython_cell":
|
|
136
|
+
# raise Exception(
|
|
137
|
+
# f"Unexpected tool call: {response.tool_calls[0]['name']}. Expected 'execute_ipython_cell'."
|
|
138
|
+
# )
|
|
139
|
+
# if (
|
|
140
|
+
# response.tool_calls[0]["args"].get("snippet") is None
|
|
141
|
+
# or not response.tool_calls[0]["args"]["snippet"].strip()
|
|
142
|
+
# ):
|
|
143
|
+
# raise Exception("Tool call 'execute_ipython_cell' requires a non-empty 'snippet' argument.")
|
|
144
|
+
# return Command(goto="sandbox", update={"messages": [response]})
|
|
145
|
+
# else:
|
|
146
|
+
# return Command(update={"messages": [response]})
|
|
147
|
+
|
|
148
|
+
async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "playbook", "sandbox"]]:
|
|
149
|
+
"""Execute tool calls"""
|
|
150
|
+
last_message = state["messages"][-1]
|
|
151
|
+
tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
|
|
152
|
+
|
|
153
|
+
tool_messages = []
|
|
154
|
+
new_tool_ids = []
|
|
155
|
+
ask_user = False
|
|
156
|
+
ai_msg = ""
|
|
157
|
+
tool_result = ""
|
|
158
|
+
|
|
159
|
+
for tool_call in tool_calls:
|
|
160
|
+
try:
|
|
161
|
+
if tool_call["name"] == "enter_playbook_mode":
|
|
162
|
+
tool_message = ToolMessage(
|
|
163
|
+
content=json.dumps("Entered Playbook Mode."),
|
|
164
|
+
name=tool_call["name"],
|
|
165
|
+
tool_call_id=tool_call["id"],
|
|
166
|
+
)
|
|
167
|
+
return Command(
|
|
168
|
+
goto="playbook",
|
|
169
|
+
update={"playbook_mode": "planning", "messages": [tool_message]}, #Entered Playbook mode
|
|
170
|
+
)
|
|
171
|
+
elif tool_call["name"] == "execute_ipython_cell":
|
|
172
|
+
return Command(goto="sandbox")
|
|
173
|
+
elif tool_call["name"] == "load_functions": # Handle load_functions separately
|
|
174
|
+
valid_tools, unconnected_links = await get_valid_tools(
|
|
175
|
+
tool_ids=tool_call["args"]["tool_ids"], registry=self.registry
|
|
176
|
+
)
|
|
177
|
+
new_tool_ids.extend(valid_tools)
|
|
178
|
+
# Create tool message response
|
|
179
|
+
tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
|
|
180
|
+
links = "\n".join(unconnected_links)
|
|
181
|
+
if links:
|
|
182
|
+
ask_user = True
|
|
183
|
+
ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {links} "
|
|
184
|
+
elif tool_call["name"] == "search_functions":
|
|
185
|
+
tool_result = await meta_tools["search_functions"].ainvoke(tool_call["args"])
|
|
186
|
+
except Exception as e:
|
|
187
|
+
tool_result = f"Error during {tool_call}: {e}"
|
|
188
|
+
|
|
189
|
+
tool_message = ToolMessage(
|
|
190
|
+
content=json.dumps(tool_result),
|
|
191
|
+
name=tool_call["name"],
|
|
192
|
+
tool_call_id=tool_call["id"],
|
|
193
|
+
)
|
|
194
|
+
tool_messages.append(tool_message)
|
|
195
|
+
|
|
196
|
+
if new_tool_ids:
|
|
197
|
+
self.tools_config.extend(new_tool_ids)
|
|
198
|
+
self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
|
|
199
|
+
self.final_instructions, self.tools_context = create_default_prompt(
|
|
200
|
+
self.exported_tools, self.additional_tools, self.instructions
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if ask_user:
|
|
204
|
+
tool_messages.append(AIMessage(content=ai_msg))
|
|
205
|
+
return Command(update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
|
|
206
|
+
|
|
207
|
+
return Command(goto="call_model", update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
|
|
208
|
+
|
|
209
|
+
# If eval_fn is a async, we define async node function.
|
|
210
|
+
if inspect.iscoroutinefunction(self.eval_fn):
|
|
211
|
+
raise ValueError("eval_fn must be a synchronous function, not a coroutine.")
|
|
212
|
+
# async def sandbox(state: StateSchema):
|
|
213
|
+
# existing_context = state.get("context", {})
|
|
214
|
+
# context = {**existing_context, **tools_context}
|
|
215
|
+
# # Execute the script in the sandbox
|
|
216
|
+
# output, new_vars = await eval_fn(state["script"], context)
|
|
217
|
+
# new_context = {**existing_context, **new_vars}
|
|
218
|
+
# return {
|
|
219
|
+
# "messages": [{"role": "user", "content": output}],
|
|
220
|
+
# "context": new_context,
|
|
221
|
+
# }
|
|
222
|
+
else:
|
|
223
|
+
|
|
224
|
+
def sandbox(state: CodeActState) -> Command[Literal["call_model"]]:
|
|
225
|
+
tool_call = state["messages"][-1].tool_calls[0] # type: ignore
|
|
226
|
+
code = tool_call["args"]["snippet"]
|
|
227
|
+
previous_add_context = state.get("add_context", {})
|
|
228
|
+
add_context = inject_context(previous_add_context, self.tools_context)
|
|
229
|
+
existing_context = state.get("context", {})
|
|
230
|
+
context = {**existing_context, **add_context}
|
|
231
|
+
# Execute the script in the sandbox
|
|
232
|
+
|
|
233
|
+
output, new_context, new_add_context = self.eval_fn(
|
|
234
|
+
code, context, previous_add_context, 180
|
|
235
|
+
) # default timeout 3 min
|
|
236
|
+
output = smart_truncate(output)
|
|
237
|
+
|
|
238
|
+
return Command(
|
|
239
|
+
goto="call_model",
|
|
240
|
+
update={
|
|
241
|
+
"messages": [
|
|
242
|
+
ToolMessage(
|
|
243
|
+
content=output,
|
|
244
|
+
name=tool_call["name"],
|
|
245
|
+
tool_call_id=tool_call["id"],
|
|
246
|
+
)
|
|
247
|
+
],
|
|
248
|
+
"context": new_context,
|
|
249
|
+
"add_context": new_add_context,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def playbook(state: CodeActState) -> Command[Literal["call_model"]]:
|
|
254
|
+
playbook_mode = state.get("playbook_mode")
|
|
255
|
+
if playbook_mode == "planning":
|
|
256
|
+
planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
|
|
257
|
+
messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
|
|
258
|
+
|
|
259
|
+
response = self.model_instance.invoke(messages)
|
|
260
|
+
response = cast(AIMessage, response)
|
|
261
|
+
response_text = get_message_text(response)
|
|
262
|
+
# Extract plan from response text between triple backticks
|
|
263
|
+
plan_match = re.search(r'```(.*?)```', response_text, re.DOTALL)
|
|
264
|
+
if plan_match:
|
|
265
|
+
self.plan = plan_match.group(1).strip()
|
|
266
|
+
else:
|
|
267
|
+
self.plan = response_text.strip()
|
|
268
|
+
return Command(update={"messages": [response], "playbook_mode": "confirming"})
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
elif playbook_mode == "confirming":
|
|
272
|
+
confirmation_instructions = self.instructions + PLAYBOOK_CONFIRMING_PROMPT
|
|
273
|
+
messages = [{"role": "system", "content": confirmation_instructions}] + state["messages"]
|
|
274
|
+
response = self.model_instance.invoke(messages)
|
|
275
|
+
response = get_message_text(response)
|
|
276
|
+
if "true" in response.lower():
|
|
277
|
+
return Command(goto="playbook", update={"playbook_mode": "generating"})
|
|
278
|
+
else:
|
|
279
|
+
return Command(goto="playbook", update={"playbook_mode": "planning"})
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
elif playbook_mode == "generating":
|
|
284
|
+
generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
|
|
285
|
+
messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
|
|
286
|
+
response = cast(AIMessage, self.model_instance.invoke(messages))
|
|
287
|
+
raw_content = get_message_text(response)
|
|
288
|
+
func_code = raw_content.strip()
|
|
289
|
+
func_code = func_code.replace("```python", "").replace("```", "")
|
|
290
|
+
func_code = func_code.strip()
|
|
291
|
+
|
|
292
|
+
# Extract function name (handle both regular and async functions)
|
|
293
|
+
match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
|
|
294
|
+
if match:
|
|
295
|
+
function_name = match.group(1)
|
|
296
|
+
else:
|
|
297
|
+
function_name = "generated_playbook"
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
current_path = Path(__file__).resolve()
|
|
301
|
+
repo_root = None
|
|
302
|
+
for ancestor in current_path.parents:
|
|
303
|
+
if ancestor.name == "src":
|
|
304
|
+
repo_root = ancestor.parent
|
|
305
|
+
break
|
|
306
|
+
if repo_root is None:
|
|
307
|
+
repo_root = current_path.parents[-1]
|
|
308
|
+
|
|
309
|
+
playbooks_dir = repo_root / "playbooks"
|
|
310
|
+
playbooks_dir.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
|
|
312
|
+
file_path = playbooks_dir / f"{function_name}.py"
|
|
313
|
+
file_path.write_text(func_code, encoding="utf-8")
|
|
314
|
+
saved_note = f"Playbook function saved to: {file_path} ```{func_code}```"
|
|
315
|
+
except Exception as e:
|
|
316
|
+
saved_note = f"Failed to save playbook function {function_name}: {e}"
|
|
317
|
+
|
|
318
|
+
# Mock tool call for exit_playbook_mode (for testing/demonstration)
|
|
319
|
+
mock_exit_tool_call = {
|
|
320
|
+
"name": "exit_playbook_mode",
|
|
321
|
+
"args": {},
|
|
322
|
+
"id": "mock_exit_playbook_123"
|
|
323
|
+
}
|
|
324
|
+
mock_assistant_message = AIMessage(
|
|
325
|
+
content="", # Can be empty or a brief message
|
|
326
|
+
tool_calls=[mock_exit_tool_call]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# Mock tool response for exit_playbook_mode
|
|
331
|
+
mock_exit_tool_response = ToolMessage(
|
|
332
|
+
content=json.dumps(f"Exited Playbook Mode.{saved_note}"),
|
|
333
|
+
name="exit_playbook_mode",
|
|
334
|
+
tool_call_id="mock_exit_playbook_123"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "playbook_mode": "normal"})
|
|
338
|
+
|
|
339
|
+
def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
|
|
340
|
+
"""Route to either normal mode or playbook creation"""
|
|
341
|
+
if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
|
|
342
|
+
return "playbook"
|
|
343
|
+
|
|
344
|
+
return "call_model"
|
|
345
|
+
|
|
346
|
+
agent = StateGraph(state_schema=CodeActState)
|
|
347
|
+
agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
|
|
348
|
+
agent.add_node(sandbox)
|
|
349
|
+
agent.add_node(playbook)
|
|
350
|
+
agent.add_node(execute_tools)
|
|
351
|
+
agent.add_conditional_edges(START, route_entry)
|
|
352
|
+
# agent.add_edge(START, "call_model")
|
|
353
|
+
return agent.compile(checkpointer=self.memory)
|
|
@@ -1,33 +1,112 @@
|
|
|
1
|
-
uneditable_prompt = """
|
|
2
|
-
You are Wingman, an AI Assistant created by AgentR. You are a creative, straight-forward and direct principal software engineer.
|
|
3
|
-
|
|
4
|
-
Your job is to answer the user's question or perform the task they ask for.
|
|
5
|
-
- Answer simple questions (which do not require you to write any code or access any external resources) directly. Note that any operation that involves using ONLY print functions should be answered directly.
|
|
6
|
-
- For task requiring operations or access to external resources, you should achieve the task by executing Python code snippets.
|
|
7
|
-
- You have access to `execute_ipython_cell` tool that allows you to execute Python code in an IPython notebook cell.
|
|
8
|
-
- In writing or natural language processing tasks DO NOT answer directly. Instead use `execute_ipython_cell` tool with the AI functions provided to you for tasks like summarizing, text generation, classification, data extraction from text or unstructured data, etc.
|
|
9
|
-
- The code you write will be executed in a sandbox environment, and you can use the output of previous executions in your code.
|
|
10
|
-
- Read and understand the output of the previous code snippet and use it to answer the user's request. Note that the code output is not visible to the user, so after the task is complete, you have to give the output to the user in a markdown format.
|
|
11
|
-
- If needed, feel free to ask for more information from the user (without using the execute_ipython_cell tool) to clarify the task.
|
|
12
|
-
|
|
13
|
-
GUIDELINES for writing code:
|
|
14
|
-
- Variables defined at the top level of previous code snippets can be referenced in your code.
|
|
15
|
-
- External functions which return a dict or list[dict] are ambiguous. Therefore, you MUST explore the structure of the returned data using `smart_print()` statements before using it, printing keys and values.
|
|
16
|
-
- Ensure to not print large amounts of data, use string truncation to limit the output to a few lines when checking the data structure.
|
|
17
|
-
- When an operation involves running a fixed set of steps on a list of items, run one run correctly and then use a for loop to run the steps on each item in the list.
|
|
18
|
-
- In a single code snippet, try to achieve as much as possible.
|
|
19
|
-
- You can only import libraries that come pre-installed with Python, and these have to be imported at the top of every code snippet where required.
|
|
20
|
-
- Wrap await calls in a function and call it using `asyncio.run` to use async functions.
|
|
21
|
-
|
|
22
|
-
NOTE: If any function throws an error requiring authentication, provide the user with a Markdown link to the authentication page and prompt them to authenticate.
|
|
23
|
-
"""
|
|
24
1
|
import inspect
|
|
25
2
|
import re
|
|
26
3
|
from collections.abc import Sequence
|
|
27
|
-
from datetime import datetime
|
|
28
4
|
|
|
29
5
|
from langchain_core.tools import StructuredTool
|
|
30
6
|
|
|
7
|
+
from universal_mcp.agents.codeact0.utils import schema_to_signature
|
|
8
|
+
|
|
9
|
+
uneditable_prompt = """
|
|
10
|
+
You are **Wingmen**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
|
|
11
|
+
|
|
12
|
+
## Responsibilities
|
|
13
|
+
|
|
14
|
+
- **Answer directly** if the task is simple (e.g. print, math, general knowledge).
|
|
15
|
+
- For any task requiring logic, execution, or data handling, use `execute_ipython_cell`.
|
|
16
|
+
- For writing or NLP tasks (summarizing, generating, extracting), always use AI functions via code — never respond directly.
|
|
17
|
+
|
|
18
|
+
## Tool vs. Function: Required Separation
|
|
19
|
+
|
|
20
|
+
You must clearly distinguish between tools (called via the tool calling API) and internal functions (used inside code blocks).
|
|
21
|
+
|
|
22
|
+
### Tools — Must Be Called via Tool Calling API
|
|
23
|
+
|
|
24
|
+
These must be called using **tool calling**, not from inside code blocks:
|
|
25
|
+
|
|
26
|
+
- `execute_ipython_cell` — For running any Python code or logic.
|
|
27
|
+
- `search_functions` — To discover available functions for a task.
|
|
28
|
+
- `load_functions` — To load a specific function by full ID.
|
|
29
|
+
|
|
30
|
+
**Do not attempt to call these inside `python` code.**
|
|
31
|
+
Use tool calling syntax for these operations.
|
|
32
|
+
|
|
33
|
+
### Functions — Must Be Used Inside Code Blocks
|
|
34
|
+
|
|
35
|
+
All other functions, including LLM functions, must always be used within code executed by `execute_ipython_cell`. These include:
|
|
36
|
+
|
|
37
|
+
- `smart_print()` — For inspecting unknown data structures before looping.
|
|
38
|
+
- `asyncio.run()` — For wrapping and executing asynchronous logic. You must not use await outside an async function. And the async function must be called by `asyncio.run()`.
|
|
39
|
+
- Any functions for applications loaded via `load_functions`.
|
|
40
|
+
- Any logic, data handling, writing, NLP, generation, summarization, or extraction functionality of LLMs.
|
|
41
|
+
|
|
42
|
+
These must be called **inside a Python code block**, and that block must be executed using `execute_ipython_cell`.
|
|
43
|
+
|
|
44
|
+
## Tool/Function Usage Policy
|
|
45
|
+
|
|
46
|
+
1. **Always Use Tools/Functions for Required Tasks**
|
|
47
|
+
Any searching, loading, or executing must be done using a tool/function call. Never answer manually if a tool/function is appropriate.
|
|
48
|
+
|
|
49
|
+
2. **Use Existing Functions First**
|
|
50
|
+
Use existing functions if available. Otherwise, use `search_functions` with a concise query describing the task.
|
|
51
|
+
|
|
52
|
+
3. **Load Only Relevant Tools**
|
|
53
|
+
When calling `load_functions`, include only relevant function IDs.
|
|
54
|
+
- Prefer connected applications over unconnected ones.
|
|
55
|
+
- If multiple functions match (i.e. if none are connected, or multiple are connected), ask the user to choose.
|
|
56
|
+
- After loading a tool, you do not need to import/declare it again. It can be called directly in further cells.
|
|
57
|
+
|
|
58
|
+
4. **Follow First Turn Process Strictly**
|
|
59
|
+
On the **first turn**, do only **one** of the following:
|
|
60
|
+
- Handle directly (if trivial)
|
|
61
|
+
- Use a tool/function (`execute_ipython_cell`, `search_functions`, etc.)
|
|
62
|
+
|
|
63
|
+
**Do not extend the conversation on the first message.**
|
|
64
|
+
|
|
65
|
+
## Coding Rules
|
|
66
|
+
|
|
67
|
+
- Use `smart_print()` to inspect unknown structures, especially those received from function outputs, before looping or branching.
|
|
68
|
+
- Validate logic with a single item before processing lists or large inputs.
|
|
69
|
+
- Try to achieve as much as possible in a single code block.
|
|
70
|
+
- Use only pre-installed Python libraries. Do import them once before using.
|
|
71
|
+
- Outer level functions, variables, classes, and imports declared previously can be used in later cells.
|
|
72
|
+
- For all functions, call using keyword arguments only. DO NOT use any positional arguments.
|
|
73
|
+
|
|
74
|
+
### **Async Function Usage — Critical**
|
|
75
|
+
|
|
76
|
+
When calling asynchronous functions:
|
|
77
|
+
- You must define or use an **inner async function**.
|
|
78
|
+
- Use `await` only **inside** that async function.
|
|
79
|
+
- Run it using `asyncio.run(<function_name>())` **without** `await` at the outer level.
|
|
80
|
+
|
|
81
|
+
**Wrong - Using `await` outside an async function**
|
|
82
|
+
```
|
|
83
|
+
result = await some_async_function()
|
|
84
|
+
```
|
|
85
|
+
**Wrong - Attaching await before asyncio.run**.
|
|
86
|
+
`await asyncio.run(main())`
|
|
87
|
+
These will raise SyntaxError: 'await' outside async function
|
|
88
|
+
The correct method is the following-
|
|
89
|
+
```
|
|
90
|
+
import asyncio
|
|
91
|
+
async def some_async_function():
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
result = await some_async_function()
|
|
96
|
+
print(result)
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
#or
|
|
100
|
+
result = asyncio.run(some_async_function(arg1 = <arg1>))
|
|
101
|
+
```
|
|
102
|
+
## Output Formatting
|
|
103
|
+
- All code results must be returned in **Markdown**.
|
|
104
|
+
- The user cannot see raw output, so format results clearly:
|
|
105
|
+
- Use tables for structured data.
|
|
106
|
+
- Provide links for files or images.
|
|
107
|
+
- Be explicit in formatting to ensure readability.
|
|
108
|
+
"""
|
|
109
|
+
|
|
31
110
|
|
|
32
111
|
def make_safe_function_name(name: str) -> str:
|
|
33
112
|
"""Convert a tool name to a valid Python function name."""
|
|
@@ -121,6 +200,7 @@ def indent(text, prefix, predicate=None):
|
|
|
121
200
|
|
|
122
201
|
def create_default_prompt(
|
|
123
202
|
tools: Sequence[StructuredTool],
|
|
203
|
+
additional_tools: Sequence[StructuredTool],
|
|
124
204
|
base_prompt: str | None = None,
|
|
125
205
|
):
|
|
126
206
|
system_prompt = uneditable_prompt.strip() + (
|
|
@@ -128,28 +208,33 @@ def create_default_prompt(
|
|
|
128
208
|
)
|
|
129
209
|
tools_context = {}
|
|
130
210
|
for tool in tools:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
211
|
+
if hasattr(tool, "func") and tool.func is not None:
|
|
212
|
+
tool_callable = tool.func
|
|
213
|
+
is_async = False
|
|
214
|
+
elif hasattr(tool, "coroutine") and tool.coroutine is not None:
|
|
135
215
|
tool_callable = tool.coroutine
|
|
136
|
-
signature = inspect.signature(tool_callable)
|
|
137
216
|
is_async = True
|
|
138
|
-
|
|
217
|
+
system_prompt += f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
|
|
218
|
+
"""{tool.description}"""
|
|
219
|
+
...
|
|
220
|
+
'''
|
|
221
|
+
safe_name = make_safe_function_name(tool.name)
|
|
222
|
+
tools_context[safe_name] = tool_callable
|
|
223
|
+
|
|
224
|
+
for tool in additional_tools:
|
|
225
|
+
if hasattr(tool, "func") and tool.func is not None:
|
|
139
226
|
tool_callable = tool.func
|
|
140
|
-
signature = inspect.signature(tool_callable)
|
|
141
227
|
is_async = False
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
system_prompt += f'''
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
{indent(dedent(tool.description), " ")}
|
|
149
|
-
"""
|
|
228
|
+
elif hasattr(tool, "coroutine") and tool.coroutine is not None:
|
|
229
|
+
tool_callable = tool.coroutine
|
|
230
|
+
is_async = True
|
|
231
|
+
system_prompt += f'''{"async " if is_async else ""}def {tool.name} {str(inspect.signature(tool_callable))}:
|
|
232
|
+
"""{tool.description}"""
|
|
150
233
|
...
|
|
151
|
-
'''
|
|
152
|
-
|
|
234
|
+
'''
|
|
235
|
+
safe_name = make_safe_function_name(tool.name)
|
|
236
|
+
tools_context[safe_name] = tool_callable
|
|
237
|
+
|
|
153
238
|
if base_prompt and base_prompt.strip():
|
|
154
239
|
system_prompt += f"Your goal is to perform the following task:\n\n{base_prompt}"
|
|
155
240
|
|
|
@@ -14,55 +14,66 @@ from universal_mcp.agents.codeact0.utils import derive_context
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def eval_unsafe(
|
|
17
|
-
code: str, _locals: dict[str, Any], add_context: dict[str, Any]
|
|
17
|
+
code: str, _locals: dict[str, Any], add_context: dict[str, Any], timeout: int = 180
|
|
18
18
|
) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
19
|
-
|
|
19
|
+
"""
|
|
20
|
+
Execute code safely with a timeout.
|
|
21
|
+
- Returns (output_str, filtered_locals_dict, new_add_context)
|
|
22
|
+
- Errors or timeout are returned as output_str.
|
|
23
|
+
- Previous variables in _locals persist across calls.
|
|
24
|
+
"""
|
|
25
|
+
|
|
20
26
|
EXCLUDE_TYPES = (
|
|
21
|
-
types.ModuleType,
|
|
27
|
+
types.ModuleType,
|
|
22
28
|
type(re.match("", "")),
|
|
23
|
-
type(threading.Lock()),
|
|
24
|
-
type(threading.RLock()),
|
|
25
|
-
threading.Event,
|
|
26
|
-
threading.Condition,
|
|
27
|
-
threading.Semaphore,
|
|
28
|
-
queue.Queue,
|
|
29
|
-
socket.socket,
|
|
30
|
-
io.IOBase,
|
|
29
|
+
type(threading.Lock()),
|
|
30
|
+
type(threading.RLock()),
|
|
31
|
+
threading.Event,
|
|
32
|
+
threading.Condition,
|
|
33
|
+
threading.Semaphore,
|
|
34
|
+
queue.Queue,
|
|
35
|
+
socket.socket,
|
|
36
|
+
io.IOBase,
|
|
31
37
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
|
|
39
|
+
result_container = {"output": "<no output>"}
|
|
40
|
+
|
|
41
|
+
def target():
|
|
42
|
+
try:
|
|
43
|
+
with contextlib.redirect_stdout(io.StringIO()) as f:
|
|
44
|
+
exec(code, _locals, _locals)
|
|
45
|
+
result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
|
|
46
|
+
except Exception as e:
|
|
47
|
+
result_container["output"] = "Error during execution: " + str(e)
|
|
48
|
+
|
|
49
|
+
thread = threading.Thread(target=target)
|
|
50
|
+
thread.start()
|
|
51
|
+
thread.join(timeout)
|
|
52
|
+
|
|
53
|
+
if thread.is_alive():
|
|
54
|
+
result_container["output"] = f"Code timeout: code execution exceeded {timeout} seconds."
|
|
55
|
+
|
|
56
|
+
# Filter locals for picklable/storable variables
|
|
45
57
|
all_vars = {}
|
|
46
58
|
for key, value in _locals.items():
|
|
47
59
|
if key == "__builtins__":
|
|
48
60
|
continue
|
|
49
|
-
|
|
50
|
-
# Skip coroutines, async generators, and coroutine functions
|
|
51
61
|
if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
|
|
52
62
|
continue
|
|
53
63
|
if inspect.isasyncgen(value) or inspect.isasyncgenfunction(value):
|
|
54
64
|
continue
|
|
55
|
-
|
|
56
|
-
# Skip "obviously unpicklable" types
|
|
57
65
|
if isinstance(value, EXCLUDE_TYPES):
|
|
58
66
|
continue
|
|
59
|
-
|
|
60
|
-
# Keep if it's not a callable OR if it has no __name__ attribute
|
|
61
67
|
if not callable(value) or not hasattr(value, "__name__"):
|
|
62
68
|
all_vars[key] = value
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
# Safely derive context
|
|
71
|
+
try:
|
|
72
|
+
new_add_context = derive_context(code, add_context)
|
|
73
|
+
except Exception:
|
|
74
|
+
new_add_context = add_context
|
|
75
|
+
|
|
76
|
+
return result_container["output"], all_vars, new_add_context
|
|
66
77
|
|
|
67
78
|
|
|
68
79
|
@tool(parse_docstring=True)
|