universal-mcp-agents 0.1.13__py3-none-any.whl → 0.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of universal-mcp-agents might be problematic. Click here for more details.
- universal_mcp/agents/__init__.py +1 -1
- universal_mcp/agents/base.py +3 -0
- universal_mcp/agents/bigtool/__init__.py +1 -1
- universal_mcp/agents/bigtool/__main__.py +4 -3
- universal_mcp/agents/bigtool/agent.py +3 -2
- universal_mcp/agents/bigtool/graph.py +68 -31
- universal_mcp/agents/bigtool/prompts.py +2 -2
- universal_mcp/agents/bigtool/tools.py +17 -4
- universal_mcp/agents/builder/__main__.py +129 -28
- universal_mcp/agents/builder/builder.py +149 -161
- universal_mcp/agents/builder/helper.py +71 -0
- universal_mcp/agents/builder/prompts.py +94 -160
- universal_mcp/agents/codeact0/__init__.py +2 -1
- universal_mcp/agents/codeact0/agent.py +13 -5
- universal_mcp/agents/codeact0/langgraph_agent.py +14 -0
- universal_mcp/agents/codeact0/llm_tool.py +1 -2
- universal_mcp/agents/codeact0/playbook_agent.py +353 -0
- universal_mcp/agents/codeact0/prompts.py +126 -41
- universal_mcp/agents/codeact0/sandbox.py +43 -32
- universal_mcp/agents/codeact0/state.py +27 -3
- universal_mcp/agents/codeact0/tools.py +180 -0
- universal_mcp/agents/codeact0/utils.py +89 -75
- universal_mcp/agents/shared/__main__.py +44 -0
- universal_mcp/agents/shared/prompts.py +49 -98
- universal_mcp/agents/shared/tool_node.py +160 -176
- universal_mcp/agents/utils.py +71 -0
- universal_mcp/applications/ui/app.py +2 -2
- {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/METADATA +3 -3
- universal_mcp_agents-0.1.15.dist-info/RECORD +50 -0
- universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
- universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -13
- universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
- universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
- universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
- universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
- universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
- universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
- universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
- universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
- universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
- universal_mcp/agents/planner/__init__.py +0 -51
- universal_mcp/agents/planner/__main__.py +0 -28
- universal_mcp/agents/planner/graph.py +0 -85
- universal_mcp/agents/planner/prompts.py +0 -14
- universal_mcp/agents/planner/state.py +0 -11
- universal_mcp_agents-0.1.13.dist-info/RECORD +0 -63
- {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/WHEEL +0 -0
universal_mcp/agents/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ from universal_mcp.agents.base import BaseAgent
|
|
|
4
4
|
from universal_mcp.agents.bigtool import BigToolAgent
|
|
5
5
|
from universal_mcp.agents.builder.builder import BuilderAgent
|
|
6
6
|
from universal_mcp.agents.codeact import CodeActAgent as CodeActScript
|
|
7
|
-
from universal_mcp.agents.codeact0 import
|
|
7
|
+
from universal_mcp.agents.codeact0 import CodeActPlaybookAgent as CodeActRepl
|
|
8
8
|
from universal_mcp.agents.react import ReactAgent
|
|
9
9
|
from universal_mcp.agents.simple import SimpleAgent
|
|
10
10
|
|
universal_mcp/agents/base.py
CHANGED
|
@@ -85,6 +85,7 @@ class BaseAgent:
|
|
|
85
85
|
event = cast(AIMessageChunk, event)
|
|
86
86
|
event.usage_metadata = aggregate.usage_metadata
|
|
87
87
|
logger.debug(f"Usage metadata: {event.usage_metadata}")
|
|
88
|
+
event.content = "" # Clear the message since it would have already been streamed above
|
|
88
89
|
yield event
|
|
89
90
|
|
|
90
91
|
async def stream_interactive(self, thread_id: str, user_input: str):
|
|
@@ -115,6 +116,8 @@ class BaseAgent:
|
|
|
115
116
|
"recursion_limit": 25,
|
|
116
117
|
"configurable": {"thread_id": thread_id},
|
|
117
118
|
"metadata": run_metadata,
|
|
119
|
+
"run_id": thread_id,
|
|
120
|
+
"run_name": self.name,
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
result = await self._graph.ainvoke(
|
|
@@ -2,15 +2,16 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
4
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from universal_mcp.agents.bigtool import BigToolAgent
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
async def main():
|
|
9
|
-
agent =
|
|
10
|
+
agent = BigToolAgent(
|
|
10
11
|
registry=AgentrRegistry(),
|
|
11
12
|
)
|
|
12
13
|
async for event in agent.stream(
|
|
13
|
-
user_input="
|
|
14
|
+
user_input="Load a supabase tool",
|
|
14
15
|
thread_id="test123",
|
|
15
16
|
):
|
|
16
17
|
logger.info(event.content)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from universal_mcp.agents.bigtool import BigToolAgent
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
async def agent():
|
|
6
|
-
agent_object = await
|
|
7
|
+
agent_object = await BigToolAgent(
|
|
7
8
|
registry=AgentrRegistry(),
|
|
8
9
|
)._build_graph()
|
|
9
10
|
return agent_object
|
|
@@ -7,10 +7,12 @@ from langchain_core.language_models import BaseChatModel
|
|
|
7
7
|
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
|
8
8
|
from langchain_core.tools import BaseTool
|
|
9
9
|
from langgraph.graph import StateGraph
|
|
10
|
-
from langgraph.types import Command
|
|
10
|
+
from langgraph.types import Command, RetryPolicy
|
|
11
11
|
from universal_mcp.tools.registry import ToolRegistry
|
|
12
12
|
from universal_mcp.types import ToolFormat
|
|
13
13
|
|
|
14
|
+
from universal_mcp.agents.utils import filter_retry_on
|
|
15
|
+
|
|
14
16
|
from .state import State
|
|
15
17
|
from .tools import get_valid_tools
|
|
16
18
|
|
|
@@ -31,7 +33,12 @@ def build_graph(
|
|
|
31
33
|
|
|
32
34
|
# Combine meta tools with currently loaded tools
|
|
33
35
|
if len(state["selected_tool_ids"]) > 0:
|
|
34
|
-
|
|
36
|
+
try:
|
|
37
|
+
current_tools = await registry.export_tools(
|
|
38
|
+
tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN
|
|
39
|
+
)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise Exception(f"Failed to export selected tools: {e}")
|
|
35
42
|
else:
|
|
36
43
|
current_tools = []
|
|
37
44
|
all_tools = (
|
|
@@ -48,23 +55,30 @@ def build_graph(
|
|
|
48
55
|
seen_names.add(tool.name)
|
|
49
56
|
unique_tools.append(tool)
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
try:
|
|
59
|
+
if isinstance(base_model, ChatAnthropic):
|
|
60
|
+
model_with_tools = base_model.bind_tools(
|
|
61
|
+
unique_tools,
|
|
62
|
+
tool_choice="auto",
|
|
63
|
+
parallel_tool_calls=False,
|
|
64
|
+
cache_control={"type": "ephemeral", "ttl": "1h"},
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
model_with_tools = base_model.bind_tools(
|
|
68
|
+
unique_tools,
|
|
69
|
+
tool_choice="auto",
|
|
70
|
+
parallel_tool_calls=False,
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise Exception(f"Failed to bind tools to model: {e}")
|
|
64
74
|
|
|
65
75
|
# Get response from model
|
|
66
76
|
messages = [SystemMessage(content=system_prompt), *state["messages"]]
|
|
67
|
-
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
response = cast(AIMessage, await model_with_tools.ainvoke(messages))
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise Exception(f"Model invocation failed: {e}")
|
|
68
82
|
|
|
69
83
|
if response.tool_calls:
|
|
70
84
|
return Command(goto="execute_tools", update={"messages": [response]})
|
|
@@ -78,27 +92,42 @@ def build_graph(
|
|
|
78
92
|
|
|
79
93
|
tool_messages = []
|
|
80
94
|
new_tool_ids = []
|
|
95
|
+
ask_user = False
|
|
81
96
|
|
|
82
97
|
for tool_call in tool_calls:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
try:
|
|
99
|
+
if tool_call["name"] == "load_tools": # Handle load_tools separately
|
|
100
|
+
valid_tools, unconnected_links = await get_valid_tools(
|
|
101
|
+
tool_ids=tool_call["args"]["tool_ids"], registry=registry
|
|
102
|
+
)
|
|
103
|
+
new_tool_ids.extend(valid_tools)
|
|
104
|
+
# Create tool message response
|
|
105
|
+
tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
|
|
106
|
+
if unconnected_links:
|
|
107
|
+
ask_user = True
|
|
108
|
+
links = "\n".join(unconnected_links)
|
|
109
|
+
ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {links} "
|
|
110
|
+
|
|
111
|
+
elif tool_call["name"] == "search_tools":
|
|
112
|
+
tool_result = await meta_tools["search_tools"].ainvoke(tool_call["args"])
|
|
113
|
+
elif tool_call["name"] == "web_search":
|
|
114
|
+
tool_result = await meta_tools["web_search"].ainvoke(tool_call["args"])
|
|
115
|
+
else:
|
|
116
|
+
# Load tools first
|
|
117
|
+
await registry.export_tools([tool_call["name"]], ToolFormat.LANGCHAIN)
|
|
118
|
+
tool_result = await registry.call_tool(tool_call["name"], tool_call["args"])
|
|
119
|
+
except Exception as e:
|
|
120
|
+
tool_result = f"Error during {tool_call}: {e}"
|
|
121
|
+
|
|
96
122
|
tool_message = ToolMessage(
|
|
97
123
|
content=json.dumps(tool_result),
|
|
98
124
|
name=tool_call["name"],
|
|
99
125
|
tool_call_id=tool_call["id"],
|
|
100
126
|
)
|
|
101
127
|
tool_messages.append(tool_message)
|
|
128
|
+
if ask_user:
|
|
129
|
+
tool_messages.append(AIMessage(content=ai_msg))
|
|
130
|
+
return Command(update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
|
|
102
131
|
|
|
103
132
|
return Command(goto="agent", update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
|
|
104
133
|
|
|
@@ -106,8 +135,16 @@ def build_graph(
|
|
|
106
135
|
workflow = StateGraph(State)
|
|
107
136
|
|
|
108
137
|
# Add nodes
|
|
109
|
-
workflow.add_node(
|
|
110
|
-
|
|
138
|
+
workflow.add_node(
|
|
139
|
+
"agent",
|
|
140
|
+
agent_node,
|
|
141
|
+
retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on, initial_interval=2, backoff_factor=2),
|
|
142
|
+
)
|
|
143
|
+
workflow.add_node(
|
|
144
|
+
"execute_tools",
|
|
145
|
+
execute_tools_node,
|
|
146
|
+
retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on, initial_interval=2, backoff_factor=2),
|
|
147
|
+
)
|
|
111
148
|
|
|
112
149
|
# Set entry point
|
|
113
150
|
workflow.set_entry_point("agent")
|
|
@@ -5,9 +5,9 @@ SYSTEM_PROMPT = """You are a helpful AI assistant, called {name}.
|
|
|
5
5
|
**Core Directives:**
|
|
6
6
|
1. **Always Use Tools for Tasks:** For any user request that requires an action (e.g., sending an email, searching for information, creating an event, displaying a chart), you MUST use a tool. Do not refuse a task if a tool might exist for it.
|
|
7
7
|
|
|
8
|
-
2. Check if your existing tools or knowledge can handle the user's request. If they can, use them. If they cannot, you must call the `search_tools` function to find the right tools for the user's request.You must not use the same/similar query multiple times in the list. The list should have multiple queries only if the task has clearly different sub-tasks. If you do not find any specific relevant tools, use the pre-loaded generic tools.
|
|
8
|
+
2. Check if your existing tools or knowledge can handle the user's request. If they can, use them. If they cannot, you must call the `search_tools` function to find the right tools for the user's request. You must not use the same/similar query multiple times in the list. The list should have multiple queries only if the task has clearly different sub-tasks. If you do not find any specific relevant tools, use the pre-loaded generic tools. Only use `search_tools` if your existing capabilities cannot handle the request.
|
|
9
9
|
|
|
10
|
-
3. **Load Tools:** After looking at the output of `search_tools`, you MUST call the `load_tools` function to load only the tools you want to use. Provide the full tool ids, not just the app names. Use your judgement to eliminate irrelevant apps that came up just because of semantic similarity. However, sometimes, multiple apps might be relevant for the same task. Prefer connected apps over unconnected apps while breaking a tie. If more than one relevant app (or none of the relevant apps) are connected, you must ask the user to choose the app. In case the user asks you to use an app that is not connected, call the apps tools normally. The tool will return a link for connecting that you should pass on to the user.
|
|
10
|
+
3. **Load Tools:** After looking at the output of `search_tools`, you MUST call the `load_tools` function to load only the tools you want to use. Provide the full tool ids, not just the app names. Use your judgement to eliminate irrelevant apps that came up just because of semantic similarity. However, sometimes, multiple apps might be relevant for the same task. Prefer connected apps over unconnected apps while breaking a tie. If more than one relevant app (or none of the relevant apps) are connected, you must ask the user to choose the app. In case the user asks you to use an app that is not connected, call the apps tools normally. The tool will return a link for connecting that you should pass on to the user. Only load tools if your existing capabilities cannot handle the request.
|
|
11
11
|
|
|
12
12
|
4. **Strictly Follow the Process:** Your only job in your first turn is to analyze the user's request and answer using existing tools/knowledge or `search_tools` with a concise query describing the core task. Do not engage in conversation, or extend the conversation beyond the user's request.
|
|
13
13
|
|
|
@@ -34,8 +34,8 @@ def create_meta_tools(tool_registry: ToolRegistry) -> dict[str, Any]:
|
|
|
34
34
|
for tools_list in query_results:
|
|
35
35
|
for tool in tools_list:
|
|
36
36
|
app = tool["id"].split("__")[0]
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
cleaned_desc = tool["description"].split("Context:")[0].strip()
|
|
38
|
+
app_tools[app].append(f"{tool['id']}: {cleaned_desc}")
|
|
39
39
|
|
|
40
40
|
# Build result string efficiently
|
|
41
41
|
result_parts = []
|
|
@@ -98,8 +98,13 @@ def create_meta_tools(tool_registry: ToolRegistry) -> dict[str, Any]:
|
|
|
98
98
|
return {"search_tools": search_tools, "load_tools": load_tools, "web_search": web_search}
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
async def get_valid_tools(tool_ids: list[str], registry: ToolRegistry) -> list[str]:
|
|
101
|
+
async def get_valid_tools(tool_ids: list[str], registry: ToolRegistry) -> tuple[list[str], list[str]]:
|
|
102
|
+
"""For a given list of tool_ids, validates the tools and returns a list of links for the apps that have not been logged in"""
|
|
102
103
|
correct, incorrect = [], []
|
|
104
|
+
connections = await registry.list_connected_apps()
|
|
105
|
+
connected_apps = {connection["app_id"] for connection in connections}
|
|
106
|
+
unconnected = set()
|
|
107
|
+
unconnected_links = []
|
|
103
108
|
app_tool_list: dict[str, set[str]] = {}
|
|
104
109
|
|
|
105
110
|
# Group tool_ids by app for fewer registry calls
|
|
@@ -132,10 +137,18 @@ async def get_valid_tools(tool_ids: list[str], registry: ToolRegistry) -> list[s
|
|
|
132
137
|
if available is None:
|
|
133
138
|
incorrect.extend(tool_id for tool_id, _ in tool_entries)
|
|
134
139
|
continue
|
|
140
|
+
if app not in connected_apps and app not in unconnected:
|
|
141
|
+
unconnected.add(app)
|
|
142
|
+
text = registry.client.get_authorization_url(app)
|
|
143
|
+
start = text.find(":") + 1
|
|
144
|
+
end = text.find(". R", start)
|
|
145
|
+
url = text[start:end].strip()
|
|
146
|
+
markdown_link = f"[{app}]({url})"
|
|
147
|
+
unconnected_links.append(markdown_link)
|
|
135
148
|
for tool_id, tool_name in tool_entries:
|
|
136
149
|
if tool_name in available:
|
|
137
150
|
correct.append(tool_id)
|
|
138
151
|
else:
|
|
139
152
|
incorrect.append(tool_id)
|
|
140
153
|
|
|
141
|
-
return correct
|
|
154
|
+
return correct, unconnected_links
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import json
|
|
3
2
|
from uuid import uuid4
|
|
4
3
|
|
|
5
4
|
from langgraph.checkpoint.memory import MemorySaver
|
|
6
5
|
from loguru import logger
|
|
7
6
|
from universal_mcp.agentr.registry import AgentrRegistry
|
|
7
|
+
from universal_mcp.types import ToolConfig
|
|
8
8
|
|
|
9
9
|
from universal_mcp.agents.builder.builder import BuilderAgent
|
|
10
|
+
from universal_mcp.agents.builder.state import Agent
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
async def run_interactive_build():
|
|
@@ -27,30 +28,43 @@ async def run_interactive_build():
|
|
|
27
28
|
|
|
28
29
|
conversation_script = [
|
|
29
30
|
"Send an email to manoj@agentr.dev with the subject 'Hello' and body 'This is a test of the Gmail agent.' from my Gmail account.",
|
|
30
|
-
"
|
|
31
|
+
"Add the mail to my draft also",
|
|
32
|
+
"also make a reddit post on r/test with the title 'Test Post' and body 'This is a test post from the Reddit agent.'",
|
|
31
33
|
]
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
# These variables will hold the state between turns
|
|
36
|
+
latest_agent: Agent | None = None
|
|
37
|
+
latest_tools: ToolConfig | None = None
|
|
38
|
+
|
|
34
39
|
for i, user_input in enumerate(conversation_script):
|
|
35
40
|
logger.info(f"\n--- Conversation Turn {i + 1} ---")
|
|
36
41
|
logger.info(f"User Request: '{user_input}'")
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
# Construct the payload based on the current state of the conversation
|
|
44
|
+
payload = {"userInput": user_input}
|
|
45
|
+
if latest_agent:
|
|
46
|
+
# On subsequent turns, pass the existing agent and tools for modification
|
|
47
|
+
payload["agent"] = latest_agent.model_dump() # Convert Pydantic model to dict
|
|
48
|
+
payload["tools"] = latest_tools
|
|
49
|
+
|
|
50
|
+
# The invoke method now takes a single payload dictionary
|
|
51
|
+
result = await agent.invoke(thread_id=thread_id, user_input=payload)
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
# Update the latest state for the next turn
|
|
54
|
+
latest_agent = result.get("generated_agent")
|
|
55
|
+
latest_tools = result.get("tool_config")
|
|
43
56
|
|
|
44
|
-
if
|
|
57
|
+
if latest_agent:
|
|
45
58
|
logger.info("--- Generated/Modified Agent ---")
|
|
46
|
-
logger.info(f"Name: {
|
|
47
|
-
logger.info(f"Description: {
|
|
48
|
-
logger.info(f"Expertise: {
|
|
49
|
-
logger.info(f"Instructions:\n{
|
|
59
|
+
logger.info(f"Name: {latest_agent.name}")
|
|
60
|
+
logger.info(f"Description: {latest_agent.description}")
|
|
61
|
+
logger.info(f"Expertise: {latest_agent.expertise}")
|
|
62
|
+
logger.info(f"Instructions:\n{latest_agent.instructions}")
|
|
63
|
+
logger.info(f"Schedule: {latest_agent.schedule}")
|
|
50
64
|
|
|
51
|
-
if
|
|
65
|
+
if latest_tools:
|
|
52
66
|
logger.info("--- Selected Tools ---")
|
|
53
|
-
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in
|
|
67
|
+
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in latest_tools.items())
|
|
54
68
|
logger.info(tools_str)
|
|
55
69
|
else:
|
|
56
70
|
logger.info("--- Selected Tools ---")
|
|
@@ -65,35 +79,62 @@ async def run_conversation_build():
|
|
|
65
79
|
agent = BuilderAgent(
|
|
66
80
|
name="Builder Agent",
|
|
67
81
|
instructions="You build agents from conversation transcripts.",
|
|
68
|
-
model="
|
|
82
|
+
model="azure/gpt-4.1",
|
|
69
83
|
registry=registry,
|
|
70
84
|
)
|
|
71
85
|
|
|
72
86
|
sample_conversation_history = [
|
|
87
|
+
{"type": "human", "content": "hi"},
|
|
88
|
+
{"type": "ai", "content": "Hello! How can I help you today?"},
|
|
89
|
+
{"type": "human", "content": "use the zenquotes tool to tell me a quote"},
|
|
90
|
+
{"type": "ai", "content": ""},
|
|
91
|
+
{
|
|
92
|
+
"type": "tool",
|
|
93
|
+
"content": "\"Tools from zenquotes (status: connected by user):\\n - zenquotes__get_random_quote: Fetches a random inspirational quote from the Zen Quotes API via an HTTP request. It parses the JSON response to extract the quote and author, returning them as a single formatted string ('quote - author'). This function is the primary tool provided by the ZenquotesApp.\\n - zenquotes__get_random_quote: Fetches a random inspirational quote from the Zen Quotes API via an HTTP request. It parses the JSON response to extract the quote and author, returning them as a single formatted string ('quote - author'). This function is the primary tool provided by the ZenquotesApp.\\n\\nTools from perplexity (status: NOT connected by user):\\n - perplexity__answer_with_search: Queries the Perplexity Chat Completions API for a web-search-grounded answer. It sends the user's prompt and model parameters to the `/chat/completions` endpoint, then parses the response to return the synthesized content and a list of supporting source citations, ideal for real-time information retrieval.\\n - perplexity__answer_with_search: Queries the Perplexity Chat Completions API for a web-search-grounded answer. It sends the user's prompt and model parameters to the `/chat/completions` endpoint, then parses the response to return the synthesized content and a list of supporting source citations, ideal for real-time information retrieval.\\n\\nCall load_tools to select the required tools only.\"",
|
|
94
|
+
},
|
|
95
|
+
{"type": "ai", "content": ""},
|
|
73
96
|
{
|
|
74
|
-
"type": "
|
|
75
|
-
"content": "
|
|
97
|
+
"type": "tool",
|
|
98
|
+
"content": "\"Successfully loaded 1 tools: ['zenquotes__get_random_quote']\"",
|
|
99
|
+
"name": "zenquotes__get_random_quote",
|
|
100
|
+
},
|
|
101
|
+
{"type": "ai", "content": ""},
|
|
102
|
+
{
|
|
103
|
+
"type": "tool",
|
|
104
|
+
"content": '"Decide upon your major definite purpose in life and then organize all your activities around it. - Brian Tracy"',
|
|
76
105
|
},
|
|
77
106
|
{
|
|
78
107
|
"type": "ai",
|
|
79
|
-
"content":
|
|
108
|
+
"content": 'Here’s your quote: \n**"Decide upon your major definite purpose in life and then organize all your activities around it." – Brian Tracy**',
|
|
109
|
+
},
|
|
110
|
+
{"type": "human", "content": "send this quote to ankit@agentr.dev using gmail"},
|
|
111
|
+
{"type": "ai", "content": ""},
|
|
112
|
+
{
|
|
113
|
+
"type": "tool",
|
|
114
|
+
"content": '"Tools from google_mail (status: connected by user):\\n - google_mail__send_email: Composes and immediately sends an email message via the Gmail API. It can function as a reply within an existing conversation if a `thread_id` is provided. This action is distinct from `send_draft`, which sends a previously saved draft message, or `create_draft`, which only saves an email.\\n - google_mail__send_draft: Sends a pre-existing Gmail draft identified by its unique ID. It posts to the `/drafts/send` endpoint, converting a saved draft into a sent message. This function acts on drafts from `create_draft` and differs from `send_email`, which composes and sends an email in one step.\\n - google_mail__create_draft: Saves a new email draft in Gmail with a specified recipient, subject, and body. An optional thread ID can create the draft as a reply within an existing conversation, distinguishing it from `send_email`, which sends immediately.\\n - google_mail__get_draft: Retrieves a specific Gmail draft by its unique ID. This function allows specifying the output format (e.g., full, raw) to control the response detail. Unlike `list_drafts`, it fetches a single, known draft rather than a collection of multiple drafts.\\n\\nCall load_tools to select the required tools only."',
|
|
80
115
|
},
|
|
116
|
+
{"type": "ai", "content": ""},
|
|
81
117
|
{
|
|
82
|
-
"type": "
|
|
83
|
-
"content": "
|
|
118
|
+
"type": "tool",
|
|
119
|
+
"content": "\"Successfully loaded 1 tools: ['google_mail__send_email']\"",
|
|
120
|
+
"name": "google_mail__send_email",
|
|
84
121
|
},
|
|
85
|
-
{"type": "ai", "content": "
|
|
122
|
+
{"type": "ai", "content": ""},
|
|
123
|
+
{"type": "tool", "content": '{"id": "199765690b278b56", "threadId": "199765690b278b56", "labelIds": ["SENT"]}'},
|
|
124
|
+
{"type": "ai", "content": "The quote has been sent to **ankit@agentr.dev** successfully. ✅"},
|
|
86
125
|
]
|
|
87
|
-
sample_tool_config = {"github": ["get_pull_requests"], "google_docs": ["create_document"]}
|
|
88
|
-
wingman_payload = {"conversation_history": sample_conversation_history, "tool_config": sample_tool_config}
|
|
89
126
|
|
|
90
127
|
logger.info(f"Payload Conversation History Length: {len(sample_conversation_history)} messages")
|
|
91
|
-
logger.info(f"Payload Tools Provided: {list(sample_tool_config.keys())}")
|
|
92
128
|
|
|
93
|
-
# The payload must be passed as a JSON string in the 'user_input'
|
|
94
|
-
payload_str = json.dumps(wingman_payload)
|
|
95
129
|
thread_id = str(uuid4())
|
|
96
|
-
|
|
130
|
+
|
|
131
|
+
# The payload contains the messages and a high-level instruction for the builder
|
|
132
|
+
payload = {
|
|
133
|
+
"userInput": "",
|
|
134
|
+
"messages": sample_conversation_history,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
result = await agent.invoke(thread_id=thread_id, user_input=payload)
|
|
97
138
|
|
|
98
139
|
generated_agent = result.get("generated_agent")
|
|
99
140
|
tool_config = result.get("tool_config")
|
|
@@ -116,9 +157,69 @@ async def run_conversation_build():
|
|
|
116
157
|
logger.error("Error: Tool configuration is missing.")
|
|
117
158
|
|
|
118
159
|
|
|
160
|
+
async def run_modification_with_manual_tool():
|
|
161
|
+
"""
|
|
162
|
+
Simulates a scenario where a user manually adds a tool to an agent's
|
|
163
|
+
configuration, and then uses the builder to modify the agent for a
|
|
164
|
+
different reason, expecting the manually added tool to be preserved.
|
|
165
|
+
"""
|
|
166
|
+
logger.info("\n\n--- SCENARIO 3: MODIFY AGENT WITH MANUAL TOOL ADDITION ---")
|
|
167
|
+
|
|
168
|
+
registry = AgentrRegistry()
|
|
169
|
+
memory = MemorySaver()
|
|
170
|
+
agent = BuilderAgent(
|
|
171
|
+
name="Builder Agent",
|
|
172
|
+
instructions="You are a builder agent that creates other agents.",
|
|
173
|
+
model="azure/gpt-4.1",
|
|
174
|
+
registry=registry,
|
|
175
|
+
memory=memory,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
thread_id = str(uuid4())
|
|
179
|
+
|
|
180
|
+
initial_request = "Send an email to manoj@agentr.dev with the subject 'Hello' using my Gmail account."
|
|
181
|
+
logger.info(f"User Request: '{initial_request}'")
|
|
182
|
+
|
|
183
|
+
# Initial agent creation
|
|
184
|
+
initial_payload = {"userInput": initial_request}
|
|
185
|
+
initial_result = await agent.invoke(thread_id=thread_id, user_input=initial_payload)
|
|
186
|
+
|
|
187
|
+
initial_agent = initial_result.get("generated_agent")
|
|
188
|
+
initial_tools = initial_result.get("tool_config")
|
|
189
|
+
|
|
190
|
+
logger.info("--- Initial Tools ---")
|
|
191
|
+
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in initial_tools.items())
|
|
192
|
+
logger.info(tools_str)
|
|
193
|
+
|
|
194
|
+
# Manually add a new tool to the configuration
|
|
195
|
+
manually_modified_tools = initial_tools.copy()
|
|
196
|
+
manually_modified_tools["reddit"] = ["create_post"]
|
|
197
|
+
logger.info("--- Manually Modified Tools ---")
|
|
198
|
+
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in manually_modified_tools.items())
|
|
199
|
+
logger.info(tools_str)
|
|
200
|
+
|
|
201
|
+
modification_request = "Also add the above email to my draft"
|
|
202
|
+
logger.info(f"User Request: '{modification_request}'")
|
|
203
|
+
|
|
204
|
+
# Prepare payload for modification, passing the existing agent and the manually updated tools
|
|
205
|
+
modification_payload = {
|
|
206
|
+
"userInput": modification_request,
|
|
207
|
+
"agent": initial_agent.model_dump(), # Convert Pydantic model to dict
|
|
208
|
+
"tools": manually_modified_tools,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
final_result = await agent.invoke(thread_id=thread_id, user_input=modification_payload)
|
|
212
|
+
|
|
213
|
+
final_tools = final_result.get("tool_config")
|
|
214
|
+
logger.info("--- Final Tools After Modification (should include manual addition) ---")
|
|
215
|
+
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in final_tools.items())
|
|
216
|
+
logger.info(tools_str)
|
|
217
|
+
|
|
218
|
+
|
|
119
219
|
async def main():
|
|
120
|
-
await run_interactive_build()
|
|
220
|
+
# await run_interactive_build()
|
|
121
221
|
await run_conversation_build()
|
|
222
|
+
# await run_modification_with_manual_tool()
|
|
122
223
|
|
|
123
224
|
|
|
124
225
|
if __name__ == "__main__":
|