universal-mcp-agents 0.1.10__py3-none-any.whl → 0.1.12__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.
- universal_mcp/agents/__init__.py +17 -19
- universal_mcp/agents/base.py +10 -7
- universal_mcp/agents/{bigtoolcache → bigtool}/__init__.py +2 -2
- universal_mcp/agents/{bigtoolcache → bigtool}/__main__.py +0 -1
- universal_mcp/agents/{bigtoolcache → bigtool}/agent.py +0 -1
- universal_mcp/agents/{bigtoolcache → bigtool}/graph.py +6 -5
- universal_mcp/agents/builder/__main__.py +125 -0
- universal_mcp/agents/builder/builder.py +225 -0
- universal_mcp/agents/builder/prompts.py +173 -0
- universal_mcp/agents/builder/state.py +24 -0
- universal_mcp/agents/cli.py +3 -2
- universal_mcp/agents/codeact/__main__.py +2 -4
- universal_mcp/agents/codeact/agent.py +188 -108
- universal_mcp/agents/codeact/models.py +11 -0
- universal_mcp/agents/codeact/prompts.py +34 -43
- universal_mcp/agents/codeact/sandbox.py +78 -40
- universal_mcp/agents/codeact/state.py +5 -4
- universal_mcp/agents/codeact0/__init__.py +3 -0
- universal_mcp/agents/codeact0/__main__.py +35 -0
- universal_mcp/agents/codeact0/agent.py +136 -0
- universal_mcp/agents/codeact0/config.py +77 -0
- universal_mcp/agents/codeact0/langgraph_graph.py +17 -0
- universal_mcp/agents/codeact0/legacy_codeact.py +104 -0
- universal_mcp/agents/codeact0/llm_tool.py +379 -0
- universal_mcp/agents/codeact0/prompts.py +156 -0
- universal_mcp/agents/codeact0/sandbox.py +90 -0
- universal_mcp/agents/codeact0/state.py +12 -0
- universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +4 -0
- universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +10 -0
- universal_mcp/agents/codeact0/usecases/11-github.yaml +13 -0
- universal_mcp/agents/codeact0/usecases/2-reddit.yaml +27 -0
- universal_mcp/agents/codeact0/usecases/2.1-instructions.md +81 -0
- universal_mcp/agents/codeact0/usecases/2.2-instructions.md +71 -0
- universal_mcp/agents/codeact0/usecases/3-earnings.yaml +4 -0
- universal_mcp/agents/codeact0/usecases/4-maps.yaml +41 -0
- universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +8 -0
- universal_mcp/agents/codeact0/usecases/6-contract.yaml +6 -0
- universal_mcp/agents/codeact0/usecases/7-overnight.yaml +14 -0
- universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +25 -0
- universal_mcp/agents/codeact0/usecases/9-learning.yaml +9 -0
- universal_mcp/agents/codeact0/utils.py +374 -0
- universal_mcp/agents/hil.py +4 -4
- universal_mcp/agents/planner/__init__.py +7 -1
- universal_mcp/agents/react.py +11 -3
- universal_mcp/agents/simple.py +12 -2
- universal_mcp/agents/utils.py +17 -0
- universal_mcp/applications/llm/__init__.py +3 -0
- universal_mcp/applications/llm/app.py +158 -0
- universal_mcp/applications/ui/app.py +118 -144
- {universal_mcp_agents-0.1.10.dist-info → universal_mcp_agents-0.1.12.dist-info}/METADATA +1 -1
- universal_mcp_agents-0.1.12.dist-info/RECORD +65 -0
- universal_mcp/agents/bigtool2/__init__.py +0 -67
- universal_mcp/agents/bigtool2/__main__.py +0 -23
- universal_mcp/agents/bigtool2/agent.py +0 -13
- universal_mcp/agents/bigtool2/graph.py +0 -155
- universal_mcp/agents/bigtool2/meta_tools.py +0 -120
- universal_mcp/agents/bigtool2/prompts.py +0 -15
- universal_mcp/agents/bigtoolcache/state.py +0 -27
- universal_mcp/agents/builder.py +0 -204
- universal_mcp_agents-0.1.10.dist-info/RECORD +0 -42
- /universal_mcp/agents/{bigtoolcache → bigtool}/context.py +0 -0
- /universal_mcp/agents/{bigtoolcache → bigtool}/prompts.py +0 -0
- /universal_mcp/agents/{bigtool2 → bigtool}/state.py +0 -0
- /universal_mcp/agents/{bigtoolcache → bigtool}/tools.py +0 -0
- {universal_mcp_agents-0.1.10.dist-info → universal_mcp_agents-0.1.12.dist-info}/WHEEL +0 -0
universal_mcp/agents/__init__.py
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
1
3
|
from universal_mcp.agents.base import BaseAgent
|
|
2
|
-
from universal_mcp.agents.
|
|
3
|
-
from universal_mcp.agents.
|
|
4
|
-
from universal_mcp.agents.
|
|
5
|
-
from universal_mcp.agents.codeact import CodeActAgent
|
|
6
|
-
from universal_mcp.agents.planner import PlannerAgent
|
|
4
|
+
from universal_mcp.agents.bigtool import BigToolAgent
|
|
5
|
+
from universal_mcp.agents.builder.builder import BuilderAgent
|
|
6
|
+
from universal_mcp.agents.codeact0 import CodeActAgent as CodeActRepl
|
|
7
|
+
from universal_mcp.agents.codeact import CodeActAgent as CodeActScript
|
|
7
8
|
from universal_mcp.agents.react import ReactAgent
|
|
8
9
|
from universal_mcp.agents.simple import SimpleAgent
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def get_agent(agent_name:
|
|
12
|
+
def get_agent(agent_name: Literal["react", "simple", "builder", "bigtool", "codeact-script", "codeact-repl"]):
|
|
12
13
|
if agent_name == "react":
|
|
13
14
|
return ReactAgent
|
|
14
15
|
elif agent_name == "simple":
|
|
15
16
|
return SimpleAgent
|
|
16
17
|
elif agent_name == "builder":
|
|
17
18
|
return BuilderAgent
|
|
18
|
-
elif agent_name == "
|
|
19
|
-
return
|
|
20
|
-
elif agent_name == "
|
|
21
|
-
return
|
|
22
|
-
elif agent_name == "
|
|
23
|
-
return
|
|
24
|
-
elif agent_name == "codeact":
|
|
25
|
-
return CodeActAgent
|
|
19
|
+
elif agent_name == "bigtool":
|
|
20
|
+
return BigToolAgent
|
|
21
|
+
elif agent_name == "codeact-script":
|
|
22
|
+
return CodeActScript
|
|
23
|
+
elif agent_name == "codeact-repl":
|
|
24
|
+
return CodeActRepl
|
|
26
25
|
else:
|
|
27
26
|
raise ValueError(
|
|
28
|
-
f"Unknown agent: {agent_name}. Possible values: react, simple, builder,
|
|
27
|
+
f"Unknown agent: {agent_name}. Possible values: react, simple, builder, bigtool, codeact-script, codeact-repl"
|
|
29
28
|
)
|
|
30
29
|
|
|
31
30
|
|
|
@@ -33,9 +32,8 @@ __all__ = [
|
|
|
33
32
|
"BaseAgent",
|
|
34
33
|
"ReactAgent",
|
|
35
34
|
"SimpleAgent",
|
|
36
|
-
"AutoAgent",
|
|
37
|
-
"BigToolAgent",
|
|
38
|
-
"PlannerAgent",
|
|
39
35
|
"BuilderAgent",
|
|
40
|
-
"
|
|
36
|
+
"BigToolAgent",
|
|
37
|
+
"CodeActScript",
|
|
38
|
+
"CodeActRepl",
|
|
41
39
|
]
|
universal_mcp/agents/base.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from typing import cast
|
|
3
3
|
from uuid import uuid4
|
|
4
4
|
|
|
5
|
-
from langchain_core.messages import
|
|
5
|
+
from langchain_core.messages import AIMessageChunk
|
|
6
6
|
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
7
7
|
from langgraph.graph import StateGraph
|
|
8
8
|
from langgraph.types import Command
|
|
@@ -90,7 +90,7 @@ class BaseAgent:
|
|
|
90
90
|
async def stream_interactive(self, thread_id: str, user_input: str):
|
|
91
91
|
await self.ainit()
|
|
92
92
|
with self.cli.display_agent_response_streaming(self.name) as stream_updater:
|
|
93
|
-
async for event in self.stream(thread_id, user_input):
|
|
93
|
+
async for event in self.stream(thread_id=thread_id, user_input=user_input):
|
|
94
94
|
if isinstance(event.content, list):
|
|
95
95
|
thinking_content = "".join([c.get("thinking", "") for c in event.content])
|
|
96
96
|
stream_updater.update(thinking_content, type_="thinking")
|
|
@@ -112,6 +112,7 @@ class BaseAgent:
|
|
|
112
112
|
run_metadata.update(metadata)
|
|
113
113
|
|
|
114
114
|
run_config = {
|
|
115
|
+
"recursion_limit": 25,
|
|
115
116
|
"configurable": {"thread_id": thread_id},
|
|
116
117
|
"metadata": run_metadata,
|
|
117
118
|
}
|
|
@@ -123,6 +124,11 @@ class BaseAgent:
|
|
|
123
124
|
)
|
|
124
125
|
return result
|
|
125
126
|
|
|
127
|
+
async def get_state(self, thread_id: str):
|
|
128
|
+
await self.ainit()
|
|
129
|
+
state = await self._graph.aget_state(config={"configurable": {"thread_id": thread_id}})
|
|
130
|
+
return state
|
|
131
|
+
|
|
126
132
|
async def run_interactive(self, thread_id: str = str(uuid4())):
|
|
127
133
|
"""Main application loop"""
|
|
128
134
|
|
|
@@ -133,7 +139,7 @@ class BaseAgent:
|
|
|
133
139
|
# Main loop
|
|
134
140
|
while True:
|
|
135
141
|
try:
|
|
136
|
-
state = self.
|
|
142
|
+
state = await self.get_state(thread_id=thread_id)
|
|
137
143
|
if state.interrupts:
|
|
138
144
|
value = self.cli.handle_interrupt(state.interrupts[0])
|
|
139
145
|
self._graph.invoke(
|
|
@@ -168,14 +174,11 @@ class BaseAgent:
|
|
|
168
174
|
continue
|
|
169
175
|
|
|
170
176
|
# Process with agent
|
|
171
|
-
await self.stream_interactive(thread_id, user_input)
|
|
177
|
+
await self.stream_interactive(thread_id=thread_id, user_input=user_input)
|
|
172
178
|
|
|
173
179
|
except KeyboardInterrupt:
|
|
174
180
|
self.cli.display_info("\nGoodbye! 👋")
|
|
175
181
|
break
|
|
176
182
|
except Exception as e:
|
|
177
|
-
import traceback
|
|
178
|
-
|
|
179
|
-
traceback.print_exc()
|
|
180
183
|
self.cli.display_error(f"An error occurred: {str(e)}")
|
|
181
184
|
break
|
|
@@ -13,7 +13,7 @@ from .prompts import SYSTEM_PROMPT
|
|
|
13
13
|
from .tools import create_meta_tools
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class
|
|
16
|
+
class BigToolAgent(BaseAgent):
|
|
17
17
|
def __init__(
|
|
18
18
|
self,
|
|
19
19
|
registry: ToolRegistry,
|
|
@@ -63,4 +63,4 @@ class BigToolAgentCache(BaseAgent):
|
|
|
63
63
|
return self._graph
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
__all__ = ["
|
|
66
|
+
__all__ = ["BigToolAgent"]
|
|
@@ -8,7 +8,6 @@ from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
|
|
8
8
|
from langchain_core.tools import BaseTool
|
|
9
9
|
from langgraph.graph import StateGraph
|
|
10
10
|
from langgraph.types import Command
|
|
11
|
-
from loguru import logger
|
|
12
11
|
from universal_mcp.tools.registry import ToolRegistry
|
|
13
12
|
from universal_mcp.types import ToolFormat
|
|
14
13
|
|
|
@@ -35,7 +34,11 @@ def build_graph(
|
|
|
35
34
|
current_tools = await registry.export_tools(tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN)
|
|
36
35
|
else:
|
|
37
36
|
current_tools = []
|
|
38
|
-
all_tools =
|
|
37
|
+
all_tools = (
|
|
38
|
+
[meta_tools["search_tools"], meta_tools["load_tools"], meta_tools.get("web_search")]
|
|
39
|
+
+ default_tools
|
|
40
|
+
+ current_tools
|
|
41
|
+
)
|
|
39
42
|
|
|
40
43
|
# Remove duplicates based on tool name
|
|
41
44
|
seen_names = set()
|
|
@@ -81,7 +84,7 @@ def build_graph(
|
|
|
81
84
|
valid_tools = await get_valid_tools(tool_ids=tool_call["args"]["tool_ids"], registry=registry)
|
|
82
85
|
new_tool_ids.extend(valid_tools)
|
|
83
86
|
# Create tool message response
|
|
84
|
-
tool_result=f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
|
|
87
|
+
tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
|
|
85
88
|
elif tool_call["name"] == "search_tools":
|
|
86
89
|
tool_result = await meta_tools["search_tools"].ainvoke(tool_call["args"])
|
|
87
90
|
elif tool_call["name"] == "web_search":
|
|
@@ -99,8 +102,6 @@ def build_graph(
|
|
|
99
102
|
|
|
100
103
|
return Command(goto="agent", update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
105
|
# Define the graph
|
|
105
106
|
workflow = StateGraph(State)
|
|
106
107
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from universal_mcp.agentr.registry import AgentrRegistry
|
|
8
|
+
|
|
9
|
+
from universal_mcp.agents.builder.builder import BuilderAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def run_interactive_build():
|
|
13
|
+
"""Simulates a multi-turn conversation to build and then modify an agent."""
|
|
14
|
+
logger.info("--- SCENARIO 1: INTERACTIVE AGENT BUILD & MODIFY ---")
|
|
15
|
+
|
|
16
|
+
registry = AgentrRegistry()
|
|
17
|
+
memory = MemorySaver()
|
|
18
|
+
agent = BuilderAgent(
|
|
19
|
+
name="Builder Agent",
|
|
20
|
+
instructions="You are a builder agent that creates other agents.",
|
|
21
|
+
model="anthropic/claude-4-sonnet-20250514",
|
|
22
|
+
registry=registry,
|
|
23
|
+
memory=memory,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
thread_id = str(uuid4())
|
|
27
|
+
|
|
28
|
+
conversation_script = [
|
|
29
|
+
"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
|
+
]
|
|
32
|
+
|
|
33
|
+
final_result = {}
|
|
34
|
+
for i, user_input in enumerate(conversation_script):
|
|
35
|
+
logger.info(f"\n--- Conversation Turn {i + 1} ---")
|
|
36
|
+
logger.info(f"User Request: '{user_input}'")
|
|
37
|
+
|
|
38
|
+
result = await agent.invoke(user_input=user_input, thread_id=thread_id)
|
|
39
|
+
final_result.update(result) # Keep updating the final result
|
|
40
|
+
|
|
41
|
+
generated_agent = final_result.get("generated_agent")
|
|
42
|
+
tool_config = final_result.get("tool_config")
|
|
43
|
+
|
|
44
|
+
if generated_agent:
|
|
45
|
+
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}")
|
|
50
|
+
|
|
51
|
+
if tool_config:
|
|
52
|
+
logger.info("--- Selected Tools ---")
|
|
53
|
+
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in tool_config.items())
|
|
54
|
+
logger.info(tools_str)
|
|
55
|
+
else:
|
|
56
|
+
logger.info("--- Selected Tools ---")
|
|
57
|
+
logger.info("No tools selected for this agent yet.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def run_conversation_build():
|
|
61
|
+
"""Simulates a one-shot agent build from a conversation history payload."""
|
|
62
|
+
logger.info("\n\n--- SCENARIO 2: AGENT BUILD FROM CONVERSATION HISTORY ---")
|
|
63
|
+
|
|
64
|
+
registry = AgentrRegistry()
|
|
65
|
+
agent = BuilderAgent(
|
|
66
|
+
name="Builder Agent",
|
|
67
|
+
instructions="You build agents from conversation transcripts.",
|
|
68
|
+
model="anthropic/claude-4-sonnet-20250514",
|
|
69
|
+
registry=registry,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
sample_conversation_history = [
|
|
73
|
+
{
|
|
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?",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"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'.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "human",
|
|
83
|
+
"content": "Awesome, thanks. Now can you draft a new Google Doc and put that list in there for me?",
|
|
84
|
+
},
|
|
85
|
+
{"type": "ai", "content": "Done. I have created a new Google Doc with the list of the last 3 pull requests."},
|
|
86
|
+
]
|
|
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
|
+
|
|
90
|
+
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
|
+
|
|
93
|
+
# The payload must be passed as a JSON string in the 'user_input'
|
|
94
|
+
payload_str = json.dumps(wingman_payload)
|
|
95
|
+
thread_id = str(uuid4())
|
|
96
|
+
result = await agent.invoke(user_input=payload_str, thread_id=thread_id)
|
|
97
|
+
|
|
98
|
+
generated_agent = result.get("generated_agent")
|
|
99
|
+
tool_config = result.get("tool_config")
|
|
100
|
+
|
|
101
|
+
if generated_agent:
|
|
102
|
+
logger.info("\n--- Generated Agent Profile ---")
|
|
103
|
+
logger.info(f"Name: {generated_agent.name}")
|
|
104
|
+
logger.info(f"Description: {generated_agent.description}")
|
|
105
|
+
logger.info(f"Expertise: {generated_agent.expertise}")
|
|
106
|
+
logger.info(f"Instructions:\n{generated_agent.instructions}")
|
|
107
|
+
logger.info(f"Schedule: {generated_agent.schedule}")
|
|
108
|
+
else:
|
|
109
|
+
logger.error("Error: Agent profile was not generated.")
|
|
110
|
+
|
|
111
|
+
if tool_config:
|
|
112
|
+
logger.info("--- Final Tool Configuration ---")
|
|
113
|
+
tools_str = "\n".join(f"- {app}: {', '.join(tool_ids)}" for app, tool_ids in tool_config.items())
|
|
114
|
+
logger.info(tools_str)
|
|
115
|
+
else:
|
|
116
|
+
logger.error("Error: Tool configuration is missing.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def main():
|
|
120
|
+
await run_interactive_build()
|
|
121
|
+
await run_conversation_build()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
from langchain_core.language_models import BaseChatModel
|
|
5
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
6
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
7
|
+
from langgraph.graph import END, START, StateGraph
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from universal_mcp.tools.registry import ToolRegistry
|
|
10
|
+
from universal_mcp.types import ToolConfig
|
|
11
|
+
|
|
12
|
+
from universal_mcp.agents.base import BaseAgent
|
|
13
|
+
from universal_mcp.agents.builder.prompts import (
|
|
14
|
+
AGENT_BUILDER_INSTRUCTIONS,
|
|
15
|
+
AGENT_FROM_CONVERSATION_PROMPT,
|
|
16
|
+
TASK_SYNTHESIS_PROMPT,
|
|
17
|
+
)
|
|
18
|
+
from universal_mcp.agents.builder.state import Agent, BuilderState
|
|
19
|
+
from universal_mcp.agents.llm import load_chat_model
|
|
20
|
+
from universal_mcp.agents.shared.tool_node import build_tool_node_graph
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def generate_agent(llm: BaseChatModel, task: str, old_agent: Agent | None = None) -> Agent:
|
|
24
|
+
"""Generates an agent from a task, optionally modifying an existing one."""
|
|
25
|
+
prompt_parts = [AGENT_BUILDER_INSTRUCTIONS]
|
|
26
|
+
if old_agent:
|
|
27
|
+
prompt_parts.append(
|
|
28
|
+
"\nThe user wants to modify the following agent design. "
|
|
29
|
+
"Incorporate their feedback into a new design.\n\n"
|
|
30
|
+
f"**User Feedback:** {task}\n\n"
|
|
31
|
+
f"{old_agent.model_dump_json(indent=2)}"
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
prompt_parts.append(f"\n\n**Task:** {task}")
|
|
35
|
+
|
|
36
|
+
prompt = "\n".join(prompt_parts)
|
|
37
|
+
structured_llm = llm.with_structured_output(Agent)
|
|
38
|
+
agent = await structured_llm.ainvoke(prompt)
|
|
39
|
+
return agent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BuilderAgent(BaseAgent):
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
name: str,
|
|
46
|
+
instructions: str,
|
|
47
|
+
model: str,
|
|
48
|
+
registry: ToolRegistry,
|
|
49
|
+
memory: BaseCheckpointSaver | None = None,
|
|
50
|
+
**kwargs,
|
|
51
|
+
):
|
|
52
|
+
super().__init__(
|
|
53
|
+
name=name,
|
|
54
|
+
instructions=instructions,
|
|
55
|
+
model=model,
|
|
56
|
+
memory=memory,
|
|
57
|
+
**kwargs,
|
|
58
|
+
)
|
|
59
|
+
self.registry = registry
|
|
60
|
+
self.llm: BaseChatModel = load_chat_model(model, thinking=False)
|
|
61
|
+
|
|
62
|
+
def _entry_point_router(self, state: BuilderState):
|
|
63
|
+
"""
|
|
64
|
+
Determines the entry point of the graph based on the input format and conversation history.
|
|
65
|
+
- If input is a JSON with 'conversation_history', it builds from the conversation.
|
|
66
|
+
- If an agent has already been generated, it assumes a modification is requested.
|
|
67
|
+
- Otherwise, it starts a fresh build from a text prompt.
|
|
68
|
+
"""
|
|
69
|
+
last_message_content = state["messages"][-1].content
|
|
70
|
+
try:
|
|
71
|
+
# Case 1: Input is a JSON for building from a conversation
|
|
72
|
+
payload = json.loads(last_message_content)
|
|
73
|
+
if isinstance(payload, dict) and "conversation_history" in payload and "tool_config" in payload:
|
|
74
|
+
logger.info("Routing to: build from conversation history.")
|
|
75
|
+
return "synthesize_from_conversation"
|
|
76
|
+
except (json.JSONDecodeError, TypeError):
|
|
77
|
+
# Input is not a valid JSON, proceed as an interactive build
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# Case 2: It's an interactive build, check for modification vs. new
|
|
81
|
+
if state.get("generated_agent"):
|
|
82
|
+
logger.info("Routing to: modify existing agent.")
|
|
83
|
+
return "synthesize_new_task"
|
|
84
|
+
else:
|
|
85
|
+
logger.info("Routing to: new agent build.")
|
|
86
|
+
return "prepare_for_build"
|
|
87
|
+
|
|
88
|
+
async def _prepare_for_build(self, state: BuilderState):
|
|
89
|
+
"""Sets the initial user task to begin the build process."""
|
|
90
|
+
last_message = state["messages"][-1]
|
|
91
|
+
task = last_message.content
|
|
92
|
+
yield {
|
|
93
|
+
"user_task": task,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async def _create_agent(self, state: BuilderState):
|
|
97
|
+
"""Creates or updates the agent definition from a user_task."""
|
|
98
|
+
task = state["user_task"]
|
|
99
|
+
agent = state.get("generated_agent")
|
|
100
|
+
|
|
101
|
+
generated_agent = await generate_agent(self.llm, task, agent)
|
|
102
|
+
yield {
|
|
103
|
+
"generated_agent": generated_agent,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async def _get_tool_config_for_task(self, task: str) -> ToolConfig:
|
|
107
|
+
"""Helper method to find and configure tools for a given task string."""
|
|
108
|
+
tool_finder_graph = build_tool_node_graph(self.llm, self.registry)
|
|
109
|
+
initial_state = {
|
|
110
|
+
"original_task": task,
|
|
111
|
+
"decomposition_attempts": 0,
|
|
112
|
+
}
|
|
113
|
+
final_state = await tool_finder_graph.ainvoke(initial_state)
|
|
114
|
+
execution_plan = final_state.get("execution_plan")
|
|
115
|
+
|
|
116
|
+
if not execution_plan:
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
apps_with_tools = defaultdict(list)
|
|
120
|
+
for step in execution_plan:
|
|
121
|
+
app_id = step.get("app_id")
|
|
122
|
+
tool_ids = step.get("tool_ids")
|
|
123
|
+
if app_id and tool_ids:
|
|
124
|
+
apps_with_tools[app_id].extend(tool_ids)
|
|
125
|
+
|
|
126
|
+
return {app_id: list(set(tools)) for app_id, tools in apps_with_tools.items()}
|
|
127
|
+
|
|
128
|
+
async def _create_tool_config(self, state: BuilderState):
|
|
129
|
+
"""Creates the tool configuration for the agent."""
|
|
130
|
+
task = state["user_task"]
|
|
131
|
+
tool_config = await self._get_tool_config_for_task(task)
|
|
132
|
+
yield {
|
|
133
|
+
"tool_config": tool_config,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async def _synthesize_new_task_from_feedback(self, state: BuilderState):
|
|
137
|
+
"""Synthesizes a new user_task from the original task and subsequent user feedback."""
|
|
138
|
+
original_task = next((msg.content for msg in state["messages"] if isinstance(msg, HumanMessage)), None)
|
|
139
|
+
modification_request = state["messages"][-1].content
|
|
140
|
+
|
|
141
|
+
if not original_task:
|
|
142
|
+
raise ValueError("Could not find the original task in the conversation history.")
|
|
143
|
+
|
|
144
|
+
synthesis_prompt = TASK_SYNTHESIS_PROMPT.format(
|
|
145
|
+
original_task=original_task,
|
|
146
|
+
modification_request=modification_request,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
response = await self.llm.ainvoke(synthesis_prompt)
|
|
150
|
+
new_synthesized_task = response.content.strip()
|
|
151
|
+
logger.info(f"The new synthesized task is: {new_synthesized_task}")
|
|
152
|
+
yield {
|
|
153
|
+
"user_task": new_synthesized_task,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async def _synthesize_from_conversation(self, state: BuilderState):
|
|
157
|
+
"""
|
|
158
|
+
Takes conversation history and used tools from input to synthesize a complete agent profile.
|
|
159
|
+
This is a one-shot generation.
|
|
160
|
+
"""
|
|
161
|
+
content_str = state["messages"][-1].content
|
|
162
|
+
initial_input = json.loads(content_str)
|
|
163
|
+
|
|
164
|
+
conversation_history = initial_input.get("conversation_history")
|
|
165
|
+
tool_config = initial_input.get("tool_config")
|
|
166
|
+
|
|
167
|
+
if not conversation_history or not tool_config:
|
|
168
|
+
raise ValueError("Input must be a dictionary containing 'conversation_history' and 'tool_config'.")
|
|
169
|
+
|
|
170
|
+
prompt = AGENT_FROM_CONVERSATION_PROMPT.format(
|
|
171
|
+
conversation_history=json.dumps(conversation_history, indent=2),
|
|
172
|
+
tool_config=json.dumps(tool_config, indent=2),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
structured_llm = self.llm.with_structured_output(Agent)
|
|
176
|
+
generated_agent_profile = await structured_llm.ainvoke(prompt)
|
|
177
|
+
|
|
178
|
+
yield {
|
|
179
|
+
"generated_agent": generated_agent_profile,
|
|
180
|
+
"tool_config": tool_config,
|
|
181
|
+
"messages": [
|
|
182
|
+
AIMessage(
|
|
183
|
+
content=f"Successfully generated agent '{generated_agent_profile.name}' from the conversation history."
|
|
184
|
+
)
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async def _build_graph(self):
|
|
189
|
+
"""Builds the conversational agent graph."""
|
|
190
|
+
builder = StateGraph(BuilderState)
|
|
191
|
+
|
|
192
|
+
# Add nodes
|
|
193
|
+
builder.add_node("prepare_for_build", self._prepare_for_build)
|
|
194
|
+
builder.add_node("create_agent", self._create_agent)
|
|
195
|
+
builder.add_node("create_tool_config", self._create_tool_config)
|
|
196
|
+
builder.add_node("synthesize_new_task", self._synthesize_new_task_from_feedback)
|
|
197
|
+
builder.add_node("synthesize_from_conversation", self._synthesize_from_conversation)
|
|
198
|
+
|
|
199
|
+
# The conditional entry point decides the workflow
|
|
200
|
+
builder.add_conditional_edges(
|
|
201
|
+
START,
|
|
202
|
+
self._entry_point_router,
|
|
203
|
+
{
|
|
204
|
+
"prepare_for_build": "prepare_for_build",
|
|
205
|
+
"synthesize_new_task": "synthesize_new_task",
|
|
206
|
+
"synthesize_from_conversation": "synthesize_from_conversation",
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Path for a fresh interactive build
|
|
211
|
+
builder.add_edge("prepare_for_build", "create_agent")
|
|
212
|
+
builder.add_edge("prepare_for_build", "create_tool_config")
|
|
213
|
+
|
|
214
|
+
# Path for modifying an existing build
|
|
215
|
+
builder.add_edge("synthesize_new_task", "create_agent")
|
|
216
|
+
builder.add_edge("synthesize_new_task", "create_tool_config")
|
|
217
|
+
|
|
218
|
+
# Path for building from conversation ends after its single step
|
|
219
|
+
builder.add_edge("synthesize_from_conversation", END)
|
|
220
|
+
|
|
221
|
+
# Interactive creation nodes lead to the end of the run
|
|
222
|
+
builder.add_edge("create_agent", END)
|
|
223
|
+
builder.add_edge("create_tool_config", END)
|
|
224
|
+
|
|
225
|
+
return builder.compile(checkpointer=self.memory)
|