universal-mcp-agents 0.1.20rc1__py3-none-any.whl → 0.1.22__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/base.py +29 -28
- universal_mcp/agents/codeact0/agent.py +100 -57
- universal_mcp/agents/codeact0/llm_tool.py +2 -254
- universal_mcp/agents/codeact0/prompts.py +93 -104
- universal_mcp/agents/codeact0/state.py +20 -2
- universal_mcp/agents/codeact0/tools.py +18 -28
- universal_mcp/agents/codeact0/utils.py +56 -11
- universal_mcp/applications/llm/app.py +2 -2
- {universal_mcp_agents-0.1.20rc1.dist-info → universal_mcp_agents-0.1.22.dist-info}/METADATA +1 -1
- {universal_mcp_agents-0.1.20rc1.dist-info → universal_mcp_agents-0.1.22.dist-info}/RECORD +11 -11
- {universal_mcp_agents-0.1.20rc1.dist-info → universal_mcp_agents-0.1.22.dist-info}/WHEEL +0 -0
universal_mcp/agents/base.py
CHANGED
|
@@ -56,40 +56,41 @@ class BaseAgent:
|
|
|
56
56
|
"run_name": self.name,
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
last_ai_chunk = None
|
|
59
60
|
async for event, meta in self._graph.astream(
|
|
60
61
|
{"messages": [{"role": "user", "content": user_input}]},
|
|
61
62
|
config=run_config,
|
|
62
63
|
context={"system_prompt": self.instructions, "model": self.model},
|
|
63
|
-
stream_mode="messages",
|
|
64
|
+
stream_mode=["messages", "custom"],
|
|
64
65
|
stream_usage=True,
|
|
65
66
|
):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Send a final finished message
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
93
94
|
|
|
94
95
|
async def stream_interactive(self, thread_id: str, user_input: str):
|
|
95
96
|
await self.ainit()
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
|
-
from collections.abc import Callable
|
|
4
3
|
from typing import Literal, cast
|
|
4
|
+
import uuid
|
|
5
5
|
|
|
6
6
|
from langchain_core.messages import AIMessage, ToolMessage
|
|
7
7
|
from langchain_core.tools import StructuredTool
|
|
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_CONFIRMING_PROMPT,
|
|
18
17
|
PLAYBOOK_GENERATING_PROMPT,
|
|
19
18
|
PLAYBOOK_PLANNING_PROMPT,
|
|
19
|
+
PLAYBOOK_META_PROMPT,
|
|
20
20
|
create_default_prompt,
|
|
21
21
|
)
|
|
22
22
|
from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
|
|
23
|
-
from universal_mcp.agents.codeact0.state import CodeActState
|
|
23
|
+
from universal_mcp.agents.codeact0.state import CodeActState, PlaybookCode, PlaybookPlan, PlaybookMeta
|
|
24
24
|
from universal_mcp.agents.codeact0.tools import (
|
|
25
25
|
create_meta_tools,
|
|
26
26
|
enter_playbook_mode,
|
|
27
27
|
get_valid_tools,
|
|
28
28
|
)
|
|
29
|
+
from universal_mcp.agents.codeact0.utils import add_tools
|
|
29
30
|
from universal_mcp.agents.llm import load_chat_model
|
|
30
31
|
from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
|
|
32
|
+
from universal_mcp.agents.codeact0.utils import get_connected_apps_string
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class CodeActPlaybookAgent(BaseAgent):
|
|
@@ -51,16 +53,22 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
51
53
|
**kwargs,
|
|
52
54
|
)
|
|
53
55
|
self.model_instance = load_chat_model(model)
|
|
54
|
-
self.
|
|
56
|
+
self.playbook_model_instance = load_chat_model("azure/gpt-4.1")
|
|
57
|
+
self.tools_config = tools or {}
|
|
55
58
|
self.registry = registry
|
|
56
59
|
self.playbook_registry = playbook_registry
|
|
60
|
+
self.playbook = playbook_registry.get_agent() if playbook_registry else None
|
|
57
61
|
self.eval_fn = eval_unsafe
|
|
58
62
|
self.sandbox_timeout = sandbox_timeout
|
|
59
|
-
self.
|
|
63
|
+
self.default_tools = {
|
|
64
|
+
"llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
|
|
65
|
+
}
|
|
66
|
+
add_tools(self.tools_config, self.default_tools)
|
|
67
|
+
|
|
60
68
|
|
|
61
69
|
async def _build_graph(self):
|
|
62
70
|
meta_tools = create_meta_tools(self.registry)
|
|
63
|
-
additional_tools = [smart_print,
|
|
71
|
+
additional_tools = [smart_print, meta_tools["web_search"]]
|
|
64
72
|
self.additional_tools = [
|
|
65
73
|
t if isinstance(t, StructuredTool) else StructuredTool.from_function(t) for t in additional_tools
|
|
66
74
|
]
|
|
@@ -145,7 +153,7 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
145
153
|
else:
|
|
146
154
|
raise Exception(
|
|
147
155
|
f"Unexpected tool call: {tool_call['name']}. "
|
|
148
|
-
"tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'"
|
|
156
|
+
"tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'. For using functions, call them in code using 'execute_ipython_cell'."
|
|
149
157
|
)
|
|
150
158
|
except Exception as e:
|
|
151
159
|
tool_result = str(e)
|
|
@@ -161,7 +169,7 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
161
169
|
self.tools_config.extend(new_tool_ids)
|
|
162
170
|
self.exported_tools = await self.registry.export_tools(new_tool_ids, ToolFormat.LANGCHAIN)
|
|
163
171
|
self.final_instructions, self.tools_context = create_default_prompt(
|
|
164
|
-
self.exported_tools, self.additional_tools, self.instructions
|
|
172
|
+
self.exported_tools, self.additional_tools, self.instructions, await get_connected_apps_string(self.registry)
|
|
165
173
|
)
|
|
166
174
|
if ask_user:
|
|
167
175
|
tool_messages.append(AIMessage(content=ai_msg))
|
|
@@ -184,41 +192,80 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
184
192
|
},
|
|
185
193
|
)
|
|
186
194
|
|
|
187
|
-
def playbook(state: CodeActState) -> Command[Literal["call_model"]]:
|
|
195
|
+
def playbook(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
|
|
188
196
|
playbook_mode = state.get("playbook_mode")
|
|
189
197
|
if playbook_mode == "planning":
|
|
198
|
+
plan_id = str(uuid.uuid4())
|
|
199
|
+
writer({
|
|
200
|
+
"type": "custom",
|
|
201
|
+
id: plan_id,
|
|
202
|
+
"name": "planning",
|
|
203
|
+
"data": {"update": bool(self.playbook)}
|
|
204
|
+
})
|
|
190
205
|
planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
|
|
191
206
|
messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
|
|
192
207
|
|
|
193
|
-
|
|
194
|
-
response =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
plan = plan_match.group(1).strip()
|
|
200
|
-
else:
|
|
201
|
-
plan = response_text.strip()
|
|
202
|
-
return Command(update={"messages": [response], "playbook_mode": "confirming", "plan": plan})
|
|
208
|
+
model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookPlan)
|
|
209
|
+
response = model_with_structured_output.invoke(messages)
|
|
210
|
+
plan = cast(PlaybookPlan, response)
|
|
211
|
+
|
|
212
|
+
writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
|
|
213
|
+
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})
|
|
203
214
|
|
|
204
215
|
elif playbook_mode == "confirming":
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
# Deterministic routing based on three exact button inputs from UI
|
|
217
|
+
user_text = ""
|
|
218
|
+
for m in reversed(state["messages"]):
|
|
219
|
+
try:
|
|
220
|
+
if getattr(m, "type", "") in {"human", "user"}:
|
|
221
|
+
user_text = (get_message_text(m) or "").strip()
|
|
222
|
+
if user_text:
|
|
223
|
+
break
|
|
224
|
+
except Exception:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
t = user_text.lower()
|
|
228
|
+
if t == "yes, this is great":
|
|
229
|
+
# Generate playbook metadata (name and description) before moving to generation
|
|
230
|
+
meta_id = str(uuid.uuid4())
|
|
231
|
+
writer({
|
|
232
|
+
"type": "custom",
|
|
233
|
+
id: meta_id,
|
|
234
|
+
"name": "metadata",
|
|
235
|
+
"data": {"update": bool(self.playbook)}
|
|
236
|
+
})
|
|
237
|
+
meta_instructions = self.instructions + PLAYBOOK_META_PROMPT
|
|
238
|
+
messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
|
|
239
|
+
|
|
240
|
+
model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookMeta)
|
|
241
|
+
meta_response = model_with_structured_output.invoke(messages)
|
|
242
|
+
meta = cast(PlaybookMeta, meta_response)
|
|
243
|
+
|
|
244
|
+
writer({"type": "custom", id: meta_id, "name": "metadata", "data": {"name": meta.name, "description": meta.description}})
|
|
245
|
+
return Command(goto="playbook", update={"playbook_mode": "generating", "playbook_name": meta.name, "playbook_description": meta.description})
|
|
246
|
+
if t == "i would like to modify the plan":
|
|
247
|
+
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"})
|
|
248
|
+
return Command(update={"playbook_mode": "planning", "messages": [prompt_ai]})
|
|
249
|
+
if t == "let's do something else":
|
|
250
|
+
return Command(goto="call_model", update={"playbook_mode": "inactive"})
|
|
251
|
+
|
|
252
|
+
# Fallback safe default
|
|
253
|
+
return Command(goto="call_model", update={"playbook_mode": "inactive"})
|
|
213
254
|
|
|
214
255
|
elif playbook_mode == "generating":
|
|
256
|
+
generate_id = str(uuid.uuid4())
|
|
257
|
+
writer({
|
|
258
|
+
"type": "custom",
|
|
259
|
+
id: generate_id,
|
|
260
|
+
"name": "generating",
|
|
261
|
+
"data": {"update": bool(self.playbook)}
|
|
262
|
+
})
|
|
215
263
|
generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
|
|
216
264
|
messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
func_code =
|
|
221
|
-
func_code = func_code.strip()
|
|
265
|
+
|
|
266
|
+
model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookCode)
|
|
267
|
+
response = model_with_structured_output.invoke(messages)
|
|
268
|
+
func_code = cast(PlaybookCode, response).code
|
|
222
269
|
|
|
223
270
|
# Extract function name (handle both regular and async functions)
|
|
224
271
|
match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
|
|
@@ -227,48 +274,44 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
227
274
|
else:
|
|
228
275
|
function_name = "generated_playbook"
|
|
229
276
|
|
|
277
|
+
# Use generated metadata if available
|
|
278
|
+
final_name = state.get("playbook_name") or function_name
|
|
279
|
+
final_description = state.get("playbook_description") or f"Generated playbook: {function_name}"
|
|
280
|
+
|
|
230
281
|
# Save or update an Agent using the helper registry
|
|
231
|
-
saved_note = ""
|
|
232
282
|
try:
|
|
233
|
-
if not self.playbook_registry:
|
|
283
|
+
if not self.playbook_registry:
|
|
234
284
|
raise ValueError("Playbook registry is not configured")
|
|
235
285
|
|
|
236
286
|
# Build instructions payload embedding the plan and function code
|
|
237
287
|
instructions_payload = {
|
|
238
288
|
"playbookPlan": state["plan"],
|
|
239
|
-
"playbookScript":
|
|
240
|
-
"name": function_name,
|
|
241
|
-
"code": func_code,
|
|
242
|
-
},
|
|
289
|
+
"playbookScript": func_code,
|
|
243
290
|
}
|
|
244
291
|
|
|
245
292
|
# Convert tool ids list to dict
|
|
246
293
|
tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
|
|
247
294
|
|
|
248
|
-
res = self.playbook_registry.
|
|
249
|
-
name=
|
|
250
|
-
description=
|
|
295
|
+
res = self.playbook_registry.upsert_agent(
|
|
296
|
+
name=final_name,
|
|
297
|
+
description=final_description,
|
|
251
298
|
instructions=instructions_payload,
|
|
252
299
|
tools=tool_dict,
|
|
253
300
|
visibility="private",
|
|
254
301
|
)
|
|
255
|
-
saved_note = f"Successfully created your playbook! Check it out here: [View Playbook](https://wingmen.info/agents/{res.id})"
|
|
256
302
|
except Exception as e:
|
|
257
|
-
|
|
303
|
+
raise e
|
|
258
304
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
name="exit_playbook_mode",
|
|
267
|
-
tool_call_id="mock_exit_playbook_123",
|
|
268
|
-
)
|
|
305
|
+
writer({
|
|
306
|
+
"type": "custom",
|
|
307
|
+
id: generate_id,
|
|
308
|
+
"name": "generating",
|
|
309
|
+
"data": {"id": str(res.id), "update": bool(self.playbook)}
|
|
310
|
+
})
|
|
311
|
+
mock_assistant_message = AIMessage(content=json.dumps(response.dict()), additional_kwargs={"type": "generating", "id": str(res.id), "update": bool(self.playbook)})
|
|
269
312
|
|
|
270
313
|
return Command(
|
|
271
|
-
update={"messages": [mock_assistant_message
|
|
314
|
+
update={"messages": [mock_assistant_message], "playbook_mode": "normal"}
|
|
272
315
|
)
|
|
273
316
|
|
|
274
317
|
async def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
|
|
@@ -277,7 +320,7 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
277
320
|
self.tools_config.extend(state.get("selected_tool_ids", []))
|
|
278
321
|
self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
|
|
279
322
|
self.final_instructions, self.tools_context = create_default_prompt(
|
|
280
|
-
self.exported_tools, self.additional_tools, self.instructions
|
|
323
|
+
self.exported_tools, self.additional_tools, self.instructions, await get_connected_apps_string(self.registry)
|
|
281
324
|
)
|
|
282
325
|
if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
|
|
283
326
|
return "playbook"
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Literal, cast
|
|
1
|
+
from typing import Any
|
|
4
2
|
|
|
5
|
-
from
|
|
6
|
-
from langchain_openai import AzureChatOpenAI
|
|
7
|
-
|
|
8
|
-
from universal_mcp.agents.codeact0.utils import get_message_text, light_copy
|
|
3
|
+
from universal_mcp.agents.codeact0.utils import light_copy
|
|
9
4
|
|
|
10
5
|
MAX_RETRIES = 3
|
|
11
6
|
|
|
@@ -28,250 +23,3 @@ def smart_print(data: Any) -> None:
|
|
|
28
23
|
data: Either a dictionary with string keys, or a list of such dictionaries
|
|
29
24
|
"""
|
|
30
25
|
print(light_copy(data)) # noqa: T201
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def creative_writer(
|
|
34
|
-
task: str,
|
|
35
|
-
context: Any | list[Any] | dict[str, Any],
|
|
36
|
-
tone: str = "normal",
|
|
37
|
-
format: Literal["markdown", "html", "plain"] = "markdown",
|
|
38
|
-
length: Literal["very-short", "concise", "normal", "long"] = "concise",
|
|
39
|
-
) -> str:
|
|
40
|
-
"""
|
|
41
|
-
Given a high-level writing task and context, returns a well-written text
|
|
42
|
-
that achieves the task, given the context.
|
|
43
|
-
|
|
44
|
-
Example Call:
|
|
45
|
-
creative_writer("Summarize this website with the goal of making it easy to understand.", web_content)
|
|
46
|
-
creative_writer("Make a markdown table summarizing the key differences between doc_1 and doc_2.", {"doc_1": str(doc_1), "doc_2": str(doc_2)})
|
|
47
|
-
creative_writer("Summarize all the provided documents.", [doc_1, doc_2, doc_3])
|
|
48
|
-
|
|
49
|
-
Important:
|
|
50
|
-
- Include specifics of the goal in the context verbatim.
|
|
51
|
-
- Be precise and direct in the task, and include as much context as possible.
|
|
52
|
-
- Include relevant high-level goals or intent in the task.
|
|
53
|
-
- You can provide multiple documents as input, and reference them in the task.
|
|
54
|
-
- You MUST provide the contents of any source documents to `creative_writer`.
|
|
55
|
-
- NEVER use `creative_writer` to produce JSON for a Pydantic model.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
task: The main writing task or directive.
|
|
59
|
-
context: A single string, list of strings, or dict mapping labels to content.
|
|
60
|
-
tone: The desired tone of the output (e.g., "normal", "flirty", "formal", "casual", "crisp", "poetic", "technical", "internet-chat", "smartass", etc.).
|
|
61
|
-
format: Output format ('markdown', 'html', 'plain-text').
|
|
62
|
-
length: Desired length of the output ('very-short', 'concise', 'normal', 'long').
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
str: The generated text output.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
context = get_context_str(context)
|
|
69
|
-
|
|
70
|
-
task = task.strip() + "\n\n"
|
|
71
|
-
if format == "markdown":
|
|
72
|
-
task += "Please write in Markdown format.\n\n"
|
|
73
|
-
elif format == "html":
|
|
74
|
-
task += "Please write in HTML format.\n\n"
|
|
75
|
-
else:
|
|
76
|
-
task += "Please write in plain text format. Don't use markdown or HTML.\n\n"
|
|
77
|
-
|
|
78
|
-
if tone not in ["normal", "default", ""]:
|
|
79
|
-
task = f"{task} (Tone instructions: {tone})"
|
|
80
|
-
|
|
81
|
-
if length not in ["normal", "default", ""]:
|
|
82
|
-
task = f"{task} (Length instructions: {length})"
|
|
83
|
-
|
|
84
|
-
prompt = f"{task}\n\nContext:\n{context}\n\n"
|
|
85
|
-
|
|
86
|
-
model = AzureChatOpenAI(model="gpt-4o", temperature=0.7)
|
|
87
|
-
|
|
88
|
-
response = model.with_retry(stop_after_attempt=MAX_RETRIES).invoke(prompt)
|
|
89
|
-
return get_message_text(response)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def ai_classify(
|
|
93
|
-
classification_task_and_requirements: str,
|
|
94
|
-
context: Any | list[Any] | dict[str, Any],
|
|
95
|
-
class_descriptions: dict[str, str],
|
|
96
|
-
) -> dict[str, Any]:
|
|
97
|
-
"""
|
|
98
|
-
Classifies and compares data based on given requirements.
|
|
99
|
-
|
|
100
|
-
Use `ai_classify` for tasks which need to classify data into one of many categories.
|
|
101
|
-
If making multiple binary classifications, call `ai_classify` for each.
|
|
102
|
-
|
|
103
|
-
Guidance:
|
|
104
|
-
- Prefer to use ai_classify operations to compare strings, rather than string ops.
|
|
105
|
-
- Prefer to include an "Unsure" category for classification tasks.
|
|
106
|
-
- The `class_descriptions` dict argument MUST be a map from possible class names to a precise description.
|
|
107
|
-
- Use precise and specific class names and concise descriptions.
|
|
108
|
-
- Pass ALL relevant context, preferably as a dict mapping labels to content.
|
|
109
|
-
- Returned dict maps each possible class name to a probability.
|
|
110
|
-
|
|
111
|
-
Example Usage:
|
|
112
|
-
classification_task_and_requirements = "Does the document contain an address?"
|
|
113
|
-
class_descriptions = {
|
|
114
|
-
"Is_Address": "Valid addresses usually have street names, city, and zip codes.",
|
|
115
|
-
"Not_Address": "Not valid addresses."
|
|
116
|
-
}
|
|
117
|
-
classification = ai_classify(
|
|
118
|
-
classification_task_and_requirements,
|
|
119
|
-
{"address": extracted_address},
|
|
120
|
-
class_descriptions
|
|
121
|
-
)
|
|
122
|
-
if classification["probabilities"]["Is_Address"] > 0.5:
|
|
123
|
-
...
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
classification_task_and_requirements: The classification question and rules.
|
|
127
|
-
context: The data to classify (string, list, or dict).
|
|
128
|
-
class_descriptions: Mapping from class names to descriptions.
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
dict: {
|
|
132
|
-
probabilities: dict[str, float],
|
|
133
|
-
reason: str,
|
|
134
|
-
top_class: str,
|
|
135
|
-
}
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
context = get_context_str(context)
|
|
139
|
-
|
|
140
|
-
prompt = (
|
|
141
|
-
f"{classification_task_and_requirements}\n\n"
|
|
142
|
-
f"\nThis is classification task\nPossible classes and descriptions:\n"
|
|
143
|
-
f"{json.dumps(class_descriptions, indent=2)}\n"
|
|
144
|
-
f"\nContext:\n{context}\n\n"
|
|
145
|
-
"Return ONLY a valid JSON object, no extra text."
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
|
|
149
|
-
|
|
150
|
-
@dataclass
|
|
151
|
-
class ClassificationResult:
|
|
152
|
-
probabilities: dict[str, float]
|
|
153
|
-
reason: str
|
|
154
|
-
top_class: str
|
|
155
|
-
|
|
156
|
-
response = (
|
|
157
|
-
model.with_structured_output(schema=ClassificationResult, method="json_mode")
|
|
158
|
-
.with_retry(stop_after_attempt=MAX_RETRIES)
|
|
159
|
-
.invoke(prompt)
|
|
160
|
-
)
|
|
161
|
-
return cast(dict[str, Any], response)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def call_llm(
|
|
165
|
-
task_instructions: str, context: Any | list[Any] | dict[str, Any], output_json_schema: dict[str, Any]
|
|
166
|
-
) -> dict[str, Any]:
|
|
167
|
-
"""
|
|
168
|
-
Call a Large Language Model (LLM) with an instruction and contextual information,
|
|
169
|
-
returning a dictionary matching the given output_json_schema.
|
|
170
|
-
Can be used for tasks like creative writing, llm reasoning based content generation, etc.
|
|
171
|
-
|
|
172
|
-
You MUST anticipate Exceptions in reasoning based tasks which will lead to some empty fields
|
|
173
|
-
in the returned output; skip this item if applicable.
|
|
174
|
-
|
|
175
|
-
General Guidelines:
|
|
176
|
-
- Be comprehensive, specific, and precise on the task instructions.
|
|
177
|
-
- Include as much context as possible.
|
|
178
|
-
- You can provide multiple items in context, and reference them in the task.
|
|
179
|
-
- Include relevant high-level goals or intent in the task.
|
|
180
|
-
- In the output_json_schema, use required field wherever necessary.
|
|
181
|
-
- The more specific your task instructions and output_json_schema are, the better the results.
|
|
182
|
-
|
|
183
|
-
Guidelines for content generation tasks:
|
|
184
|
-
- Feel free to add instructions for tone, length, and format (markdown, html, plain-text, xml)
|
|
185
|
-
- Some examples of tone are: "normal", "flirty", "formal", "casual", "crisp", "poetic", "technical", "internet-chat", "smartass", etc.
|
|
186
|
-
- Prefer length to be concise by default. Other examples are: "very-short", "concise", "normal", "long", "2-3 lines", etc.
|
|
187
|
-
- In format prefer plain-text but you can also use markdown and html wherever useful.
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
instruction: The main directive for the LLM (e.g., "Summarize the article" or "Extract key entities").
|
|
191
|
-
context:
|
|
192
|
-
A dictionary containing named text elements that provide additional
|
|
193
|
-
information for the LLM. Keys are labels (e.g., 'article', 'transcript'),
|
|
194
|
-
values are strings of content.
|
|
195
|
-
output_json_schema: must be a valid JSON schema with top-level 'title' and 'description' keys.
|
|
196
|
-
|
|
197
|
-
Returns:
|
|
198
|
-
dict: Parsed JSON object matching the desired output_json_schema.
|
|
199
|
-
|
|
200
|
-
"""
|
|
201
|
-
context = get_context_str(context)
|
|
202
|
-
|
|
203
|
-
prompt = f"{task_instructions}\n\nContext:\n{context}\n\nReturn ONLY a valid JSON object, no extra text."
|
|
204
|
-
|
|
205
|
-
model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
|
|
206
|
-
|
|
207
|
-
response = (
|
|
208
|
-
model.with_structured_output(schema=output_json_schema, method="json_mode")
|
|
209
|
-
.with_retry(stop_after_attempt=MAX_RETRIES)
|
|
210
|
-
.invoke(prompt)
|
|
211
|
-
)
|
|
212
|
-
return cast(dict[str, Any], response)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def data_extractor(
|
|
216
|
-
extraction_task: str, source: Any | list[Any] | dict[str, Any], output_json_schema: dict[str, Any]
|
|
217
|
-
) -> dict[str, Any]:
|
|
218
|
-
"""
|
|
219
|
-
Extracts structured data from unstructured data (documents, webpages, images, large bodies of text),
|
|
220
|
-
returning a dictionary matching the given output_json_schema.
|
|
221
|
-
|
|
222
|
-
You MUST anticipate Exception raised for unextractable data; skip this item if applicable.
|
|
223
|
-
|
|
224
|
-
Strongly prefer to:
|
|
225
|
-
- Be comprehensive, specific, and precise on the data you want to extract.
|
|
226
|
-
- Use optional fields everywhere.
|
|
227
|
-
- Extract multiple items from each source unless otherwise specified.
|
|
228
|
-
- The more specific your extraction task and output_json_schema are, the better the results.
|
|
229
|
-
|
|
230
|
-
Args:
|
|
231
|
-
extraction_task: The directive describing what to extract.
|
|
232
|
-
source: The unstructured data to extract from.
|
|
233
|
-
output_json_schema: must be a valid JSON schema with top-level 'title' and 'description' keys.
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
dict: Parsed JSON object matching the desired output_json_schema.
|
|
237
|
-
|
|
238
|
-
Example:
|
|
239
|
-
news_articles_schema = {
|
|
240
|
-
"title": "NewsArticleList",
|
|
241
|
-
"description": "A list of news articles with headlines and URLs",
|
|
242
|
-
"type": "object",
|
|
243
|
-
"properties": {
|
|
244
|
-
"articles": {
|
|
245
|
-
"type": "array",
|
|
246
|
-
"items": {
|
|
247
|
-
"type": "object",
|
|
248
|
-
"properties": {
|
|
249
|
-
"headline": {
|
|
250
|
-
"type": "string"
|
|
251
|
-
},
|
|
252
|
-
"url": {
|
|
253
|
-
"type": "string"
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
"required": ["headline", "url"]
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
|
-
"required": ["articles"]
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
news_articles = data_extractor("Extract headlines and their corresponding URLs.", content, news_articles_schema)
|
|
264
|
-
"""
|
|
265
|
-
|
|
266
|
-
context = get_context_str(source)
|
|
267
|
-
|
|
268
|
-
prompt = f"{extraction_task}\n\nContext:\n{context}\n\nReturn ONLY a valid JSON object, no extra text."
|
|
269
|
-
|
|
270
|
-
model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
|
|
271
|
-
|
|
272
|
-
response = (
|
|
273
|
-
model.with_structured_output(schema=output_json_schema, method="json_mode")
|
|
274
|
-
.with_retry(stop_after_attempt=MAX_RETRIES)
|
|
275
|
-
.invoke(prompt)
|
|
276
|
-
)
|
|
277
|
-
return cast(dict[str, Any], response)
|
|
@@ -3,11 +3,10 @@ import re
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
|
|
5
5
|
from langchain_core.tools import StructuredTool
|
|
6
|
-
|
|
7
6
|
from universal_mcp.agents.codeact0.utils import schema_to_signature
|
|
8
7
|
|
|
9
8
|
uneditable_prompt = """
|
|
10
|
-
You are **
|
|
9
|
+
You are **Ruzo**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
|
|
11
10
|
|
|
12
11
|
Your job is to answer the user's question or perform the task they ask for.
|
|
13
12
|
- 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 in the chat. NEVER write a string yourself and print it.
|
|
@@ -21,7 +20,13 @@ Your job is to answer the user's question or perform the task they ask for.
|
|
|
21
20
|
- 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. Similarly, you should only use print/smart_print for your own analysis, the user does not get the output.
|
|
22
21
|
- If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
**Code Execution Guidelines:**
|
|
24
|
+
- The code you write will be executed in a sandbox environment, and you can use the output of previous executions in your code. Variables, functions, imports are retained.
|
|
25
|
+
- 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. Similarly, you should only use print/smart_print for your own analysis, the user does not get the output.
|
|
26
|
+
- If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
|
|
27
|
+
- Always describe in 2-3 lines about the current progress. In each step, mention what has been achieved and what you are planning to do next.
|
|
28
|
+
|
|
29
|
+
**Coding Best Practices:**
|
|
25
30
|
- Variables defined at the top level of previous code snippets can be referenced in your code.
|
|
26
31
|
- 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. `smart_print` truncates long strings from data, preventing huge output logs.
|
|
27
32
|
- 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.
|
|
@@ -29,25 +34,40 @@ GUIDELINES for writing code:
|
|
|
29
34
|
- You can only import libraries that come pre-installed with Python. However, do consider searching for external functions first, using the search and load tools to access them in the code.
|
|
30
35
|
- For displaying final results to the user, you must present your output in markdown format, including image links, so that they are rendered and displayed to the user. The code output is NOT visible to the user.
|
|
31
36
|
- Call all functions using keyword arguments only, never positional arguments.
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
|
|
38
|
+
**Async Functions (Critical Rules):**
|
|
39
|
+
Use async functions only as follows:
|
|
40
|
+
- Case 1: Top-level await without asyncio.run()
|
|
34
41
|
Wrap in async function and call with asyncio.run():
|
|
42
|
+
```python
|
|
35
43
|
async def main():
|
|
36
44
|
result = await some_async_function()
|
|
37
45
|
return result
|
|
38
46
|
asyncio.run(main())
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
```
|
|
48
|
+
- Case 2: Using asyncio.run() directly
|
|
49
|
+
If code already contains asyncio.run(), use as-is — do not wrap again:
|
|
50
|
+
```python
|
|
41
51
|
asyncio.run(some_async_function())
|
|
52
|
+
```
|
|
42
53
|
Rules:
|
|
43
54
|
- Never use await outside an async function
|
|
44
55
|
- Never use await asyncio.run()
|
|
45
56
|
- Never nest asyncio.run() calls
|
|
57
|
+
|
|
58
|
+
**Final Output Requirements:**
|
|
59
|
+
- Once you have all the information about the task, return the text directly to user in markdown format. No need to call `execute_ipython_cell` again.
|
|
60
|
+
- Always respond in github flavoured markdown format.
|
|
61
|
+
- For charts and diagrams, use mermaid chart in markdown directly.
|
|
62
|
+
- Your final response should contain the complete answer to the user's request in a clear, well-formatted manner that directly addresses what they asked for.
|
|
46
63
|
"""
|
|
47
64
|
|
|
48
65
|
PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
|
|
49
66
|
|
|
50
|
-
TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
|
|
67
|
+
TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
|
|
68
|
+
Do not include the searching and loading of tools. Assume that the tools have already been loaded.
|
|
69
|
+
The plan is a sequence of steps.
|
|
70
|
+
You must output a JSON object with a single key "steps", which is a list of strings. Each string is a step in the playbook.
|
|
51
71
|
|
|
52
72
|
Your plan should:
|
|
53
73
|
1. Identify the key steps in the workflow
|
|
@@ -56,24 +76,45 @@ Your plan should:
|
|
|
56
76
|
4. Be clear and concise
|
|
57
77
|
|
|
58
78
|
Example:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
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.
|
|
79
|
+
{
|
|
80
|
+
"steps": [
|
|
81
|
+
"Connect to database using `db_connection_string`",
|
|
82
|
+
"Query user data for `user_id`",
|
|
83
|
+
"Process results and calculate `metric_name`",
|
|
84
|
+
"Send notification to `email_address`"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Now create a plan based on the conversation history. Do not include any other text or explanation in your response. Just the JSON object.
|
|
70
89
|
"""
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
|
|
92
|
+
PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function.
|
|
93
|
+
Your response must be ONLY the Python code for the function.
|
|
94
|
+
Do not include any other text, markdown, or explanations in your response.
|
|
95
|
+
Your response should start with `def` or `async def`.
|
|
74
96
|
The function should be a single, complete piece of code that can be executed independently, based on previously executed code snippets that executed correctly.
|
|
75
97
|
The parameters of the function should be the same as the final confirmed playbook plan.
|
|
76
|
-
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
PLAYBOOK_META_PROMPT = """
|
|
102
|
+
You are preparing metadata for a reusable playbook based on the confirmed step-by-step plan.
|
|
103
|
+
|
|
104
|
+
TASK: Create a concise, human-friendly name and a short description for the playbook.
|
|
105
|
+
|
|
106
|
+
INPUTS:
|
|
107
|
+
- Conversation context and plan steps will be provided in prior messages
|
|
108
|
+
|
|
109
|
+
REQUIREMENTS:
|
|
110
|
+
1. Name: 3-6 words, Title Case, no punctuation except hyphens if needed
|
|
111
|
+
2. Description: Single sentence, <= 140 characters, clearly states what the playbook does
|
|
112
|
+
|
|
113
|
+
OUTPUT: Return ONLY a JSON object with exactly these keys:
|
|
114
|
+
{
|
|
115
|
+
"name": "...",
|
|
116
|
+
"description": "..."
|
|
117
|
+
}
|
|
77
118
|
"""
|
|
78
119
|
|
|
79
120
|
|
|
@@ -90,92 +131,20 @@ def make_safe_function_name(name: str) -> str:
|
|
|
90
131
|
return safe_name
|
|
91
132
|
|
|
92
133
|
|
|
93
|
-
def dedent(text):
|
|
94
|
-
"""Remove any common leading whitespace from every line in `text`.
|
|
95
|
-
|
|
96
|
-
This can be used to make triple-quoted strings line up with the left
|
|
97
|
-
edge of the display, while still presenting them in the source code
|
|
98
|
-
in indented form.
|
|
99
|
-
|
|
100
|
-
Note that tabs and spaces are both treated as whitespace, but they
|
|
101
|
-
are not equal: the lines " hello" and "\\thello" are
|
|
102
|
-
considered to have no common leading whitespace.
|
|
103
|
-
|
|
104
|
-
Entirely blank lines are normalized to a newline character.
|
|
105
|
-
"""
|
|
106
|
-
# Look for the longest leading string of spaces and tabs common to
|
|
107
|
-
# all lines.
|
|
108
|
-
margin = None
|
|
109
|
-
_whitespace_only_re = re.compile("^[ \t]+$", re.MULTILINE)
|
|
110
|
-
_leading_whitespace_re = re.compile("(^[ \t]*)(?:[^ \t\n])", re.MULTILINE)
|
|
111
|
-
text = _whitespace_only_re.sub("", text)
|
|
112
|
-
indents = _leading_whitespace_re.findall(text)
|
|
113
|
-
for indent in indents:
|
|
114
|
-
if margin is None:
|
|
115
|
-
margin = indent
|
|
116
|
-
|
|
117
|
-
# Current line more deeply indented than previous winner:
|
|
118
|
-
# no change (previous winner is still on top).
|
|
119
|
-
elif indent.startswith(margin):
|
|
120
|
-
pass
|
|
121
|
-
|
|
122
|
-
# Current line consistent with and no deeper than previous winner:
|
|
123
|
-
# it's the new winner.
|
|
124
|
-
elif margin.startswith(indent):
|
|
125
|
-
margin = indent
|
|
126
|
-
|
|
127
|
-
# Find the largest common whitespace between current line and previous
|
|
128
|
-
# winner.
|
|
129
|
-
else:
|
|
130
|
-
for i, (x, y) in enumerate(zip(margin, indent)):
|
|
131
|
-
if x != y:
|
|
132
|
-
margin = margin[:i]
|
|
133
|
-
break
|
|
134
|
-
|
|
135
|
-
# sanity check (testing/debugging only)
|
|
136
|
-
if 0 and margin:
|
|
137
|
-
for line in text.split("\n"):
|
|
138
|
-
assert not line or line.startswith(margin), f"line = {line!r}, margin = {margin!r}"
|
|
139
|
-
|
|
140
|
-
if margin:
|
|
141
|
-
text = re.sub(r"(?m)^" + margin, "", text)
|
|
142
|
-
return text
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def indent(text, prefix, predicate=None):
|
|
146
|
-
"""Adds 'prefix' to the beginning of selected lines in 'text'.
|
|
147
|
-
|
|
148
|
-
If 'predicate' is provided, 'prefix' will only be added to the lines
|
|
149
|
-
where 'predicate(line)' is True. If 'predicate' is not provided,
|
|
150
|
-
it will default to adding 'prefix' to all non-empty lines that do not
|
|
151
|
-
consist solely of whitespace characters.
|
|
152
|
-
"""
|
|
153
|
-
if predicate is None:
|
|
154
|
-
# str.splitlines(True) doesn't produce empty string.
|
|
155
|
-
# ''.splitlines(True) => []
|
|
156
|
-
# 'foo\n'.splitlines(True) => ['foo\n']
|
|
157
|
-
# So we can use just `not s.isspace()` here.
|
|
158
|
-
def predicate(s):
|
|
159
|
-
return not s.isspace()
|
|
160
|
-
|
|
161
|
-
prefixed_lines = []
|
|
162
|
-
for line in text.splitlines(True):
|
|
163
|
-
if predicate(line):
|
|
164
|
-
prefixed_lines.append(prefix)
|
|
165
|
-
prefixed_lines.append(line)
|
|
166
|
-
|
|
167
|
-
return "".join(prefixed_lines)
|
|
168
|
-
|
|
169
|
-
|
|
170
134
|
def create_default_prompt(
|
|
171
135
|
tools: Sequence[StructuredTool],
|
|
172
136
|
additional_tools: Sequence[StructuredTool],
|
|
173
137
|
base_prompt: str | None = None,
|
|
138
|
+
apps_string: str | None = None,
|
|
139
|
+
playbook: object | None = None,
|
|
174
140
|
):
|
|
175
|
-
system_prompt = uneditable_prompt.strip()
|
|
176
|
-
|
|
177
|
-
|
|
141
|
+
system_prompt = uneditable_prompt.strip()
|
|
142
|
+
if apps_string:
|
|
143
|
+
system_prompt += f"\n\n**Connected external applications (These apps have been logged into by the user):**\n{apps_string}\n\n Use `search_functions` to search for functions you can perform using the above. You can also discover more applications using the `search_functions` tool to find additional tools and integrations, if required.\n"
|
|
144
|
+
system_prompt += "\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
|
|
145
|
+
|
|
178
146
|
tools_context = {}
|
|
147
|
+
|
|
179
148
|
for tool in tools:
|
|
180
149
|
if hasattr(tool, "func") and tool.func is not None:
|
|
181
150
|
tool_callable = tool.func
|
|
@@ -186,7 +155,7 @@ def create_default_prompt(
|
|
|
186
155
|
system_prompt += f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
|
|
187
156
|
"""{tool.description}"""
|
|
188
157
|
...
|
|
189
|
-
|
|
158
|
+
'''
|
|
190
159
|
safe_name = make_safe_function_name(tool.name)
|
|
191
160
|
tools_context[safe_name] = tool_callable
|
|
192
161
|
|
|
@@ -200,11 +169,31 @@ def create_default_prompt(
|
|
|
200
169
|
system_prompt += f'''{"async " if is_async else ""}def {tool.name} {str(inspect.signature(tool_callable))}:
|
|
201
170
|
"""{tool.description}"""
|
|
202
171
|
...
|
|
203
|
-
|
|
172
|
+
'''
|
|
204
173
|
safe_name = make_safe_function_name(tool.name)
|
|
205
174
|
tools_context[safe_name] = tool_callable
|
|
206
175
|
|
|
207
176
|
if base_prompt and base_prompt.strip():
|
|
208
177
|
system_prompt += f"Your goal is to perform the following task:\n\n{base_prompt}"
|
|
209
178
|
|
|
179
|
+
# Append existing playbook (plan + code) if provided
|
|
180
|
+
try:
|
|
181
|
+
if playbook and hasattr(playbook, "instructions"):
|
|
182
|
+
pb = playbook.instructions or {}
|
|
183
|
+
plan = pb.get("playbookPlan")
|
|
184
|
+
code = pb.get("playbookScript")
|
|
185
|
+
if plan or code:
|
|
186
|
+
system_prompt += "\n\nExisting Playbook Provided:\n"
|
|
187
|
+
if plan:
|
|
188
|
+
if isinstance(plan, list):
|
|
189
|
+
plan_block = "\n".join(f"- {str(s)}" for s in plan)
|
|
190
|
+
else:
|
|
191
|
+
plan_block = str(plan)
|
|
192
|
+
system_prompt += f"Plan Steps:\n{plan_block}\n"
|
|
193
|
+
if code:
|
|
194
|
+
system_prompt += f"\nScript:\n```python\n{str(code)}\n```\n"
|
|
195
|
+
except Exception:
|
|
196
|
+
# Silently ignore formatting issues
|
|
197
|
+
pass
|
|
198
|
+
|
|
210
199
|
return system_prompt, tools_context
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
from typing import Annotated, Any
|
|
1
|
+
from typing import Annotated, Any, List
|
|
2
2
|
|
|
3
3
|
from langgraph.prebuilt.chat_agent_executor import AgentState
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PlaybookPlan(BaseModel):
|
|
8
|
+
steps: List[str] = Field(description="The steps of the playbook.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlaybookCode(BaseModel):
|
|
12
|
+
code: str = Field(description="The Python code for the playbook.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PlaybookMeta(BaseModel):
|
|
16
|
+
name: str = Field(description="Concise, title-cased playbook name (3-6 words).")
|
|
17
|
+
description: str = Field(description="Short, one-sentence description (<= 140 chars).")
|
|
4
18
|
|
|
5
19
|
|
|
6
20
|
def _enqueue(left: list, right: list) -> list:
|
|
@@ -36,5 +50,9 @@ class CodeActState(AgentState):
|
|
|
36
50
|
"""State for the playbook agent."""
|
|
37
51
|
selected_tool_ids: Annotated[list[str], _enqueue]
|
|
38
52
|
"""Queue for tools exported from registry"""
|
|
39
|
-
plan: str | None
|
|
53
|
+
plan: list[str] | None
|
|
40
54
|
"""Plan for the playbook agent."""
|
|
55
|
+
playbook_name: str | None
|
|
56
|
+
"""Generated playbook name after confirmation."""
|
|
57
|
+
playbook_description: str | None
|
|
58
|
+
"""Generated short description after confirmation."""
|
|
@@ -4,6 +4,7 @@ from collections import defaultdict
|
|
|
4
4
|
from typing import Annotated, Any
|
|
5
5
|
|
|
6
6
|
from langchain_core.tools import tool
|
|
7
|
+
from loguru import logger
|
|
7
8
|
from pydantic import Field
|
|
8
9
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
9
10
|
from universal_mcp.types import ToolFormat
|
|
@@ -69,9 +70,10 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
|
|
|
69
70
|
|
|
70
71
|
canonical_app_id = None
|
|
71
72
|
found_tools_result = []
|
|
73
|
+
THRESHOLD = 0.8
|
|
72
74
|
|
|
73
75
|
if app_id:
|
|
74
|
-
relevant_apps = await registry.search_apps(query=app_id, distance_threshold=
|
|
76
|
+
relevant_apps = await registry.search_apps(query=app_id, distance_threshold=THRESHOLD)
|
|
75
77
|
if not relevant_apps:
|
|
76
78
|
return {
|
|
77
79
|
"found_tools": [],
|
|
@@ -103,11 +105,11 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
|
|
|
103
105
|
prioritized_app_id_list = [canonical_app_id]
|
|
104
106
|
else:
|
|
105
107
|
# 1. Perform an initial broad search for tools.
|
|
106
|
-
initial_tool_search_tasks = [registry.search_tools(query=q, distance_threshold=
|
|
108
|
+
initial_tool_search_tasks = [registry.search_tools(query=q, distance_threshold=THRESHOLD) for q in queries]
|
|
107
109
|
initial_tool_results = await asyncio.gather(*initial_tool_search_tasks)
|
|
108
110
|
|
|
109
111
|
# 2. Search for relevant apps.
|
|
110
|
-
app_search_tasks = [registry.search_apps(query=q, distance_threshold=
|
|
112
|
+
app_search_tasks = [registry.search_apps(query=q, distance_threshold=THRESHOLD) for q in queries]
|
|
111
113
|
app_search_results = await asyncio.gather(*app_search_tasks)
|
|
112
114
|
|
|
113
115
|
# 3. Create a prioritized list of app IDs for the final search.
|
|
@@ -130,7 +132,7 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
|
|
|
130
132
|
for app_id_to_search in prioritized_app_id_list:
|
|
131
133
|
for query in queries:
|
|
132
134
|
final_tool_search_tasks.append(
|
|
133
|
-
registry.search_tools(query=query, app_id=app_id_to_search, distance_threshold=
|
|
135
|
+
registry.search_tools(query=query, app_id=app_id_to_search, distance_threshold=THRESHOLD)
|
|
134
136
|
)
|
|
135
137
|
query_results = await asyncio.gather(*final_tool_search_tasks)
|
|
136
138
|
|
|
@@ -208,39 +210,27 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
|
|
|
208
210
|
|
|
209
211
|
@tool
|
|
210
212
|
async def web_search(query: str) -> dict:
|
|
211
|
-
"""
|
|
212
|
-
Useful when you need information from a wide range of real-time sources on the web.
|
|
213
|
-
Do not use this when you need to access contents of a specific webpage.
|
|
213
|
+
"""
|
|
214
|
+
Get an LLM answer to a question informed by Exa search results. Useful when you need information from a wide range of real-time sources on the web. Do not use this when you need to access contents of a specific webpage.
|
|
214
215
|
|
|
215
|
-
This tool performs
|
|
216
|
-
1. Provides a **direct answer** for factual queries
|
|
216
|
+
This tool performs an Exa `/answer` request, which:
|
|
217
|
+
1. Provides a **direct answer** for factual queries (e.g., "What is the capital of France?" → "Paris")
|
|
217
218
|
2. Generates a **summary with citations** for open-ended questions
|
|
219
|
+
(e.g., "What is the state of AI in healthcare?" → A detailed summary with source links)
|
|
218
220
|
|
|
219
221
|
Args:
|
|
220
222
|
query (str): The question or topic to answer.
|
|
221
|
-
|
|
222
223
|
Returns:
|
|
223
|
-
dict: A structured response containing:
|
|
224
|
-
- answer (str): Generated answer
|
|
225
|
-
- citations (list[
|
|
224
|
+
dict: A structured response containing only:
|
|
225
|
+
- answer (str): Generated answer
|
|
226
|
+
- citations (list[dict]): List of cited sources
|
|
226
227
|
"""
|
|
227
|
-
await tool_registry.export_tools(["
|
|
228
|
-
|
|
229
|
-
response = await tool_registry.call_tool(
|
|
230
|
-
"perplexity__answer_with_search",
|
|
231
|
-
{
|
|
232
|
-
"query": query,
|
|
233
|
-
"model": "sonar",
|
|
234
|
-
"temperature": 1.0,
|
|
235
|
-
"system_prompt": (
|
|
236
|
-
"You are a helpful AI assistant that answers questions using real-time information from the web."
|
|
237
|
-
),
|
|
238
|
-
},
|
|
239
|
-
)
|
|
228
|
+
await tool_registry.export_tools(["exa__answer"], ToolFormat.LANGCHAIN)
|
|
229
|
+
response = await tool_registry.call_tool("exa__answer", {"query": query, "text": True})
|
|
240
230
|
|
|
241
231
|
# Extract only desired fields
|
|
242
232
|
return {
|
|
243
|
-
"answer": response.get("
|
|
233
|
+
"answer": response.get("answer"),
|
|
244
234
|
"citations": response.get("citations", []),
|
|
245
235
|
}
|
|
246
236
|
|
|
@@ -288,7 +278,7 @@ async def get_valid_tools(tool_ids: list[str], registry: AgentrRegistry) -> tupl
|
|
|
288
278
|
continue
|
|
289
279
|
if app not in connected_apps and app not in unconnected:
|
|
290
280
|
unconnected.add(app)
|
|
291
|
-
text = registry.authorise_app(app_id=app)
|
|
281
|
+
text = await registry.authorise_app(app_id=app)
|
|
292
282
|
start = text.find(":") + 1
|
|
293
283
|
end = text.find(". R", start)
|
|
294
284
|
url = text[start:end].strip()
|
|
@@ -5,10 +5,18 @@ from collections.abc import Sequence
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from langchain_core.messages import BaseMessage
|
|
8
|
+
from universal_mcp.types import ToolConfig
|
|
8
9
|
|
|
9
10
|
MAX_CHARS = 5000
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def add_tools(tool_config: ToolConfig, tools_to_add: ToolConfig):
|
|
14
|
+
for app_id, new_tools in tools_to_add.items():
|
|
15
|
+
all_tools = tool_config.get(app_id, []) + new_tools
|
|
16
|
+
tool_config[app_id] = list(set(all_tools))
|
|
17
|
+
return tool_config
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
def light_copy(data):
|
|
13
21
|
"""
|
|
14
22
|
Deep copy a dict[str, any] or Sequence[any] with string truncation.
|
|
@@ -325,35 +333,48 @@ def inject_context(
|
|
|
325
333
|
return namespace
|
|
326
334
|
|
|
327
335
|
|
|
328
|
-
def schema_to_signature(schema: dict, func_name="my_function") -> str:
|
|
336
|
+
def schema_to_signature(schema: dict, func_name: str = "my_function") -> str:
|
|
337
|
+
"""
|
|
338
|
+
Convert a JSON schema into a Python-style function signature string.
|
|
339
|
+
Handles fields with `type`, `anyOf`, defaults, and missing metadata safely.
|
|
340
|
+
"""
|
|
329
341
|
type_map = {
|
|
330
342
|
"integer": "int",
|
|
331
343
|
"string": "str",
|
|
332
344
|
"boolean": "bool",
|
|
333
345
|
"null": "None",
|
|
346
|
+
"number": "float",
|
|
347
|
+
"array": "list",
|
|
348
|
+
"object": "dict",
|
|
334
349
|
}
|
|
335
350
|
|
|
336
351
|
params = []
|
|
337
352
|
for name, meta in schema.items():
|
|
338
|
-
|
|
339
|
-
|
|
353
|
+
if not isinstance(meta, dict):
|
|
354
|
+
typ = "Any"
|
|
355
|
+
elif "type" in meta:
|
|
340
356
|
typ = type_map.get(meta["type"], "Any")
|
|
341
357
|
elif "anyOf" in meta:
|
|
342
|
-
types = [
|
|
343
|
-
|
|
358
|
+
types = []
|
|
359
|
+
for t in meta["anyOf"]:
|
|
360
|
+
if not isinstance(t, dict):
|
|
361
|
+
continue
|
|
362
|
+
t_type = t.get("type")
|
|
363
|
+
types.append(type_map.get(t_type, "Any") if t_type else "Any")
|
|
364
|
+
typ = " | ".join(sorted(set(types))) if types else "Any"
|
|
344
365
|
else:
|
|
345
366
|
typ = "Any"
|
|
346
367
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
368
|
+
# Handle defaults gracefully
|
|
369
|
+
default = meta.get("default")
|
|
370
|
+
if default is None:
|
|
371
|
+
params.append(f"{name}: {typ}")
|
|
372
|
+
else:
|
|
373
|
+
params.append(f"{name}: {typ} = {repr(default)}")
|
|
351
374
|
|
|
352
|
-
# join into signature
|
|
353
375
|
param_str = ",\n ".join(params)
|
|
354
376
|
return f"def {func_name}(\n {param_str},\n):"
|
|
355
377
|
|
|
356
|
-
|
|
357
378
|
def smart_truncate(
|
|
358
379
|
output: str, max_chars_full: int = 2000, max_lines_headtail: int = 20, summary_threshold: int = 10000
|
|
359
380
|
) -> str:
|
|
@@ -386,3 +407,27 @@ def smart_truncate(
|
|
|
386
407
|
truncated = truncated[:summary_threshold] + "\n... [output truncated to fit context] ..."
|
|
387
408
|
|
|
388
409
|
return truncated
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
async def get_connected_apps_string(registry) -> str:
|
|
413
|
+
"""Get a formatted string of connected applications from the registry."""
|
|
414
|
+
if not registry:
|
|
415
|
+
return ""
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
# Get connected apps from registry
|
|
419
|
+
connections = await registry.list_connected_apps()
|
|
420
|
+
if not connections:
|
|
421
|
+
return "No applications are currently connected."
|
|
422
|
+
|
|
423
|
+
# Extract app names from connections
|
|
424
|
+
connected_app_ids = {connection["app_id"] for connection in connections}
|
|
425
|
+
|
|
426
|
+
# Format the apps list
|
|
427
|
+
apps_list = []
|
|
428
|
+
for app_id in connected_app_ids:
|
|
429
|
+
apps_list.append(f"- {app_id}")
|
|
430
|
+
|
|
431
|
+
return "\n".join(apps_list)
|
|
432
|
+
except Exception:
|
|
433
|
+
return "Unable to retrieve connected applications."
|
|
@@ -162,11 +162,11 @@ class LlmApp(BaseApplication):
|
|
|
162
162
|
top_class: str = Field(..., description="The class with the highest probability.")
|
|
163
163
|
|
|
164
164
|
response = (
|
|
165
|
-
model.with_structured_output(schema=ClassificationResult
|
|
165
|
+
model.with_structured_output(schema=ClassificationResult)
|
|
166
166
|
.with_retry(stop_after_attempt=MAX_RETRIES)
|
|
167
167
|
.invoke(prompt)
|
|
168
168
|
)
|
|
169
|
-
return
|
|
169
|
+
return response.model_dump_json(indent=2)
|
|
170
170
|
|
|
171
171
|
def extract_data(
|
|
172
172
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: universal-mcp-agents
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.22
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
universal_mcp/agents/__init__.py,sha256=Rh6vKqpwuZ2joC9nzLFHUI1G7jzbwwC3p0mgpH5qRgo,1112
|
|
2
|
-
universal_mcp/agents/base.py,sha256=
|
|
2
|
+
universal_mcp/agents/base.py,sha256=CEnY8y2as_XR311t9v2iqd4DOCSyhpOPOBDcZKNJMpc,7378
|
|
3
3
|
universal_mcp/agents/cli.py,sha256=bXdpgxsOMjclm1STHJgx10ocX9EebQ11DrxH0p6KMZk,943
|
|
4
4
|
universal_mcp/agents/hil.py,sha256=_5PCK6q0goGm8qylJq44aSp2MadP-yCPvhOJYKqWLMo,3808
|
|
5
5
|
universal_mcp/agents/llm.py,sha256=hVRwjZs3MHl5_3BWedmurs2Jt1oZDfFX0Zj9F8KH7fk,1787
|
|
@@ -22,23 +22,23 @@ universal_mcp/agents/builder/prompts.py,sha256=8Xs6uzTUHguDRngVMLak3lkXFkk2VV_uQ
|
|
|
22
22
|
universal_mcp/agents/builder/state.py,sha256=7DeWllxfN-yD6cd9wJ3KIgjO8TctkJvVjAbZT8W_zqk,922
|
|
23
23
|
universal_mcp/agents/codeact0/__init__.py,sha256=8-fvUo1Sm6dURGI-lW-X3Kd78LqySYbb5NMkNJ4NDwg,76
|
|
24
24
|
universal_mcp/agents/codeact0/__main__.py,sha256=_7qSz97YnRgYJTESkALS5_eBIGHiMjA5rhr3IAeBvVo,896
|
|
25
|
-
universal_mcp/agents/codeact0/agent.py,sha256=
|
|
25
|
+
universal_mcp/agents/codeact0/agent.py,sha256=Cxdh9eJvrIfUISdFQKXnYrGc6PePrIGusSvWiALmAJo,16634
|
|
26
26
|
universal_mcp/agents/codeact0/config.py,sha256=H-1woj_nhSDwf15F63WYn723y4qlRefXzGxuH81uYF0,2215
|
|
27
27
|
universal_mcp/agents/codeact0/langgraph_agent.py,sha256=8nz2wq-LexImx-l1y9_f81fK72IQetnCeljwgnduNGY,420
|
|
28
|
-
universal_mcp/agents/codeact0/llm_tool.py,sha256
|
|
29
|
-
universal_mcp/agents/codeact0/prompts.py,sha256=
|
|
28
|
+
universal_mcp/agents/codeact0/llm_tool.py,sha256=-pAz04OrbZ_dJ2ueysT1qZd02DrbLY4EbU0tiuF_UNU,798
|
|
29
|
+
universal_mcp/agents/codeact0/prompts.py,sha256=Gk6WTx2X7IOlbC3wYtGwKWDoeUtgpkMeiyfhj6LEq2I,11221
|
|
30
30
|
universal_mcp/agents/codeact0/sandbox.py,sha256=Xw4tbUV_6haYIZZvteJi6lIYsW6ni_3DCRCOkslTKgM,4459
|
|
31
|
-
universal_mcp/agents/codeact0/state.py,sha256=
|
|
32
|
-
universal_mcp/agents/codeact0/tools.py,sha256=
|
|
33
|
-
universal_mcp/agents/codeact0/utils.py,sha256=
|
|
31
|
+
universal_mcp/agents/codeact0/state.py,sha256=co3BZBuMIt1FP2qzgsbsLkyKbddCG1ieKyAw9TAskSU,1944
|
|
32
|
+
universal_mcp/agents/codeact0/tools.py,sha256=WRJKNIf6_cP_enElboEyVd-aHF-kk6aq8Bg0biZijR4,13363
|
|
33
|
+
universal_mcp/agents/codeact0/utils.py,sha256=PgisAxmqYIzimc4lFA1nV-R7gxkICIrtO1q0FQZ-UoY,17580
|
|
34
34
|
universal_mcp/agents/shared/__main__.py,sha256=XxH5qGDpgFWfq7fwQfgKULXGiUgeTp_YKfcxftuVZq8,1452
|
|
35
35
|
universal_mcp/agents/shared/prompts.py,sha256=yjP3zbbuKi87qCj21qwTTicz8TqtkKgnyGSeEjMu3ho,3761
|
|
36
36
|
universal_mcp/agents/shared/tool_node.py,sha256=DC9F-Ri28Pam0u3sXWNODVgmj9PtAEUb5qP1qOoGgfs,9169
|
|
37
37
|
universal_mcp/applications/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
38
|
universal_mcp/applications/filesystem/app.py,sha256=0TRjjm8YnslVRSmfkXI7qQOAlqWlD1eEn8Jm0xBeigs,5561
|
|
39
39
|
universal_mcp/applications/llm/__init__.py,sha256=_XGRxN3O1--ZS5joAsPf8IlI9Qa6negsJrwJ5VJXno0,46
|
|
40
|
-
universal_mcp/applications/llm/app.py,sha256=
|
|
40
|
+
universal_mcp/applications/llm/app.py,sha256=g9mK-luOLUshZzBGyQZMOHBeCSXmh2kCKir40YnsGUo,12727
|
|
41
41
|
universal_mcp/applications/ui/app.py,sha256=c7OkZsO2fRtndgAzAQbKu-1xXRuRp9Kjgml57YD2NR4,9459
|
|
42
|
-
universal_mcp_agents-0.1.
|
|
43
|
-
universal_mcp_agents-0.1.
|
|
44
|
-
universal_mcp_agents-0.1.
|
|
42
|
+
universal_mcp_agents-0.1.22.dist-info/METADATA,sha256=6f34bvvybyI6et8rvKIu-4BtFJHGmt0YZmaZutJBXuw,878
|
|
43
|
+
universal_mcp_agents-0.1.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
44
|
+
universal_mcp_agents-0.1.22.dist-info/RECORD,,
|
|
File without changes
|