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.

Files changed (49) hide show
  1. universal_mcp/agents/__init__.py +1 -1
  2. universal_mcp/agents/base.py +3 -0
  3. universal_mcp/agents/bigtool/__init__.py +1 -1
  4. universal_mcp/agents/bigtool/__main__.py +4 -3
  5. universal_mcp/agents/bigtool/agent.py +3 -2
  6. universal_mcp/agents/bigtool/graph.py +68 -31
  7. universal_mcp/agents/bigtool/prompts.py +2 -2
  8. universal_mcp/agents/bigtool/tools.py +17 -4
  9. universal_mcp/agents/builder/__main__.py +129 -28
  10. universal_mcp/agents/builder/builder.py +149 -161
  11. universal_mcp/agents/builder/helper.py +71 -0
  12. universal_mcp/agents/builder/prompts.py +94 -160
  13. universal_mcp/agents/codeact0/__init__.py +2 -1
  14. universal_mcp/agents/codeact0/agent.py +13 -5
  15. universal_mcp/agents/codeact0/langgraph_agent.py +14 -0
  16. universal_mcp/agents/codeact0/llm_tool.py +1 -2
  17. universal_mcp/agents/codeact0/playbook_agent.py +353 -0
  18. universal_mcp/agents/codeact0/prompts.py +126 -41
  19. universal_mcp/agents/codeact0/sandbox.py +43 -32
  20. universal_mcp/agents/codeact0/state.py +27 -3
  21. universal_mcp/agents/codeact0/tools.py +180 -0
  22. universal_mcp/agents/codeact0/utils.py +89 -75
  23. universal_mcp/agents/shared/__main__.py +44 -0
  24. universal_mcp/agents/shared/prompts.py +49 -98
  25. universal_mcp/agents/shared/tool_node.py +160 -176
  26. universal_mcp/agents/utils.py +71 -0
  27. universal_mcp/applications/ui/app.py +2 -2
  28. {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/METADATA +3 -3
  29. universal_mcp_agents-0.1.15.dist-info/RECORD +50 -0
  30. universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
  31. universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
  32. universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -13
  33. universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
  34. universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
  35. universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
  36. universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
  37. universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
  38. universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
  39. universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
  40. universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
  41. universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
  42. universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
  43. universal_mcp/agents/planner/__init__.py +0 -51
  44. universal_mcp/agents/planner/__main__.py +0 -28
  45. universal_mcp/agents/planner/graph.py +0 -85
  46. universal_mcp/agents/planner/prompts.py +0 -14
  47. universal_mcp/agents/planner/state.py +0 -11
  48. universal_mcp_agents-0.1.13.dist-info/RECORD +0 -63
  49. {universal_mcp_agents-0.1.13.dist-info → universal_mcp_agents-0.1.15.dist-info}/WHEEL +0 -0
@@ -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 CodeActAgent as CodeActRepl
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
 
@@ -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(
@@ -56,7 +56,7 @@ class BigToolAgent(BaseAgent):
56
56
  compiled_graph = graph_builder.compile(checkpointer=self.memory)
57
57
  return compiled_graph
58
58
  except Exception as e:
59
- raise e
59
+ raise Exception(f"Failed to build AutoAgent graph: {e}")
60
60
 
61
61
  @property
62
62
  def graph(self):
@@ -2,15 +2,16 @@ import asyncio
2
2
 
3
3
  from loguru import logger
4
4
  from universal_mcp.agentr.registry import AgentrRegistry
5
- from universal_mcp.agents.bigtoolcache import BigToolAgentCache
5
+
6
+ from universal_mcp.agents.bigtool import BigToolAgent
6
7
 
7
8
 
8
9
  async def main():
9
- agent = BigToolAgentCache(
10
+ agent = BigToolAgent(
10
11
  registry=AgentrRegistry(),
11
12
  )
12
13
  async for event in agent.stream(
13
- user_input="Send an email to manoj@agentr.dev",
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
- from universal_mcp.agents.bigtoolcache import BigToolAgentCache
2
+
3
+ from universal_mcp.agents.bigtool import BigToolAgent
3
4
 
4
5
 
5
6
  async def agent():
6
- agent_object = await BigToolAgentCache(
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
- current_tools = await registry.export_tools(tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN)
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
- if isinstance(base_model, ChatAnthropic):
52
- model_with_tools = base_model.bind_tools(
53
- unique_tools,
54
- tool_choice="auto",
55
- parallel_tool_calls=False,
56
- cache_control={"type": "ephemeral", "ttl": "1h"},
57
- )
58
- else:
59
- model_with_tools = base_model.bind_tools(
60
- unique_tools,
61
- tool_choice="auto",
62
- parallel_tool_calls=False,
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
- response = cast(AIMessage, await model_with_tools.ainvoke(messages))
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
- if tool_call["name"] == "load_tools": # Handle load_tools separately
84
- valid_tools = await get_valid_tools(tool_ids=tool_call["args"]["tool_ids"], registry=registry)
85
- new_tool_ids.extend(valid_tools)
86
- # Create tool message response
87
- tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
88
- elif tool_call["name"] == "search_tools":
89
- tool_result = await meta_tools["search_tools"].ainvoke(tool_call["args"])
90
- elif tool_call["name"] == "web_search":
91
- tool_result = await meta_tools["web_search"].ainvoke(tool_call["args"])
92
- else:
93
- # Load tools first
94
- await registry.export_tools([tool_call["name"]], ToolFormat.LANGCHAIN)
95
- tool_result = await registry.call_tool(tool_call["name"], tool_call["args"])
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("agent", agent_node)
110
- workflow.add_node("execute_tools", execute_tools_node)
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
- if len(app_tools[app]) < 5:
38
- app_tools[app].append(f"{tool['id']}: {tool['description']}")
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
- "Use outlook instead of gmail",
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
- final_result = {}
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
- result = await agent.invoke(user_input=user_input, thread_id=thread_id)
39
- final_result.update(result) # Keep updating the final result
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
- generated_agent = final_result.get("generated_agent")
42
- tool_config = final_result.get("tool_config")
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 generated_agent:
57
+ if latest_agent:
45
58
  logger.info("--- Generated/Modified Agent ---")
46
- logger.info(f"Name: {generated_agent.name}")
47
- logger.info(f"Description: {generated_agent.description}")
48
- logger.info(f"Expertise: {generated_agent.expertise}")
49
- logger.info(f"Instructions:\n{generated_agent.instructions}")
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 tool_config:
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 tool_config.items())
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="anthropic/claude-4-sonnet-20250514",
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": "human",
75
- "content": "Hey, can you look at our main branch on the universal-mcp repo and tell me what the last 3 pull requests were?",
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": "Of course. The last 3 pull requests are: #101 'Fix login bug', #102 'Update documentation', and #103 'Add new chart component'.",
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": "human",
83
- "content": "Awesome, thanks. Now can you draft a new Google Doc and put that list in there for me?",
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": "Done. I have created a new Google Doc with the list of the last 3 pull requests."},
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
- result = await agent.invoke(user_input=payload_str, thread_id=thread_id)
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__":