universal-mcp-agents 0.1.20rc1__tar.gz → 0.1.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of universal-mcp-agents might be problematic. Click here for more details.
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/PKG-INFO +1 -1
- universal_mcp_agents-0.1.21/bech.py +38 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/bump_and_release.sh +1 -1
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/pyproject.toml +1 -1
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/base.py +29 -28
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/agent.py +74 -53
- universal_mcp_agents-0.1.21/src/universal_mcp/agents/codeact0/llm_tool.py +25 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/prompts.py +65 -114
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/state.py +11 -2
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/tools.py +4 -13
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/utils.py +8 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/applications/llm/app.py +2 -2
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/uv.lock +1 -1
- universal_mcp_agents-0.1.20rc1/src/universal_mcp/agents/codeact0/llm_tool.py +0 -277
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.github/workflows/evals.yml +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.github/workflows/lint.yml +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.github/workflows/release-please.yml +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.github/workflows/tests.yml +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.gitignore +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.pre-commit-config.yaml +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/GEMINI.md +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/PROMPTS.md +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/README.md +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/__init__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/dataset.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/datasets/codeact.jsonl +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/datasets/exact.jsonl +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/datasets/tasks.jsonl +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/evaluators.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/prompts.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/run.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/utils.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/tests/test_agents.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/__init__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/__init__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/__main__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/agent.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/context.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/graph.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/prompts.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/state.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/bigtool/tools.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/builder/__main__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/builder/builder.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/builder/helper.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/builder/prompts.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/builder/state.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/cli.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/__init__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/__main__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/config.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/langgraph_agent.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/codeact0/sandbox.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/hil.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/llm.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/react.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/sandbox.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/shared/__main__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/shared/prompts.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/shared/tool_node.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/simple.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/utils.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/applications/filesystem/__init__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/applications/filesystem/app.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/applications/llm/__init__.py +0 -0
- {universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/applications/ui/app.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: universal-mcp-agents
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.21
|
|
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
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from universal_mcp.agentr.registry import AgentrRegistry
|
|
2
|
+
from universal_mcp.agents import get_agent
|
|
3
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
4
|
+
from universal_mcp.agents.utils import messages_to_list
|
|
5
|
+
import time
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
start_time = time.time()
|
|
11
|
+
memory = MemorySaver()
|
|
12
|
+
logger.info(f"Checkpointer: Time consumed: {time.time() - start_time}")
|
|
13
|
+
agent_cls = get_agent("codeact-repl")
|
|
14
|
+
logger.info(f"Get class: Time consumed: {time.time() - start_time}")
|
|
15
|
+
registry = AgentrRegistry()
|
|
16
|
+
logger.info(f"Init Registry: Time consumed: {time.time() - start_time}")
|
|
17
|
+
agent = agent_cls(
|
|
18
|
+
name="CodeAct Agent",
|
|
19
|
+
instructions="Be very concise in your answers.",
|
|
20
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
21
|
+
tools={},
|
|
22
|
+
registry=registry,
|
|
23
|
+
memory=memory,
|
|
24
|
+
)
|
|
25
|
+
logger.info(f"Create agent: Time consumed: {time.time() - start_time}")
|
|
26
|
+
print("Init agent...")
|
|
27
|
+
await agent.ainit()
|
|
28
|
+
logger.info(f"Init Agent: Time consumed: {time.time() - start_time}")
|
|
29
|
+
print("Starting agent...")
|
|
30
|
+
async for e in agent.stream(user_input="hi"):
|
|
31
|
+
logger.info(f"First token: Time consumed: {time.time() - start_time}")
|
|
32
|
+
print(e)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/base.py
RENAMED
|
@@ -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,31 +1,31 @@
|
|
|
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,
|
|
20
19
|
create_default_prompt,
|
|
21
20
|
)
|
|
22
21
|
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
|
|
22
|
+
from universal_mcp.agents.codeact0.state import CodeActState, PlaybookCode, PlaybookPlan
|
|
24
23
|
from universal_mcp.agents.codeact0.tools import (
|
|
25
24
|
create_meta_tools,
|
|
26
25
|
enter_playbook_mode,
|
|
27
26
|
get_valid_tools,
|
|
28
27
|
)
|
|
28
|
+
from universal_mcp.agents.codeact0.utils import add_tools
|
|
29
29
|
from universal_mcp.agents.llm import load_chat_model
|
|
30
30
|
from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
|
|
31
31
|
|
|
@@ -51,16 +51,22 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
51
51
|
**kwargs,
|
|
52
52
|
)
|
|
53
53
|
self.model_instance = load_chat_model(model)
|
|
54
|
-
self.
|
|
54
|
+
self.playbook_model_instance = load_chat_model("azure/gpt-4.1")
|
|
55
|
+
self.tools_config = tools or {}
|
|
55
56
|
self.registry = registry
|
|
56
57
|
self.playbook_registry = playbook_registry
|
|
58
|
+
self.playbook = playbook_registry.get_agent() if playbook_registry else None
|
|
57
59
|
self.eval_fn = eval_unsafe
|
|
58
60
|
self.sandbox_timeout = sandbox_timeout
|
|
59
|
-
self.
|
|
61
|
+
self.default_tools = {
|
|
62
|
+
"llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
|
|
63
|
+
"markitdown": ["convert_to_markdown"],
|
|
64
|
+
}
|
|
65
|
+
add_tools(self.tools_config, self.default_tools)
|
|
60
66
|
|
|
61
67
|
async def _build_graph(self):
|
|
62
68
|
meta_tools = create_meta_tools(self.registry)
|
|
63
|
-
additional_tools = [smart_print,
|
|
69
|
+
additional_tools = [smart_print, meta_tools["web_search"]]
|
|
64
70
|
self.additional_tools = [
|
|
65
71
|
t if isinstance(t, StructuredTool) else StructuredTool.from_function(t) for t in additional_tools
|
|
66
72
|
]
|
|
@@ -161,7 +167,7 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
161
167
|
self.tools_config.extend(new_tool_ids)
|
|
162
168
|
self.exported_tools = await self.registry.export_tools(new_tool_ids, ToolFormat.LANGCHAIN)
|
|
163
169
|
self.final_instructions, self.tools_context = create_default_prompt(
|
|
164
|
-
self.exported_tools, self.additional_tools, self.instructions
|
|
170
|
+
self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
|
|
165
171
|
)
|
|
166
172
|
if ask_user:
|
|
167
173
|
tool_messages.append(AIMessage(content=ai_msg))
|
|
@@ -184,41 +190,64 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
184
190
|
},
|
|
185
191
|
)
|
|
186
192
|
|
|
187
|
-
def playbook(state: CodeActState) -> Command[Literal["call_model"]]:
|
|
193
|
+
def playbook(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
|
|
188
194
|
playbook_mode = state.get("playbook_mode")
|
|
189
195
|
if playbook_mode == "planning":
|
|
196
|
+
plan_id = str(uuid.uuid4())
|
|
197
|
+
writer({
|
|
198
|
+
"type": "custom",
|
|
199
|
+
id: plan_id,
|
|
200
|
+
"name": "planning",
|
|
201
|
+
"data": {"update": bool(self.playbook)}
|
|
202
|
+
})
|
|
190
203
|
planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
|
|
191
204
|
messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
|
|
192
205
|
|
|
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})
|
|
206
|
+
model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookPlan)
|
|
207
|
+
response = model_with_structured_output.invoke(messages)
|
|
208
|
+
plan = cast(PlaybookPlan, response)
|
|
209
|
+
|
|
210
|
+
writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
|
|
211
|
+
return Command(update={"messages": [AIMessage(content=json.dumps(plan.dict()), additional_kwargs={"type": "planning", "plan": plan.steps, "update": bool(self.playbook)})], "playbook_mode": "confirming", "plan": plan.steps})
|
|
203
212
|
|
|
204
213
|
elif playbook_mode == "confirming":
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
# Deterministic routing based on three exact button inputs from UI
|
|
215
|
+
user_text = ""
|
|
216
|
+
for m in reversed(state["messages"]):
|
|
217
|
+
try:
|
|
218
|
+
if getattr(m, "type", "") in {"human", "user"}:
|
|
219
|
+
user_text = (get_message_text(m) or "").strip()
|
|
220
|
+
if user_text:
|
|
221
|
+
break
|
|
222
|
+
except Exception:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
t = user_text.lower()
|
|
226
|
+
if t == "yes, this is great":
|
|
210
227
|
return Command(goto="playbook", update={"playbook_mode": "generating"})
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
if t == "i would like to modify the plan":
|
|
229
|
+
prompt_ai = AIMessage(content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.", additional_kwargs={"stream": "true"})
|
|
230
|
+
return Command(update={"playbook_mode": "planning", "messages": [prompt_ai]})
|
|
231
|
+
if t == "let's do something else":
|
|
232
|
+
return Command(goto="call_model", update={"playbook_mode": "inactive"})
|
|
233
|
+
|
|
234
|
+
# Fallback safe default
|
|
235
|
+
return Command(goto="call_model", update={"playbook_mode": "inactive"})
|
|
213
236
|
|
|
214
237
|
elif playbook_mode == "generating":
|
|
238
|
+
generate_id = str(uuid.uuid4())
|
|
239
|
+
writer({
|
|
240
|
+
"type": "custom",
|
|
241
|
+
id: generate_id,
|
|
242
|
+
"name": "generating",
|
|
243
|
+
"data": {"update": bool(self.playbook)}
|
|
244
|
+
})
|
|
215
245
|
generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
|
|
216
246
|
messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
func_code =
|
|
221
|
-
func_code = func_code.strip()
|
|
247
|
+
|
|
248
|
+
model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookCode)
|
|
249
|
+
response = model_with_structured_output.invoke(messages)
|
|
250
|
+
func_code = cast(PlaybookCode, response).code
|
|
222
251
|
|
|
223
252
|
# Extract function name (handle both regular and async functions)
|
|
224
253
|
match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
|
|
@@ -228,47 +257,39 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
228
257
|
function_name = "generated_playbook"
|
|
229
258
|
|
|
230
259
|
# Save or update an Agent using the helper registry
|
|
231
|
-
saved_note = ""
|
|
232
260
|
try:
|
|
233
|
-
if not self.playbook_registry:
|
|
261
|
+
if not self.playbook_registry:
|
|
234
262
|
raise ValueError("Playbook registry is not configured")
|
|
235
263
|
|
|
236
264
|
# Build instructions payload embedding the plan and function code
|
|
237
265
|
instructions_payload = {
|
|
238
266
|
"playbookPlan": state["plan"],
|
|
239
|
-
"playbookScript":
|
|
240
|
-
"name": function_name,
|
|
241
|
-
"code": func_code,
|
|
242
|
-
},
|
|
267
|
+
"playbookScript": func_code,
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
# Convert tool ids list to dict
|
|
246
271
|
tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
|
|
247
272
|
|
|
248
|
-
res = self.playbook_registry.
|
|
273
|
+
res = self.playbook_registry.upsert_agent(
|
|
249
274
|
name=function_name,
|
|
250
275
|
description=f"Generated playbook: {function_name}",
|
|
251
276
|
instructions=instructions_payload,
|
|
252
277
|
tools=tool_dict,
|
|
253
278
|
visibility="private",
|
|
254
279
|
)
|
|
255
|
-
saved_note = f"Successfully created your playbook! Check it out here: [View Playbook](https://wingmen.info/agents/{res.id})"
|
|
256
280
|
except Exception as e:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# Mock tool call for exit_playbook_mode (for testing/demonstration)
|
|
260
|
-
mock_exit_tool_call = {"name": "exit_playbook_mode", "args": {}, "id": "mock_exit_playbook_123"}
|
|
261
|
-
mock_assistant_message = AIMessage(content=saved_note, tool_calls=[mock_exit_tool_call])
|
|
281
|
+
raise e
|
|
262
282
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
name
|
|
267
|
-
|
|
268
|
-
)
|
|
283
|
+
writer({
|
|
284
|
+
"type": "custom",
|
|
285
|
+
id: generate_id,
|
|
286
|
+
"name": "generating",
|
|
287
|
+
"data": {"id": str(res.id), "update": bool(self.playbook)}
|
|
288
|
+
})
|
|
289
|
+
mock_assistant_message = AIMessage(content=json.dumps(response.dict()), additional_kwargs={"type": "generating", "id": str(res.id), "update": bool(self.playbook)})
|
|
269
290
|
|
|
270
291
|
return Command(
|
|
271
|
-
update={"messages": [mock_assistant_message
|
|
292
|
+
update={"messages": [mock_assistant_message], "playbook_mode": "normal"}
|
|
272
293
|
)
|
|
273
294
|
|
|
274
295
|
async def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
|
|
@@ -277,7 +298,7 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
277
298
|
self.tools_config.extend(state.get("selected_tool_ids", []))
|
|
278
299
|
self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
|
|
279
300
|
self.final_instructions, self.tools_context = create_default_prompt(
|
|
280
|
-
self.exported_tools, self.additional_tools, self.instructions
|
|
301
|
+
self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
|
|
281
302
|
)
|
|
282
303
|
if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
|
|
283
304
|
return "playbook"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from universal_mcp.agents.codeact0.utils import light_copy
|
|
4
|
+
|
|
5
|
+
MAX_RETRIES = 3
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_context_str(source: Any | list[Any] | dict[str, Any]) -> str:
|
|
9
|
+
"""Converts context to a string representation."""
|
|
10
|
+
if not isinstance(source, dict):
|
|
11
|
+
if isinstance(source, list):
|
|
12
|
+
source = {f"doc_{i + 1}": str(doc) for i, doc in enumerate(source)}
|
|
13
|
+
else:
|
|
14
|
+
source = {"content": str(source)}
|
|
15
|
+
|
|
16
|
+
return "\n".join(f"<{k}>\n{str(v)}\n</{k}>" for k, v in source.items())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def smart_print(data: Any) -> None:
|
|
20
|
+
"""Prints a dictionary or list of dictionaries with string values truncated to 30 characters.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
data: Either a dictionary with string keys, or a list of such dictionaries
|
|
24
|
+
"""
|
|
25
|
+
print(light_copy(data)) # noqa: T201
|
|
@@ -4,10 +4,8 @@ from collections.abc import Sequence
|
|
|
4
4
|
|
|
5
5
|
from langchain_core.tools import StructuredTool
|
|
6
6
|
|
|
7
|
-
from universal_mcp.agents.codeact0.utils import schema_to_signature
|
|
8
|
-
|
|
9
7
|
uneditable_prompt = """
|
|
10
|
-
You are **
|
|
8
|
+
You are **Ruzo**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
|
|
11
9
|
|
|
12
10
|
Your job is to answer the user's question or perform the task they ask for.
|
|
13
11
|
- 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 +19,13 @@ Your job is to answer the user's question or perform the task they ask for.
|
|
|
21
19
|
- 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
20
|
- If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
**Code Execution Guidelines:**
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
- If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
|
|
26
|
+
- 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.
|
|
27
|
+
|
|
28
|
+
**Coding Best Practices:**
|
|
25
29
|
- Variables defined at the top level of previous code snippets can be referenced in your code.
|
|
26
30
|
- 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
31
|
- 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 +33,40 @@ GUIDELINES for writing code:
|
|
|
29
33
|
- 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
34
|
- 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
35
|
- Call all functions using keyword arguments only, never positional arguments.
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
|
|
37
|
+
**Async Functions (Critical Rules):**
|
|
38
|
+
Use async functions only as follows:
|
|
39
|
+
- Case 1: Top-level await without asyncio.run()
|
|
34
40
|
Wrap in async function and call with asyncio.run():
|
|
41
|
+
```python
|
|
35
42
|
async def main():
|
|
36
43
|
result = await some_async_function()
|
|
37
44
|
return result
|
|
38
45
|
asyncio.run(main())
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
```
|
|
47
|
+
- Case 2: Using asyncio.run() directly
|
|
48
|
+
If code already contains asyncio.run(), use as-is — do not wrap again:
|
|
49
|
+
```python
|
|
41
50
|
asyncio.run(some_async_function())
|
|
51
|
+
```
|
|
42
52
|
Rules:
|
|
43
53
|
- Never use await outside an async function
|
|
44
54
|
- Never use await asyncio.run()
|
|
45
55
|
- Never nest asyncio.run() calls
|
|
56
|
+
|
|
57
|
+
**Final Output Requirements:**
|
|
58
|
+
- 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.
|
|
59
|
+
- Always respond in github flavoured markdown format.
|
|
60
|
+
- For charts and diagrams, use mermaid chart in markdown directly.
|
|
61
|
+
- 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
62
|
"""
|
|
47
63
|
|
|
48
64
|
PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
|
|
49
65
|
|
|
50
|
-
TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
|
|
66
|
+
TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
|
|
67
|
+
Do not include the searching and loading of tools. Assume that the tools have already been loaded.
|
|
68
|
+
The plan is a sequence of steps.
|
|
69
|
+
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
70
|
|
|
52
71
|
Your plan should:
|
|
53
72
|
1. Identify the key steps in the workflow
|
|
@@ -56,24 +75,25 @@ Your plan should:
|
|
|
56
75
|
4. Be clear and concise
|
|
57
76
|
|
|
58
77
|
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.
|
|
78
|
+
{
|
|
79
|
+
"steps": [
|
|
80
|
+
"Connect to database using `db_connection_string`",
|
|
81
|
+
"Query user data for `user_id`",
|
|
82
|
+
"Process results and calculate `metric_name`",
|
|
83
|
+
"Send notification to `email_address`"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
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
88
|
"""
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
|
|
91
|
+
PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function.
|
|
92
|
+
Your response must be ONLY the Python code for the function.
|
|
93
|
+
Do not include any other text, markdown, or explanations in your response.
|
|
94
|
+
Your response should start with `def` or `async def`.
|
|
74
95
|
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
96
|
The parameters of the function should be the same as the final confirmed playbook plan.
|
|
76
|
-
Do not include anything other than python code in your response
|
|
77
97
|
"""
|
|
78
98
|
|
|
79
99
|
|
|
@@ -90,107 +110,18 @@ def make_safe_function_name(name: str) -> str:
|
|
|
90
110
|
return safe_name
|
|
91
111
|
|
|
92
112
|
|
|
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
113
|
def create_default_prompt(
|
|
171
114
|
tools: Sequence[StructuredTool],
|
|
172
115
|
additional_tools: Sequence[StructuredTool],
|
|
173
116
|
base_prompt: str | None = None,
|
|
117
|
+
playbook: object | None = None,
|
|
174
118
|
):
|
|
175
119
|
system_prompt = uneditable_prompt.strip() + (
|
|
176
120
|
"\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
|
|
177
121
|
)
|
|
178
122
|
tools_context = {}
|
|
179
|
-
for tool in tools:
|
|
180
|
-
if hasattr(tool, "func") and tool.func is not None:
|
|
181
|
-
tool_callable = tool.func
|
|
182
|
-
is_async = False
|
|
183
|
-
elif hasattr(tool, "coroutine") and tool.coroutine is not None:
|
|
184
|
-
tool_callable = tool.coroutine
|
|
185
|
-
is_async = True
|
|
186
|
-
system_prompt += f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
|
|
187
|
-
"""{tool.description}"""
|
|
188
|
-
...
|
|
189
|
-
'''
|
|
190
|
-
safe_name = make_safe_function_name(tool.name)
|
|
191
|
-
tools_context[safe_name] = tool_callable
|
|
192
123
|
|
|
193
|
-
for tool in additional_tools:
|
|
124
|
+
for tool in tools + additional_tools:
|
|
194
125
|
if hasattr(tool, "func") and tool.func is not None:
|
|
195
126
|
tool_callable = tool.func
|
|
196
127
|
is_async = False
|
|
@@ -207,4 +138,24 @@ def create_default_prompt(
|
|
|
207
138
|
if base_prompt and base_prompt.strip():
|
|
208
139
|
system_prompt += f"Your goal is to perform the following task:\n\n{base_prompt}"
|
|
209
140
|
|
|
141
|
+
# Append existing playbook (plan + code) if provided
|
|
142
|
+
try:
|
|
143
|
+
if playbook and hasattr(playbook, "instructions"):
|
|
144
|
+
pb = playbook.instructions or {}
|
|
145
|
+
plan = pb.get("playbookPlan")
|
|
146
|
+
code = pb.get("playbookScript")
|
|
147
|
+
if plan or code:
|
|
148
|
+
system_prompt += "\n\nExisting Playbook Provided:\n"
|
|
149
|
+
if plan:
|
|
150
|
+
if isinstance(plan, list):
|
|
151
|
+
plan_block = "\n".join(f"- {str(s)}" for s in plan)
|
|
152
|
+
else:
|
|
153
|
+
plan_block = str(plan)
|
|
154
|
+
system_prompt += f"Plan Steps:\n{plan_block}\n"
|
|
155
|
+
if code:
|
|
156
|
+
system_prompt += f"\nScript:\n```python\n{str(code)}\n```\n"
|
|
157
|
+
except Exception:
|
|
158
|
+
# Silently ignore formatting issues
|
|
159
|
+
pass
|
|
160
|
+
|
|
210
161
|
return system_prompt, tools_context
|
|
@@ -1,6 +1,15 @@
|
|
|
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.")
|
|
4
13
|
|
|
5
14
|
|
|
6
15
|
def _enqueue(left: list, right: list) -> list:
|
|
@@ -36,5 +45,5 @@ class CodeActState(AgentState):
|
|
|
36
45
|
"""State for the playbook agent."""
|
|
37
46
|
selected_tool_ids: Annotated[list[str], _enqueue]
|
|
38
47
|
"""Queue for tools exported from registry"""
|
|
39
|
-
plan: str | None
|
|
48
|
+
plan: list[str] | None
|
|
40
49
|
"""Plan for the playbook agent."""
|
|
@@ -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
|
|
@@ -224,19 +225,9 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
|
|
|
224
225
|
- answer (str): Generated answer with markdown formatting and citation numbers [1][2]
|
|
225
226
|
- citations (list[str]): List of source URLs corresponding to citation numbers
|
|
226
227
|
"""
|
|
227
|
-
await tool_registry.export_tools(["
|
|
228
|
-
|
|
229
|
-
|
|
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})
|
|
230
|
+
logger.info(f"Web search response: {response}")
|
|
240
231
|
|
|
241
232
|
# Extract only desired fields
|
|
242
233
|
return {
|
|
@@ -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.
|
|
@@ -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,277 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Literal, cast
|
|
4
|
-
|
|
5
|
-
from langchain.chat_models import init_chat_model
|
|
6
|
-
from langchain_openai import AzureChatOpenAI
|
|
7
|
-
|
|
8
|
-
from universal_mcp.agents.codeact0.utils import get_message_text, light_copy
|
|
9
|
-
|
|
10
|
-
MAX_RETRIES = 3
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_context_str(source: Any | list[Any] | dict[str, Any]) -> str:
|
|
14
|
-
"""Converts context to a string representation."""
|
|
15
|
-
if not isinstance(source, dict):
|
|
16
|
-
if isinstance(source, list):
|
|
17
|
-
source = {f"doc_{i + 1}": str(doc) for i, doc in enumerate(source)}
|
|
18
|
-
else:
|
|
19
|
-
source = {"content": str(source)}
|
|
20
|
-
|
|
21
|
-
return "\n".join(f"<{k}>\n{str(v)}\n</{k}>" for k, v in source.items())
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def smart_print(data: Any) -> None:
|
|
25
|
-
"""Prints a dictionary or list of dictionaries with string values truncated to 30 characters.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
data: Either a dictionary with string keys, or a list of such dictionaries
|
|
29
|
-
"""
|
|
30
|
-
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)
|
|
File without changes
|
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/.github/workflows/release-please.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/datasets/codeact.jsonl
RENAMED
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/datasets/exact.jsonl
RENAMED
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/evals/datasets/tasks.jsonl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/hil.py
RENAMED
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/llm.py
RENAMED
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/react.py
RENAMED
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/sandbox.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/simple.py
RENAMED
|
File without changes
|
{universal_mcp_agents-0.1.20rc1 → universal_mcp_agents-0.1.21}/src/universal_mcp/agents/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|