universal-mcp-agents 0.1.14__py3-none-any.whl → 0.1.16__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 (47) hide show
  1. universal_mcp/agents/__init__.py +1 -1
  2. universal_mcp/agents/base.py +2 -1
  3. universal_mcp/agents/bigtool/__main__.py +4 -3
  4. universal_mcp/agents/bigtool/agent.py +1 -0
  5. universal_mcp/agents/bigtool/graph.py +7 -4
  6. universal_mcp/agents/bigtool/tools.py +4 -5
  7. universal_mcp/agents/builder/__main__.py +49 -23
  8. universal_mcp/agents/builder/builder.py +101 -102
  9. universal_mcp/agents/builder/helper.py +4 -6
  10. universal_mcp/agents/builder/prompts.py +92 -39
  11. universal_mcp/agents/builder/state.py +1 -1
  12. universal_mcp/agents/codeact0/__init__.py +2 -1
  13. universal_mcp/agents/codeact0/agent.py +12 -5
  14. universal_mcp/agents/codeact0/langgraph_agent.py +11 -14
  15. universal_mcp/agents/codeact0/llm_tool.py +2 -2
  16. universal_mcp/agents/codeact0/playbook_agent.py +364 -0
  17. universal_mcp/agents/codeact0/prompts.py +113 -39
  18. universal_mcp/agents/codeact0/sandbox.py +43 -32
  19. universal_mcp/agents/codeact0/state.py +29 -3
  20. universal_mcp/agents/codeact0/tools.py +186 -0
  21. universal_mcp/agents/codeact0/utils.py +53 -18
  22. universal_mcp/agents/shared/__main__.py +3 -2
  23. universal_mcp/agents/shared/prompts.py +1 -1
  24. universal_mcp/agents/shared/tool_node.py +17 -12
  25. universal_mcp/agents/utils.py +36 -12
  26. {universal_mcp_agents-0.1.14.dist-info → universal_mcp_agents-0.1.16.dist-info}/METADATA +3 -3
  27. universal_mcp_agents-0.1.16.dist-info/RECORD +50 -0
  28. universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
  29. universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
  30. universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -14
  31. universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
  32. universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
  33. universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
  34. universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
  35. universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
  36. universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
  37. universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
  38. universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
  39. universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
  40. universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
  41. universal_mcp/agents/planner/__init__.py +0 -51
  42. universal_mcp/agents/planner/__main__.py +0 -28
  43. universal_mcp/agents/planner/graph.py +0 -85
  44. universal_mcp/agents/planner/prompts.py +0 -14
  45. universal_mcp/agents/planner/state.py +0 -11
  46. universal_mcp_agents-0.1.14.dist-info/RECORD +0 -66
  47. {universal_mcp_agents-0.1.14.dist-info → universal_mcp_agents-0.1.16.dist-info}/WHEEL +0 -0
@@ -1,54 +1,107 @@
1
- NEW_AGENT_PROMPT = r"""
2
- # ROLE & GOAL
3
- You are a specialized Agent Generation AI. Your primary function is to create a complete, high-quality AI agent profile based on the information provided.
1
+ import json
4
2
 
5
- # INPUTS
6
- 1. **User Task (Optional):** A brief, initial request from the user. This might be vague or specific.
7
- 2. **Conversation History (Optional):** A transcript of a conversation. This is the **primary source of truth**. If the conversation history is provided, it should be prioritized over the User Task to understand the user's full, potentially multi-step, objective.
3
+ from universal_mcp.types import ToolConfig
8
4
 
9
- # INSTRUCTIONS
10
- Analyze the available inputs to fully understand the user's intent. Synthesize this understanding into a complete agent profile according to the specified JSON schema.
11
5
 
12
- - The first line of the `instructions` field in your output MUST be a single, complete sentence that serves as the definitive task for the agent. This sentence should be synthesized from your analysis of the inputs.
13
- - The rest of the `instructions` should provide clear, actionable commands for the agent, covering its role, responsibilities, interaction style, and output formatting.
6
+ def _build_prompt(
7
+ user_task: str | None = None,
8
+ conversation_history: list[dict] | None = None,
9
+ existing_instructions: str | None = None,
10
+ modification_request: str | None = None,
11
+ tool_config: ToolConfig | None = None,
12
+ ) -> str:
13
+ """Dynamically builds a cohesive and effective prompt for the LLM based on the provided inputs."""
14
14
 
15
- # TASK
16
- Based on the following inputs, generate a complete agent profile.
15
+ core_prompt = r"""
16
+ You are a master AI Agent Architect. Your purpose is to design and define highly effective AI agents by interpreting user requests and generating a precise agent profile in JSON format.
17
17
 
18
- **User Task:**
19
- {user_task}
18
+ Your process is systematic and thorough. You will analyze all provided information to construct a complete and coherent agent definition.
19
+ """
20
20
 
21
- **Conversation History:**
22
- {conversation_history}
21
+ analysis_sections = ["\n# I. Analysis of Provided Inputs\n"]
22
+ analysis_sections.append("You are to analyze the following information to understand the user's requirements:\n")
23
23
 
24
- **YOUR JSON OUTPUT:**
25
- """
24
+ if user_task:
25
+ analysis_sections.append(f"## Primary User Task:\n```\n{user_task}\n```\n")
26
+
27
+ if conversation_history:
28
+ analysis_sections.append(
29
+ "## Conversation History:\n"
30
+ "Pay special attention to the messages from the 'human' user. These are direct expressions of their needs and expectations for the agent's behavior. Include the user specific personal information like email-id or anything else which is personal in the agent's instruction.\n"
31
+ f"```json\n{json.dumps(conversation_history, indent=2)}\n```\n"
32
+ )
26
33
 
27
- MODIFY_AGENT_PROMPT = r"""
28
- # ROLE & GOAL
29
- You are an expert Agent Modification AI. Your task is to intelligently update an existing AI agent's profile by integrating a user's modification request.
34
+ if existing_instructions:
35
+ analysis_sections.append(
36
+ "## Existing Agent Instructions:\n"
37
+ "This is the baseline definition for the current agent. Your task will be to modify this based on the user's new requests.\n"
38
+ f"```\n{existing_instructions}\n```\n"
39
+ )
30
40
 
31
- # CORE PRINCIPLES
32
- 1. **Synthesize, Don't Just Add:** Do not simply append the modification request. You must seamlessly integrate the user's feedback into the agent's existing instructions, creating a new, coherent set of commands.
33
- 2. **Holistic Update:** The modification may require changes to not only the instructions but also the agent's name, description, and expertise. You must re-evaluate the entire profile.
34
- 3. **Prioritize New Information:** The user's latest modification request and any new conversation history are the primary drivers for the changes.
41
+ if modification_request:
42
+ analysis_sections.append(
43
+ "## Modification Request:\n"
44
+ "The user wants to change the existing agent. You must incorporate these changes into the new agent definition.\n"
45
+ f"```\n{modification_request}\n```\n"
46
+ )
35
47
 
36
- # INPUTS
37
- 1. **Existing Agent Instructions:** The original instructions that define the agent's current behavior.
38
- 2. **Modification Request:** The user's new input, specifying the desired changes.
39
- 3. **Conversation History (Optional):** A transcript of the conversation that may provide additional context for the modification.
48
+ if tool_config:
49
+ analysis_sections.append(
50
+ "## Tool Configuration:\n"
51
+ "The agent has access to the following tools. The agent's instructions should reflect the appropriate use of these tools.\n"
52
+ f"```json\n{json.dumps(tool_config, indent=2)}\n```\n"
53
+ )
40
54
 
41
- # TASK
42
- Update the agent profile based on the user's feedback.
55
+ framework_prompt = r"""
56
+ # II. Agent Definition Framework
43
57
 
44
- **Existing Agent Instructions:**
45
- {existing_instructions}
58
+ Based on your analysis, you will now define the agent's profile.
46
59
 
47
- **Modification Request:**
48
- {modification_request}
60
+ ## 1. Intent Synthesis
61
+ - **Primary Goal:** In a single sentence, what is the core objective of this agent?
62
+ - **Key Requirements & Constraints:** List any specific requirements, rules, or limitations the agent must adhere to.
63
+
64
+ ## 2. Agent Profile Generation
65
+ You will now construct the complete agent profile.
66
+
67
+ - **Name (2-4 words):** A concise and memorable name that reflects the agent's core function.
68
+ - **Description (1-2 sentences):** A clear and compelling summary of the agent's purpose and value.
69
+ - **Expertise:** A specific, well-defined area of expertise (e.g., "Python Code Generation and Debugging," not "Programming").
70
+ - **Instructions:**
71
+ - This is the most critical part of your output. Write a comprehensive set of system instructions for the agent.
72
+ - The instructions should contain all the necessary details for the agent to call the tools , use the information from the conversation history, and fulfill the user's primary task.
73
+ - The instructions should be written in markdown and be direct, actionable commands.
74
+ - Start with the user's primary task.
75
+ - Clearly define the agent's role and responsibilities.
76
+ - Provide explicit rules for its behavior and interaction style.
77
+ - If tools are provided, explain how and when the agent should use them.
78
+ - Specify the desired output format (e.g., JSON, markdown, plain text).
79
+ - **Schedule:**
80
+ - If the user specifies a schedule, provide a cron expression for when the agent should run.
81
+ - The output for the schedule should only be the cron expression itself (e.g., "0 9 * * *"). Do not add any explanatory text.
82
+ """
83
+
84
+ final_task_prompt = r"""
85
+ # III. Your Task
86
+
87
+ Generate a single JSON object that represents the complete agent profile. The JSON object should have the following structure:
88
+
89
+ {
90
+ "name": "...",
91
+ "description": "...",
92
+ "expertise": "...",
93
+ "instructions": "...",
94
+ "schedule": "..."
95
+ }
96
+
97
+ **YOUR JSON OUTPUT:**
98
+ """
49
99
 
50
- **Conversation History:**
51
- {conversation_history}
100
+ full_prompt = [
101
+ core_prompt,
102
+ "".join(analysis_sections),
103
+ framework_prompt,
104
+ final_task_prompt,
105
+ ]
52
106
 
53
- **YOUR UPDATED JSON OUTPUT:**
54
- """
107
+ return "\n".join(full_prompt)
@@ -21,4 +21,4 @@ class BuilderState(TypedDict):
21
21
  user_task: str | None
22
22
  generated_agent: Agent | None
23
23
  tool_config: ToolConfig | None
24
- messages: Annotated[Sequence[BaseMessage], add_messages]
24
+ messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -1,3 +1,4 @@
1
1
  from .agent import CodeActAgent
2
+ from .playbook_agent import CodeActPlaybookAgent
2
3
 
3
- __all__ = ["CodeActAgent"]
4
+ __all__ = ["CodeActAgent", "CodeActPlaybookAgent"]
@@ -18,9 +18,9 @@ from universal_mcp.agents.codeact0.prompts import (
18
18
  )
19
19
  from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
20
20
  from universal_mcp.agents.codeact0.state import CodeActState
21
- from universal_mcp.agents.utils import filter_retry_on
22
- from universal_mcp.agents.codeact0.utils import inject_context
21
+ from universal_mcp.agents.codeact0.utils import inject_context, smart_truncate
23
22
  from universal_mcp.agents.llm import load_chat_model
23
+ from universal_mcp.agents.utils import filter_retry_on
24
24
 
25
25
 
26
26
  class CodeActAgent(BaseAgent):
@@ -64,9 +64,11 @@ class CodeActAgent(BaseAgent):
64
64
  raise ValueError("Tools are configured but no registry is provided")
65
65
  # Langchain tools are fine
66
66
  exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
67
- additional_tools= [smart_print, data_extractor, ai_classify, call_llm]
67
+ additional_tools = [smart_print, data_extractor, ai_classify, call_llm]
68
68
  additional_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in additional_tools]
69
- self.instructions, self.tools_context = create_default_prompt(exported_tools, additional_tools, self.instructions)
69
+ self.instructions, self.tools_context = create_default_prompt(
70
+ exported_tools, additional_tools, self.instructions
71
+ )
70
72
 
71
73
  def call_model(state: CodeActState) -> Command[Literal["sandbox"]]:
72
74
  messages = [{"role": "system", "content": self.instructions}] + state["messages"]
@@ -114,7 +116,12 @@ class CodeActAgent(BaseAgent):
114
116
  existing_context = state.get("context", {})
115
117
  context = {**existing_context, **add_context}
116
118
  # Execute the script in the sandbox
117
- output, new_context, new_add_context = self.eval_fn(code, context, previous_add_context)
119
+
120
+ output, new_context, new_add_context = self.eval_fn(
121
+ code, context, previous_add_context, 180
122
+ ) # default timeout 3 min
123
+ output = smart_truncate(output)
124
+
118
125
  return Command(
119
126
  goto="call_model",
120
127
  update={
@@ -1,17 +1,14 @@
1
- import asyncio
2
-
3
- from langgraph.checkpoint.memory import MemorySaver
4
- from rich import print
5
1
  from universal_mcp.agentr.registry import AgentrRegistry
6
2
 
7
- from universal_mcp.agents.codeact0.agent import CodeActAgent
8
- from universal_mcp.agents.utils import messages_to_list
3
+ from universal_mcp.agents.codeact0.playbook_agent import CodeActPlaybookAgent
4
+
5
+
9
6
  async def agent():
10
- agent_obj = CodeActAgent(
11
- name="CodeAct Agent",
12
- instructions="Be very concise in your answers.",
13
- model="anthropic:claude-4-sonnet-20250514",
14
- tools={"google_calendar": ["get_upcoming_events"], "exa" : ["search_with_filters"]},
15
- registry=AgentrRegistry()
16
- )
17
- return await agent_obj._build_graph()
7
+ agent_obj = CodeActPlaybookAgent(
8
+ name="CodeAct Agent",
9
+ instructions="Be very concise in your answers.",
10
+ model="anthropic:claude-4-sonnet-20250514",
11
+ tools=[],
12
+ registry=AgentrRegistry(),
13
+ )
14
+ return await agent_obj._build_graph()
@@ -5,7 +5,7 @@ from typing import Any, Literal, cast
5
5
  from langchain.chat_models import init_chat_model
6
6
  from langchain_openai import AzureChatOpenAI
7
7
 
8
- from universal_mcp.agents.codeact0.utils import get_message_text, light_copy
8
+ from universal_mcp.agents.codeact0.utils import get_message_text
9
9
 
10
10
  MAX_RETRIES = 3
11
11
 
@@ -27,7 +27,7 @@ def smart_print(data: Any) -> None:
27
27
  Args:
28
28
  data: Either a dictionary with string keys, or a list of such dictionaries
29
29
  """
30
- print(light_copy(data))
30
+ print(light_copy(data)) # noqa
31
31
 
32
32
 
33
33
  def creative_writer(
@@ -0,0 +1,364 @@
1
+ import inspect
2
+ import json
3
+ import re
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Literal, cast
8
+
9
+ from langchain_core.messages import AIMessage, RemoveMessage, ToolMessage
10
+ from langchain_core.tools import StructuredTool
11
+ from langchain_core.tools import tool as create_tool
12
+ from langgraph.checkpoint.base import BaseCheckpointSaver
13
+ from langgraph.graph import START, StateGraph
14
+ from langgraph.types import Command, RetryPolicy
15
+ from universal_mcp.tools.registry import ToolRegistry
16
+ from universal_mcp.types import ToolFormat, ToolConfig
17
+
18
+ from universal_mcp.agents.base import BaseAgent
19
+ from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
20
+ from universal_mcp.agents.codeact0.prompts import (
21
+ create_default_prompt,
22
+ )
23
+ from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
24
+ from universal_mcp.agents.codeact0.state import CodeActState
25
+ from universal_mcp.agents.codeact0.tools import create_meta_tools, enter_playbook_mode, exit_playbook_mode, get_valid_tools
26
+ from universal_mcp.agents.codeact0.utils import inject_context, smart_truncate
27
+ from universal_mcp.agents.llm import load_chat_model
28
+ from universal_mcp.agents.utils import filter_retry_on, get_message_text, convert_tool_ids_to_dict
29
+
30
+ PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
31
+
32
+ TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function. Do not include the searching and loading of tools. Assume that the tools have already been loaded.
33
+
34
+ Your plan should:
35
+ 1. Identify the key steps in the workflow
36
+ 2. Mark user-specific variables that should become the main playbook function parameters using `variable_name` syntax. Intermediate variables should not be highlighted using ``
37
+ 3. Keep the logic generic and reusable
38
+ 4. Be clear and concise
39
+
40
+ Example:
41
+ ```
42
+ 1. Connect to database using `db_connection_string`
43
+ 2. Query user data for `user_id`
44
+ 3. Process results and calculate `metric_name`
45
+ 4. Send notification to `email_address`
46
+ ```
47
+
48
+ Now create a plan based on the conversation history. Enclose it between ``` and ```. Ask the user if the plan is okay."""
49
+
50
+
51
+
52
+ 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.
53
+ """
54
+
55
+ PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function. Return the function in Python code.
56
+ Do not include any other text in your response.
57
+ The function should be a single, complete piece of code that can be executed independently, based on previously executed code snippets that executed correctly.
58
+ The parameters of the function should be the same as the final confirmed playbook plan.
59
+ Do not include anything other than python code in your response
60
+ """
61
+
62
+
63
+ class CodeActPlaybookAgent(BaseAgent):
64
+ def __init__(
65
+ self,
66
+ name: str,
67
+ instructions: str,
68
+ model: str,
69
+ memory: BaseCheckpointSaver | None = None,
70
+ tools: ToolConfig | None = None,
71
+ registry: ToolRegistry | None = None,
72
+ playbook_registry: object | None = None,
73
+ sandbox_timeout: int = 20,
74
+ **kwargs,
75
+ ):
76
+ super().__init__(
77
+ name=name,
78
+ instructions=instructions,
79
+ model=model,
80
+ memory=memory,
81
+ **kwargs,
82
+ )
83
+ self.model_instance = load_chat_model(model, thinking=True)
84
+ self.tools_config = tools or []
85
+ self.registry = registry
86
+ self.playbook_registry = playbook_registry
87
+ self.eval_fn = eval_unsafe
88
+ self.sandbox_timeout = sandbox_timeout
89
+ self.processed_tools: list[StructuredTool | Callable] = []
90
+
91
+ async def _build_graph(self):
92
+ meta_tools = create_meta_tools(self.registry)
93
+ additional_tools = [smart_print, data_extractor, ai_classify, call_llm, meta_tools["web_search"]]
94
+ self.additional_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in additional_tools]
95
+ async def call_model(state: CodeActState) -> Command[Literal["sandbox", "execute_tools"]]:
96
+ self.exported_tools = []
97
+ if self.tools_config:
98
+ # Convert dict format to list format if needed
99
+ if isinstance(self.tools_config, dict):
100
+ self.tools_config = [
101
+ f"{provider}__{tool}"
102
+ for provider, tools in self.tools_config.items()
103
+ for tool in tools
104
+ ]
105
+ if not self.registry:
106
+ raise ValueError("Tools are configured but no registry is provided")
107
+ # Langchain tools are fine
108
+ self.tools_config.extend(state.get('selected_tool_ids',[]))
109
+ self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
110
+ self.final_instructions, self.tools_context = create_default_prompt(
111
+ self.exported_tools, self.additional_tools, self.instructions
112
+ )
113
+ messages = [{"role": "system", "content": self.final_instructions}] + state["messages"]
114
+
115
+ # Run the model and potentially loop for reflection
116
+ model_with_tools = self.model_instance.bind_tools(
117
+ tools=[
118
+ execute_ipython_cell,
119
+ enter_playbook_mode,
120
+ meta_tools["search_functions"],
121
+ meta_tools["load_functions"],
122
+ ],
123
+ tool_choice="auto",
124
+ )
125
+ response = cast(AIMessage, model_with_tools.invoke(messages))
126
+ if response.tool_calls:
127
+ return Command(goto="execute_tools", update={"messages": [response]})
128
+ else:
129
+ return Command(update={"messages": [response], "model_with_tools": model_with_tools})
130
+
131
+ # if response.tool_calls:
132
+ # if len(response.tool_calls) > 1:
133
+ # raise Exception("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
134
+ # if response.tool_calls[0]["name"] == "enter_playbook_mode":
135
+ # return Command(goto="playbook", update = {"playbook_mode": "planning"})
136
+ # if response.tool_calls[0]["name"] != "execute_ipython_cell":
137
+ # raise Exception(
138
+ # f"Unexpected tool call: {response.tool_calls[0]['name']}. Expected 'execute_ipython_cell'."
139
+ # )
140
+ # if (
141
+ # response.tool_calls[0]["args"].get("snippet") is None
142
+ # or not response.tool_calls[0]["args"]["snippet"].strip()
143
+ # ):
144
+ # raise Exception("Tool call 'execute_ipython_cell' requires a non-empty 'snippet' argument.")
145
+ # return Command(goto="sandbox", update={"messages": [response]})
146
+ # else:
147
+ # return Command(update={"messages": [response]})
148
+
149
+ async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "playbook", "sandbox"]]:
150
+ """Execute tool calls"""
151
+ last_message = state["messages"][-1]
152
+ tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
153
+
154
+ tool_messages = []
155
+ new_tool_ids = []
156
+ ask_user = False
157
+ ai_msg = ""
158
+ tool_result = ""
159
+
160
+ for tool_call in tool_calls:
161
+ try:
162
+ if tool_call["name"] == "enter_playbook_mode":
163
+ tool_message = ToolMessage(
164
+ content=json.dumps("Entered Playbook Mode."),
165
+ name=tool_call["name"],
166
+ tool_call_id=tool_call["id"],
167
+ )
168
+ return Command(
169
+ goto="playbook",
170
+ update={"playbook_mode": "planning", "messages": [tool_message]}, #Entered Playbook mode
171
+ )
172
+ elif tool_call["name"] == "execute_ipython_cell":
173
+ return Command(goto="sandbox")
174
+ elif tool_call["name"] == "load_functions": # Handle load_functions separately
175
+ valid_tools, unconnected_links = await get_valid_tools(
176
+ tool_ids=tool_call["args"]["tool_ids"], registry=self.registry
177
+ )
178
+ new_tool_ids.extend(valid_tools)
179
+ # Create tool message response
180
+ tool_result = f"Successfully loaded {len(valid_tools)} tools: {valid_tools}"
181
+ links = "\n".join(unconnected_links)
182
+ if links:
183
+ ask_user = True
184
+ ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {links} "
185
+ elif tool_call["name"] == "search_functions":
186
+ tool_result = await meta_tools["search_functions"].ainvoke(tool_call["args"])
187
+ except Exception as e:
188
+ tool_result = f"Error during {tool_call}: {e}"
189
+
190
+ tool_message = ToolMessage(
191
+ content=json.dumps(tool_result),
192
+ name=tool_call["name"],
193
+ tool_call_id=tool_call["id"],
194
+ )
195
+ tool_messages.append(tool_message)
196
+
197
+ if new_tool_ids:
198
+ self.tools_config.extend(new_tool_ids)
199
+ self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
200
+ self.final_instructions, self.tools_context = create_default_prompt(
201
+ self.exported_tools, self.additional_tools, self.instructions
202
+ )
203
+
204
+ if ask_user:
205
+ tool_messages.append(AIMessage(content=ai_msg))
206
+ return Command(update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
207
+
208
+ return Command(goto="call_model", update={"messages": tool_messages, "selected_tool_ids": new_tool_ids})
209
+
210
+ # If eval_fn is a async, we define async node function.
211
+ if inspect.iscoroutinefunction(self.eval_fn):
212
+ raise ValueError("eval_fn must be a synchronous function, not a coroutine.")
213
+ # async def sandbox(state: StateSchema):
214
+ # existing_context = state.get("context", {})
215
+ # context = {**existing_context, **tools_context}
216
+ # # Execute the script in the sandbox
217
+ # output, new_vars = await eval_fn(state["script"], context)
218
+ # new_context = {**existing_context, **new_vars}
219
+ # return {
220
+ # "messages": [{"role": "user", "content": output}],
221
+ # "context": new_context,
222
+ # }
223
+ else:
224
+
225
+ def sandbox(state: CodeActState) -> Command[Literal["call_model"]]:
226
+ tool_call = state["messages"][-1].tool_calls[0] # type: ignore
227
+ code = tool_call["args"]["snippet"]
228
+ previous_add_context = state.get("add_context", {})
229
+ add_context = inject_context(previous_add_context, self.tools_context)
230
+ existing_context = state.get("context", {})
231
+ context = {**existing_context, **add_context}
232
+ # Execute the script in the sandbox
233
+
234
+ output, new_context, new_add_context = self.eval_fn(
235
+ code, context, previous_add_context, 180
236
+ ) # default timeout 3 min
237
+ output = smart_truncate(output)
238
+
239
+ return Command(
240
+ goto="call_model",
241
+ update={
242
+ "messages": [
243
+ ToolMessage(
244
+ content=output,
245
+ name=tool_call["name"],
246
+ tool_call_id=tool_call["id"],
247
+ )
248
+ ],
249
+ "context": new_context,
250
+ "add_context": new_add_context,
251
+ },
252
+ )
253
+
254
+ def playbook(state: CodeActState) -> Command[Literal["call_model"]]:
255
+ playbook_mode = state.get("playbook_mode")
256
+ if playbook_mode == "planning":
257
+ planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
258
+ messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
259
+
260
+ response = self.model_instance.invoke(messages)
261
+ response = cast(AIMessage, response)
262
+ response_text = get_message_text(response)
263
+ # Extract plan from response text between triple backticks
264
+ plan_match = re.search(r'```(.*?)```', response_text, re.DOTALL)
265
+ if plan_match:
266
+ plan = plan_match.group(1).strip()
267
+ else:
268
+ plan = response_text.strip()
269
+ return Command(update={"messages": [response], "playbook_mode": "confirming", "plan": plan})
270
+
271
+
272
+ elif playbook_mode == "confirming":
273
+ confirmation_instructions = self.instructions + PLAYBOOK_CONFIRMING_PROMPT
274
+ messages = [{"role": "system", "content": confirmation_instructions}] + state["messages"]
275
+ response = self.model_instance.invoke(messages, stream=False)
276
+ response = get_message_text(response)
277
+ if "true" in response.lower():
278
+ return Command(goto="playbook", update={"playbook_mode": "generating"})
279
+ else:
280
+ return Command(goto="playbook", update={"playbook_mode": "planning"})
281
+
282
+
283
+
284
+ elif playbook_mode == "generating":
285
+ generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
286
+ messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
287
+ response = cast(AIMessage, self.model_instance.invoke(messages))
288
+ raw_content = get_message_text(response)
289
+ func_code = raw_content.strip()
290
+ func_code = func_code.replace("```python", "").replace("```", "")
291
+ func_code = func_code.strip()
292
+
293
+ # Extract function name (handle both regular and async functions)
294
+ match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
295
+ if match:
296
+ function_name = match.group(1)
297
+ else:
298
+ function_name = "generated_playbook"
299
+
300
+ # Save or update an Agent using the helper registry
301
+ saved_note = ""
302
+ try:
303
+ if not self.playbook_registry:
304
+ raise ValueError("Playbook registry is not configured")
305
+
306
+ # Build instructions payload embedding the plan and function code
307
+ instructions_payload = {
308
+ "playbookPlan": state["plan"],
309
+ "playbookScript": {
310
+ "name": function_name,
311
+ "code": func_code,
312
+ },
313
+ }
314
+
315
+ # Convert tool ids list to dict
316
+ tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
317
+
318
+ res = self.playbook_registry.create_agent(
319
+ name=function_name,
320
+ description=f"Generated playbook: {function_name}",
321
+ instructions=instructions_payload,
322
+ tools=tool_dict,
323
+ visibility="private",
324
+ )
325
+ saved_note = f"Successfully created your playbook! Check it out here: [View Playbook](https://wingmen.info/agents/{res.id})"
326
+ except Exception as e:
327
+ saved_note = f"Failed to save generated playbook as Agent '{function_name}': {e}"
328
+
329
+ # Mock tool call for exit_playbook_mode (for testing/demonstration)
330
+ mock_exit_tool_call = {
331
+ "name": "exit_playbook_mode",
332
+ "args": {},
333
+ "id": "mock_exit_playbook_123"
334
+ }
335
+ mock_assistant_message = AIMessage(
336
+ content=saved_note,
337
+ tool_calls=[mock_exit_tool_call]
338
+ )
339
+
340
+
341
+ # Mock tool response for exit_playbook_mode
342
+ mock_exit_tool_response = ToolMessage(
343
+ content=json.dumps(f"Exited Playbook Mode.{saved_note}"),
344
+ name="exit_playbook_mode",
345
+ tool_call_id="mock_exit_playbook_123"
346
+ )
347
+
348
+ return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "playbook_mode": "normal"})
349
+
350
+ def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
351
+ """Route to either normal mode or playbook creation"""
352
+ if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
353
+ return "playbook"
354
+
355
+ return "call_model"
356
+
357
+ agent = StateGraph(state_schema=CodeActState)
358
+ agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
359
+ agent.add_node(sandbox)
360
+ agent.add_node(playbook)
361
+ agent.add_node(execute_tools)
362
+ agent.add_conditional_edges(START, route_entry)
363
+ # agent.add_edge(START, "call_model")
364
+ return agent.compile(checkpointer=self.memory)