universal-mcp-agents 0.1.20rc1__py3-none-any.whl → 0.1.21__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.

@@ -56,40 +56,41 @@ class BaseAgent:
56
56
  "run_name": self.name,
57
57
  }
58
58
 
59
+ last_ai_chunk = None
59
60
  async for event, meta in self._graph.astream(
60
61
  {"messages": [{"role": "user", "content": user_input}]},
61
62
  config=run_config,
62
63
  context={"system_prompt": self.instructions, "model": self.model},
63
- stream_mode="messages",
64
+ stream_mode=["messages", "custom"],
64
65
  stream_usage=True,
65
66
  ):
66
- # Only forward assistant token chunks that are not tool-related.
67
- type_ = type(event)
68
- tags = meta.get("tags", []) if isinstance(meta, dict) else []
69
- is_quiet = isinstance(tags, list) and ("quiet" in tags)
70
- if is_quiet:
71
- continue
72
- # Handle different types of messages
73
- if type_ == AIMessageChunk:
74
- # Accumulate billing and aggregate message
75
- aggregate = event if aggregate is None else aggregate + event
76
- # Ignore intermeddite finish messages
77
- if "finish_reason" in event.response_metadata:
78
- # Got LLM finish reason ignore it
79
- logger.debug(
80
- f"Finish event: {event}, reason: {event.response_metadata['finish_reason']}, Metadata: {meta}"
81
- )
82
- pass
83
- else:
84
- logger.debug(f"Event: {event}, Metadata: {meta}")
85
- yield event
86
- # Send a final finished message
87
- # The last event would be finish
88
- event = cast(AIMessageChunk, event)
89
- event.usage_metadata = aggregate.usage_metadata
90
- logger.debug(f"Usage metadata: {event.usage_metadata}")
91
- event.content = "" # Clear the message since it would have already been streamed above
92
- yield event
67
+ if event == "messages" and isinstance(meta, (tuple, list)) and len(meta) == 2:
68
+ payload, meta_dict = meta
69
+ is_playbook = isinstance(meta_dict, dict) and meta_dict.get("langgraph_node") == "playbook"
70
+ additional_kwargs = getattr(payload, "additional_kwargs", {}) or {}
71
+ if is_playbook and not additional_kwargs.get("stream"):
72
+ continue
73
+ if isinstance(payload, AIMessageChunk):
74
+ last_ai_chunk = payload
75
+ aggregate = payload if aggregate is None else aggregate + payload
76
+ if "finish_reason" in payload.response_metadata:
77
+ logger.debug(
78
+ f"Finish event: {payload}, reason: {payload.response_metadata['finish_reason']}, Metadata: {meta_dict}"
79
+ )
80
+ pass
81
+ logger.debug(f"Event: {payload}, Metadata: {meta_dict}")
82
+ yield payload
83
+
84
+ if event == "custom":
85
+ yield meta
86
+
87
+ # Send a final finished message if we saw any AI chunks (to carry usage)
88
+ if last_ai_chunk is not None and aggregate is not None:
89
+ event = cast(AIMessageChunk, last_ai_chunk)
90
+ event.usage_metadata = aggregate.usage_metadata
91
+ logger.debug(f"Usage metadata: {event.usage_metadata}")
92
+ event.content = "" # Clear the message since it would have already been streamed above
93
+ yield event
93
94
 
94
95
  async def stream_interactive(self, thread_id: str, user_input: str):
95
96
  await self.ainit()
@@ -1,31 +1,31 @@
1
1
  import json
2
2
  import re
3
- from collections.abc import Callable
4
3
  from typing import Literal, cast
4
+ import uuid
5
5
 
6
6
  from langchain_core.messages import AIMessage, ToolMessage
7
7
  from langchain_core.tools import StructuredTool
8
8
  from langgraph.checkpoint.base import BaseCheckpointSaver
9
9
  from langgraph.graph import START, StateGraph
10
- from langgraph.types import Command, RetryPolicy
10
+ from langgraph.types import Command, RetryPolicy, StreamWriter
11
11
  from universal_mcp.tools.registry import ToolRegistry
12
12
  from universal_mcp.types import ToolConfig, ToolFormat
13
13
 
14
14
  from universal_mcp.agents.base import BaseAgent
15
- from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
15
+ from universal_mcp.agents.codeact0.llm_tool import smart_print
16
16
  from universal_mcp.agents.codeact0.prompts import (
17
- PLAYBOOK_CONFIRMING_PROMPT,
18
17
  PLAYBOOK_GENERATING_PROMPT,
19
18
  PLAYBOOK_PLANNING_PROMPT,
20
19
  create_default_prompt,
21
20
  )
22
21
  from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
23
- from universal_mcp.agents.codeact0.state import CodeActState
22
+ from universal_mcp.agents.codeact0.state import CodeActState, PlaybookCode, PlaybookPlan
24
23
  from universal_mcp.agents.codeact0.tools import (
25
24
  create_meta_tools,
26
25
  enter_playbook_mode,
27
26
  get_valid_tools,
28
27
  )
28
+ from universal_mcp.agents.codeact0.utils import add_tools
29
29
  from universal_mcp.agents.llm import load_chat_model
30
30
  from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
31
31
 
@@ -51,16 +51,22 @@ class CodeActPlaybookAgent(BaseAgent):
51
51
  **kwargs,
52
52
  )
53
53
  self.model_instance = load_chat_model(model)
54
- self.tools_config = tools or []
54
+ self.playbook_model_instance = load_chat_model("azure/gpt-4.1")
55
+ self.tools_config = tools or {}
55
56
  self.registry = registry
56
57
  self.playbook_registry = playbook_registry
58
+ self.playbook = playbook_registry.get_agent() if playbook_registry else None
57
59
  self.eval_fn = eval_unsafe
58
60
  self.sandbox_timeout = sandbox_timeout
59
- self.processed_tools: list[StructuredTool | Callable] = []
61
+ self.default_tools = {
62
+ "llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
63
+ "markitdown": ["convert_to_markdown"],
64
+ }
65
+ add_tools(self.tools_config, self.default_tools)
60
66
 
61
67
  async def _build_graph(self):
62
68
  meta_tools = create_meta_tools(self.registry)
63
- additional_tools = [smart_print, data_extractor, ai_classify, call_llm, meta_tools["web_search"]]
69
+ additional_tools = [smart_print, meta_tools["web_search"]]
64
70
  self.additional_tools = [
65
71
  t if isinstance(t, StructuredTool) else StructuredTool.from_function(t) for t in additional_tools
66
72
  ]
@@ -161,7 +167,7 @@ class CodeActPlaybookAgent(BaseAgent):
161
167
  self.tools_config.extend(new_tool_ids)
162
168
  self.exported_tools = await self.registry.export_tools(new_tool_ids, ToolFormat.LANGCHAIN)
163
169
  self.final_instructions, self.tools_context = create_default_prompt(
164
- self.exported_tools, self.additional_tools, self.instructions
170
+ self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
165
171
  )
166
172
  if ask_user:
167
173
  tool_messages.append(AIMessage(content=ai_msg))
@@ -184,41 +190,64 @@ class CodeActPlaybookAgent(BaseAgent):
184
190
  },
185
191
  )
186
192
 
187
- def playbook(state: CodeActState) -> Command[Literal["call_model"]]:
193
+ def playbook(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
188
194
  playbook_mode = state.get("playbook_mode")
189
195
  if playbook_mode == "planning":
196
+ plan_id = str(uuid.uuid4())
197
+ writer({
198
+ "type": "custom",
199
+ id: plan_id,
200
+ "name": "planning",
201
+ "data": {"update": bool(self.playbook)}
202
+ })
190
203
  planning_instructions = self.instructions + PLAYBOOK_PLANNING_PROMPT
191
204
  messages = [{"role": "system", "content": planning_instructions}] + state["messages"]
192
205
 
193
- response = self.model_instance.invoke(messages)
194
- response = cast(AIMessage, response)
195
- response_text = get_message_text(response)
196
- # Extract plan from response text between triple backticks
197
- plan_match = re.search(r"```(.*?)```", response_text, re.DOTALL)
198
- if plan_match:
199
- plan = plan_match.group(1).strip()
200
- else:
201
- plan = response_text.strip()
202
- return Command(update={"messages": [response], "playbook_mode": "confirming", "plan": plan})
206
+ model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookPlan)
207
+ response = model_with_structured_output.invoke(messages)
208
+ plan = cast(PlaybookPlan, response)
209
+
210
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
211
+ return Command(update={"messages": [AIMessage(content=json.dumps(plan.dict()), additional_kwargs={"type": "planning", "plan": plan.steps, "update": bool(self.playbook)})], "playbook_mode": "confirming", "plan": plan.steps})
203
212
 
204
213
  elif playbook_mode == "confirming":
205
- confirmation_instructions = self.instructions + PLAYBOOK_CONFIRMING_PROMPT
206
- messages = [{"role": "system", "content": confirmation_instructions}] + state["messages"]
207
- response = self.model_instance.invoke(messages, stream=False)
208
- response = get_message_text(response)
209
- if "true" in response.lower():
214
+ # Deterministic routing based on three exact button inputs from UI
215
+ user_text = ""
216
+ for m in reversed(state["messages"]):
217
+ try:
218
+ if getattr(m, "type", "") in {"human", "user"}:
219
+ user_text = (get_message_text(m) or "").strip()
220
+ if user_text:
221
+ break
222
+ except Exception:
223
+ continue
224
+
225
+ t = user_text.lower()
226
+ if t == "yes, this is great":
210
227
  return Command(goto="playbook", update={"playbook_mode": "generating"})
211
- else:
212
- return Command(goto="playbook", update={"playbook_mode": "planning"})
228
+ if t == "i would like to modify the plan":
229
+ prompt_ai = AIMessage(content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.", additional_kwargs={"stream": "true"})
230
+ return Command(update={"playbook_mode": "planning", "messages": [prompt_ai]})
231
+ if t == "let's do something else":
232
+ return Command(goto="call_model", update={"playbook_mode": "inactive"})
233
+
234
+ # Fallback safe default
235
+ return Command(goto="call_model", update={"playbook_mode": "inactive"})
213
236
 
214
237
  elif playbook_mode == "generating":
238
+ generate_id = str(uuid.uuid4())
239
+ writer({
240
+ "type": "custom",
241
+ id: generate_id,
242
+ "name": "generating",
243
+ "data": {"update": bool(self.playbook)}
244
+ })
215
245
  generating_instructions = self.instructions + PLAYBOOK_GENERATING_PROMPT
216
246
  messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
217
- response = cast(AIMessage, self.model_instance.invoke(messages))
218
- raw_content = get_message_text(response)
219
- func_code = raw_content.strip()
220
- func_code = func_code.replace("```python", "").replace("```", "")
221
- func_code = func_code.strip()
247
+
248
+ model_with_structured_output = self.playbook_model_instance.with_structured_output(PlaybookCode)
249
+ response = model_with_structured_output.invoke(messages)
250
+ func_code = cast(PlaybookCode, response).code
222
251
 
223
252
  # Extract function name (handle both regular and async functions)
224
253
  match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
@@ -228,47 +257,39 @@ class CodeActPlaybookAgent(BaseAgent):
228
257
  function_name = "generated_playbook"
229
258
 
230
259
  # Save or update an Agent using the helper registry
231
- saved_note = ""
232
260
  try:
233
- if not self.playbook_registry:
261
+ if not self.playbook_registry:
234
262
  raise ValueError("Playbook registry is not configured")
235
263
 
236
264
  # Build instructions payload embedding the plan and function code
237
265
  instructions_payload = {
238
266
  "playbookPlan": state["plan"],
239
- "playbookScript": {
240
- "name": function_name,
241
- "code": func_code,
242
- },
267
+ "playbookScript": func_code,
243
268
  }
244
269
 
245
270
  # Convert tool ids list to dict
246
271
  tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
247
272
 
248
- res = self.playbook_registry.create_agent(
273
+ res = self.playbook_registry.upsert_agent(
249
274
  name=function_name,
250
275
  description=f"Generated playbook: {function_name}",
251
276
  instructions=instructions_payload,
252
277
  tools=tool_dict,
253
278
  visibility="private",
254
279
  )
255
- saved_note = f"Successfully created your playbook! Check it out here: [View Playbook](https://wingmen.info/agents/{res.id})"
256
280
  except Exception as e:
257
- saved_note = f"Failed to save generated playbook as Agent '{function_name}': {e}"
258
-
259
- # Mock tool call for exit_playbook_mode (for testing/demonstration)
260
- mock_exit_tool_call = {"name": "exit_playbook_mode", "args": {}, "id": "mock_exit_playbook_123"}
261
- mock_assistant_message = AIMessage(content=saved_note, tool_calls=[mock_exit_tool_call])
281
+ raise e
262
282
 
263
- # Mock tool response for exit_playbook_mode
264
- mock_exit_tool_response = ToolMessage(
265
- content=json.dumps(f"Exited Playbook Mode.{saved_note}"),
266
- name="exit_playbook_mode",
267
- tool_call_id="mock_exit_playbook_123",
268
- )
283
+ writer({
284
+ "type": "custom",
285
+ id: generate_id,
286
+ "name": "generating",
287
+ "data": {"id": str(res.id), "update": bool(self.playbook)}
288
+ })
289
+ mock_assistant_message = AIMessage(content=json.dumps(response.dict()), additional_kwargs={"type": "generating", "id": str(res.id), "update": bool(self.playbook)})
269
290
 
270
291
  return Command(
271
- update={"messages": [mock_assistant_message, mock_exit_tool_response], "playbook_mode": "normal"}
292
+ update={"messages": [mock_assistant_message], "playbook_mode": "normal"}
272
293
  )
273
294
 
274
295
  async def route_entry(state: CodeActState) -> Literal["call_model", "playbook"]:
@@ -277,7 +298,7 @@ class CodeActPlaybookAgent(BaseAgent):
277
298
  self.tools_config.extend(state.get("selected_tool_ids", []))
278
299
  self.exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
279
300
  self.final_instructions, self.tools_context = create_default_prompt(
280
- self.exported_tools, self.additional_tools, self.instructions
301
+ self.exported_tools, self.additional_tools, self.instructions, playbook=self.playbook
281
302
  )
282
303
  if state.get("playbook_mode") in ["planning", "confirming", "generating"]:
283
304
  return "playbook"
@@ -1,11 +1,6 @@
1
- import json
2
- from dataclasses import dataclass
3
- from typing import Any, Literal, cast
1
+ from typing import Any
4
2
 
5
- from langchain.chat_models import init_chat_model
6
- from langchain_openai import AzureChatOpenAI
7
-
8
- from universal_mcp.agents.codeact0.utils import get_message_text, light_copy
3
+ from universal_mcp.agents.codeact0.utils import light_copy
9
4
 
10
5
  MAX_RETRIES = 3
11
6
 
@@ -28,250 +23,3 @@ def smart_print(data: Any) -> None:
28
23
  data: Either a dictionary with string keys, or a list of such dictionaries
29
24
  """
30
25
  print(light_copy(data)) # noqa: T201
31
-
32
-
33
- def creative_writer(
34
- task: str,
35
- context: Any | list[Any] | dict[str, Any],
36
- tone: str = "normal",
37
- format: Literal["markdown", "html", "plain"] = "markdown",
38
- length: Literal["very-short", "concise", "normal", "long"] = "concise",
39
- ) -> str:
40
- """
41
- Given a high-level writing task and context, returns a well-written text
42
- that achieves the task, given the context.
43
-
44
- Example Call:
45
- creative_writer("Summarize this website with the goal of making it easy to understand.", web_content)
46
- creative_writer("Make a markdown table summarizing the key differences between doc_1 and doc_2.", {"doc_1": str(doc_1), "doc_2": str(doc_2)})
47
- creative_writer("Summarize all the provided documents.", [doc_1, doc_2, doc_3])
48
-
49
- Important:
50
- - Include specifics of the goal in the context verbatim.
51
- - Be precise and direct in the task, and include as much context as possible.
52
- - Include relevant high-level goals or intent in the task.
53
- - You can provide multiple documents as input, and reference them in the task.
54
- - You MUST provide the contents of any source documents to `creative_writer`.
55
- - NEVER use `creative_writer` to produce JSON for a Pydantic model.
56
-
57
- Args:
58
- task: The main writing task or directive.
59
- context: A single string, list of strings, or dict mapping labels to content.
60
- tone: The desired tone of the output (e.g., "normal", "flirty", "formal", "casual", "crisp", "poetic", "technical", "internet-chat", "smartass", etc.).
61
- format: Output format ('markdown', 'html', 'plain-text').
62
- length: Desired length of the output ('very-short', 'concise', 'normal', 'long').
63
-
64
- Returns:
65
- str: The generated text output.
66
- """
67
-
68
- context = get_context_str(context)
69
-
70
- task = task.strip() + "\n\n"
71
- if format == "markdown":
72
- task += "Please write in Markdown format.\n\n"
73
- elif format == "html":
74
- task += "Please write in HTML format.\n\n"
75
- else:
76
- task += "Please write in plain text format. Don't use markdown or HTML.\n\n"
77
-
78
- if tone not in ["normal", "default", ""]:
79
- task = f"{task} (Tone instructions: {tone})"
80
-
81
- if length not in ["normal", "default", ""]:
82
- task = f"{task} (Length instructions: {length})"
83
-
84
- prompt = f"{task}\n\nContext:\n{context}\n\n"
85
-
86
- model = AzureChatOpenAI(model="gpt-4o", temperature=0.7)
87
-
88
- response = model.with_retry(stop_after_attempt=MAX_RETRIES).invoke(prompt)
89
- return get_message_text(response)
90
-
91
-
92
- def ai_classify(
93
- classification_task_and_requirements: str,
94
- context: Any | list[Any] | dict[str, Any],
95
- class_descriptions: dict[str, str],
96
- ) -> dict[str, Any]:
97
- """
98
- Classifies and compares data based on given requirements.
99
-
100
- Use `ai_classify` for tasks which need to classify data into one of many categories.
101
- If making multiple binary classifications, call `ai_classify` for each.
102
-
103
- Guidance:
104
- - Prefer to use ai_classify operations to compare strings, rather than string ops.
105
- - Prefer to include an "Unsure" category for classification tasks.
106
- - The `class_descriptions` dict argument MUST be a map from possible class names to a precise description.
107
- - Use precise and specific class names and concise descriptions.
108
- - Pass ALL relevant context, preferably as a dict mapping labels to content.
109
- - Returned dict maps each possible class name to a probability.
110
-
111
- Example Usage:
112
- classification_task_and_requirements = "Does the document contain an address?"
113
- class_descriptions = {
114
- "Is_Address": "Valid addresses usually have street names, city, and zip codes.",
115
- "Not_Address": "Not valid addresses."
116
- }
117
- classification = ai_classify(
118
- classification_task_and_requirements,
119
- {"address": extracted_address},
120
- class_descriptions
121
- )
122
- if classification["probabilities"]["Is_Address"] > 0.5:
123
- ...
124
-
125
- Args:
126
- classification_task_and_requirements: The classification question and rules.
127
- context: The data to classify (string, list, or dict).
128
- class_descriptions: Mapping from class names to descriptions.
129
-
130
- Returns:
131
- dict: {
132
- probabilities: dict[str, float],
133
- reason: str,
134
- top_class: str,
135
- }
136
- """
137
-
138
- context = get_context_str(context)
139
-
140
- prompt = (
141
- f"{classification_task_and_requirements}\n\n"
142
- f"\nThis is classification task\nPossible classes and descriptions:\n"
143
- f"{json.dumps(class_descriptions, indent=2)}\n"
144
- f"\nContext:\n{context}\n\n"
145
- "Return ONLY a valid JSON object, no extra text."
146
- )
147
-
148
- model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
149
-
150
- @dataclass
151
- class ClassificationResult:
152
- probabilities: dict[str, float]
153
- reason: str
154
- top_class: str
155
-
156
- response = (
157
- model.with_structured_output(schema=ClassificationResult, method="json_mode")
158
- .with_retry(stop_after_attempt=MAX_RETRIES)
159
- .invoke(prompt)
160
- )
161
- return cast(dict[str, Any], response)
162
-
163
-
164
- def call_llm(
165
- task_instructions: str, context: Any | list[Any] | dict[str, Any], output_json_schema: dict[str, Any]
166
- ) -> dict[str, Any]:
167
- """
168
- Call a Large Language Model (LLM) with an instruction and contextual information,
169
- returning a dictionary matching the given output_json_schema.
170
- Can be used for tasks like creative writing, llm reasoning based content generation, etc.
171
-
172
- You MUST anticipate Exceptions in reasoning based tasks which will lead to some empty fields
173
- in the returned output; skip this item if applicable.
174
-
175
- General Guidelines:
176
- - Be comprehensive, specific, and precise on the task instructions.
177
- - Include as much context as possible.
178
- - You can provide multiple items in context, and reference them in the task.
179
- - Include relevant high-level goals or intent in the task.
180
- - In the output_json_schema, use required field wherever necessary.
181
- - The more specific your task instructions and output_json_schema are, the better the results.
182
-
183
- Guidelines for content generation tasks:
184
- - Feel free to add instructions for tone, length, and format (markdown, html, plain-text, xml)
185
- - Some examples of tone are: "normal", "flirty", "formal", "casual", "crisp", "poetic", "technical", "internet-chat", "smartass", etc.
186
- - Prefer length to be concise by default. Other examples are: "very-short", "concise", "normal", "long", "2-3 lines", etc.
187
- - In format prefer plain-text but you can also use markdown and html wherever useful.
188
-
189
- Args:
190
- instruction: The main directive for the LLM (e.g., "Summarize the article" or "Extract key entities").
191
- context:
192
- A dictionary containing named text elements that provide additional
193
- information for the LLM. Keys are labels (e.g., 'article', 'transcript'),
194
- values are strings of content.
195
- output_json_schema: must be a valid JSON schema with top-level 'title' and 'description' keys.
196
-
197
- Returns:
198
- dict: Parsed JSON object matching the desired output_json_schema.
199
-
200
- """
201
- context = get_context_str(context)
202
-
203
- prompt = f"{task_instructions}\n\nContext:\n{context}\n\nReturn ONLY a valid JSON object, no extra text."
204
-
205
- model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
206
-
207
- response = (
208
- model.with_structured_output(schema=output_json_schema, method="json_mode")
209
- .with_retry(stop_after_attempt=MAX_RETRIES)
210
- .invoke(prompt)
211
- )
212
- return cast(dict[str, Any], response)
213
-
214
-
215
- def data_extractor(
216
- extraction_task: str, source: Any | list[Any] | dict[str, Any], output_json_schema: dict[str, Any]
217
- ) -> dict[str, Any]:
218
- """
219
- Extracts structured data from unstructured data (documents, webpages, images, large bodies of text),
220
- returning a dictionary matching the given output_json_schema.
221
-
222
- You MUST anticipate Exception raised for unextractable data; skip this item if applicable.
223
-
224
- Strongly prefer to:
225
- - Be comprehensive, specific, and precise on the data you want to extract.
226
- - Use optional fields everywhere.
227
- - Extract multiple items from each source unless otherwise specified.
228
- - The more specific your extraction task and output_json_schema are, the better the results.
229
-
230
- Args:
231
- extraction_task: The directive describing what to extract.
232
- source: The unstructured data to extract from.
233
- output_json_schema: must be a valid JSON schema with top-level 'title' and 'description' keys.
234
-
235
- Returns:
236
- dict: Parsed JSON object matching the desired output_json_schema.
237
-
238
- Example:
239
- news_articles_schema = {
240
- "title": "NewsArticleList",
241
- "description": "A list of news articles with headlines and URLs",
242
- "type": "object",
243
- "properties": {
244
- "articles": {
245
- "type": "array",
246
- "items": {
247
- "type": "object",
248
- "properties": {
249
- "headline": {
250
- "type": "string"
251
- },
252
- "url": {
253
- "type": "string"
254
- }
255
- },
256
- "required": ["headline", "url"]
257
- }
258
- }
259
- },
260
- "required": ["articles"]
261
- }
262
-
263
- news_articles = data_extractor("Extract headlines and their corresponding URLs.", content, news_articles_schema)
264
- """
265
-
266
- context = get_context_str(source)
267
-
268
- prompt = f"{extraction_task}\n\nContext:\n{context}\n\nReturn ONLY a valid JSON object, no extra text."
269
-
270
- model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
271
-
272
- response = (
273
- model.with_structured_output(schema=output_json_schema, method="json_mode")
274
- .with_retry(stop_after_attempt=MAX_RETRIES)
275
- .invoke(prompt)
276
- )
277
- return cast(dict[str, Any], response)
@@ -4,10 +4,8 @@ from collections.abc import Sequence
4
4
 
5
5
  from langchain_core.tools import StructuredTool
6
6
 
7
- from universal_mcp.agents.codeact0.utils import schema_to_signature
8
-
9
7
  uneditable_prompt = """
10
- You are **Wingmen**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
8
+ You are **Ruzo**, an AI Assistant created by AgentR — a creative, straight-forward, and direct principal software engineer with access to tools.
11
9
 
12
10
  Your job is to answer the user's question or perform the task they ask for.
13
11
  - Answer simple questions (which do not require you to write any code or access any external resources) directly. Note that any operation that involves using ONLY print functions should be answered directly in the chat. NEVER write a string yourself and print it.
@@ -21,7 +19,13 @@ Your job is to answer the user's question or perform the task they ask for.
21
19
  - Read and understand the output of the previous code snippet and use it to answer the user's request. Note that the code output is NOT visible to the user, so after the task is complete, you have to give the output to the user in a markdown format. Similarly, you should only use print/smart_print for your own analysis, the user does not get the output.
22
20
  - If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
23
21
 
24
- GUIDELINES for writing code:
22
+ **Code Execution Guidelines:**
23
+ - The code you write will be executed in a sandbox environment, and you can use the output of previous executions in your code. Variables, functions, imports are retained.
24
+ - Read and understand the output of the previous code snippet and use it to answer the user's request. Note that the code output is NOT visible to the user, so after the task is complete, you have to give the output to the user in a markdown format. Similarly, you should only use print/smart_print for your own analysis, the user does not get the output.
25
+ - If needed, feel free to ask for more information from the user (without using the `execute_ipython_cell` tool) to clarify the task.
26
+ - Always describe in 2-3 lines about the current progress. In each step, mention what has been achieved and what you are planning to do next.
27
+
28
+ **Coding Best Practices:**
25
29
  - Variables defined at the top level of previous code snippets can be referenced in your code.
26
30
  - External functions which return a dict or list[dict] are ambiguous. Therefore, you MUST explore the structure of the returned data using `smart_print()` statements before using it, printing keys and values. `smart_print` truncates long strings from data, preventing huge output logs.
27
31
  - When an operation involves running a fixed set of steps on a list of items, run one run correctly and then use a for loop to run the steps on each item in the list.
@@ -29,25 +33,40 @@ GUIDELINES for writing code:
29
33
  - You can only import libraries that come pre-installed with Python. However, do consider searching for external functions first, using the search and load tools to access them in the code.
30
34
  - For displaying final results to the user, you must present your output in markdown format, including image links, so that they are rendered and displayed to the user. The code output is NOT visible to the user.
31
35
  - Call all functions using keyword arguments only, never positional arguments.
32
- - Async Functions (Critical): Use them only as follows-
33
- Case 1: Top-level await without asyncio.run()
36
+
37
+ **Async Functions (Critical Rules):**
38
+ Use async functions only as follows:
39
+ - Case 1: Top-level await without asyncio.run()
34
40
  Wrap in async function and call with asyncio.run():
41
+ ```python
35
42
  async def main():
36
43
  result = await some_async_function()
37
44
  return result
38
45
  asyncio.run(main())
39
- Case 2: Using asyncio.run() directly
40
- If code already contains asyncio.run(), use as-is — do not wrap again:
46
+ ```
47
+ - Case 2: Using asyncio.run() directly
48
+ If code already contains asyncio.run(), use as-is — do not wrap again:
49
+ ```python
41
50
  asyncio.run(some_async_function())
51
+ ```
42
52
  Rules:
43
53
  - Never use await outside an async function
44
54
  - Never use await asyncio.run()
45
55
  - Never nest asyncio.run() calls
56
+
57
+ **Final Output Requirements:**
58
+ - Once you have all the information about the task, return the text directly to user in markdown format. No need to call `execute_ipython_cell` again.
59
+ - Always respond in github flavoured markdown format.
60
+ - For charts and diagrams, use mermaid chart in markdown directly.
61
+ - Your final response should contain the complete answer to the user's request in a clear, well-formatted manner that directly addresses what they asked for.
46
62
  """
47
63
 
48
64
  PLAYBOOK_PLANNING_PROMPT = """Now, you are tasked with creating a reusable playbook from the user's previous workflow.
49
65
 
50
- TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function. Do not include the searching and loading of tools. Assume that the tools have already been loaded.
66
+ TASK: Analyze the conversation history and code execution to create a step-by-step plan for a reusable function.
67
+ Do not include the searching and loading of tools. Assume that the tools have already been loaded.
68
+ The plan is a sequence of steps.
69
+ You must output a JSON object with a single key "steps", which is a list of strings. Each string is a step in the playbook.
51
70
 
52
71
  Your plan should:
53
72
  1. Identify the key steps in the workflow
@@ -56,24 +75,25 @@ Your plan should:
56
75
  4. Be clear and concise
57
76
 
58
77
  Example:
59
- ```
60
- 1. Connect to database using `db_connection_string`
61
- 2. Query user data for `user_id`
62
- 3. Process results and calculate `metric_name`
63
- 4. Send notification to `email_address`
64
- ```
65
-
66
- Now create a plan based on the conversation history. Enclose it between ``` and ```. Ask the user if the plan is okay."""
67
-
68
-
69
- PLAYBOOK_CONFIRMING_PROMPT = """Now, you are tasked with confirming the playbook plan. Return True if the user is happy with the plan, False otherwise. Do not say anything else in your response. The user response will be the last message in the chain.
78
+ {
79
+ "steps": [
80
+ "Connect to database using `db_connection_string`",
81
+ "Query user data for `user_id`",
82
+ "Process results and calculate `metric_name`",
83
+ "Send notification to `email_address`"
84
+ ]
85
+ }
86
+
87
+ Now create a plan based on the conversation history. Do not include any other text or explanation in your response. Just the JSON object.
70
88
  """
71
89
 
72
- PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function. Return the function in Python code.
73
- Do not include any other text in your response.
90
+
91
+ PLAYBOOK_GENERATING_PROMPT = """Now, you are tasked with generating the playbook function.
92
+ Your response must be ONLY the Python code for the function.
93
+ Do not include any other text, markdown, or explanations in your response.
94
+ Your response should start with `def` or `async def`.
74
95
  The function should be a single, complete piece of code that can be executed independently, based on previously executed code snippets that executed correctly.
75
96
  The parameters of the function should be the same as the final confirmed playbook plan.
76
- Do not include anything other than python code in your response
77
97
  """
78
98
 
79
99
 
@@ -90,107 +110,18 @@ def make_safe_function_name(name: str) -> str:
90
110
  return safe_name
91
111
 
92
112
 
93
- def dedent(text):
94
- """Remove any common leading whitespace from every line in `text`.
95
-
96
- This can be used to make triple-quoted strings line up with the left
97
- edge of the display, while still presenting them in the source code
98
- in indented form.
99
-
100
- Note that tabs and spaces are both treated as whitespace, but they
101
- are not equal: the lines " hello" and "\\thello" are
102
- considered to have no common leading whitespace.
103
-
104
- Entirely blank lines are normalized to a newline character.
105
- """
106
- # Look for the longest leading string of spaces and tabs common to
107
- # all lines.
108
- margin = None
109
- _whitespace_only_re = re.compile("^[ \t]+$", re.MULTILINE)
110
- _leading_whitespace_re = re.compile("(^[ \t]*)(?:[^ \t\n])", re.MULTILINE)
111
- text = _whitespace_only_re.sub("", text)
112
- indents = _leading_whitespace_re.findall(text)
113
- for indent in indents:
114
- if margin is None:
115
- margin = indent
116
-
117
- # Current line more deeply indented than previous winner:
118
- # no change (previous winner is still on top).
119
- elif indent.startswith(margin):
120
- pass
121
-
122
- # Current line consistent with and no deeper than previous winner:
123
- # it's the new winner.
124
- elif margin.startswith(indent):
125
- margin = indent
126
-
127
- # Find the largest common whitespace between current line and previous
128
- # winner.
129
- else:
130
- for i, (x, y) in enumerate(zip(margin, indent)):
131
- if x != y:
132
- margin = margin[:i]
133
- break
134
-
135
- # sanity check (testing/debugging only)
136
- if 0 and margin:
137
- for line in text.split("\n"):
138
- assert not line or line.startswith(margin), f"line = {line!r}, margin = {margin!r}"
139
-
140
- if margin:
141
- text = re.sub(r"(?m)^" + margin, "", text)
142
- return text
143
-
144
-
145
- def indent(text, prefix, predicate=None):
146
- """Adds 'prefix' to the beginning of selected lines in 'text'.
147
-
148
- If 'predicate' is provided, 'prefix' will only be added to the lines
149
- where 'predicate(line)' is True. If 'predicate' is not provided,
150
- it will default to adding 'prefix' to all non-empty lines that do not
151
- consist solely of whitespace characters.
152
- """
153
- if predicate is None:
154
- # str.splitlines(True) doesn't produce empty string.
155
- # ''.splitlines(True) => []
156
- # 'foo\n'.splitlines(True) => ['foo\n']
157
- # So we can use just `not s.isspace()` here.
158
- def predicate(s):
159
- return not s.isspace()
160
-
161
- prefixed_lines = []
162
- for line in text.splitlines(True):
163
- if predicate(line):
164
- prefixed_lines.append(prefix)
165
- prefixed_lines.append(line)
166
-
167
- return "".join(prefixed_lines)
168
-
169
-
170
113
  def create_default_prompt(
171
114
  tools: Sequence[StructuredTool],
172
115
  additional_tools: Sequence[StructuredTool],
173
116
  base_prompt: str | None = None,
117
+ playbook: object | None = None,
174
118
  ):
175
119
  system_prompt = uneditable_prompt.strip() + (
176
120
  "\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
177
121
  )
178
122
  tools_context = {}
179
- for tool in tools:
180
- if hasattr(tool, "func") and tool.func is not None:
181
- tool_callable = tool.func
182
- is_async = False
183
- elif hasattr(tool, "coroutine") and tool.coroutine is not None:
184
- tool_callable = tool.coroutine
185
- is_async = True
186
- system_prompt += f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
187
- """{tool.description}"""
188
- ...
189
- '''
190
- safe_name = make_safe_function_name(tool.name)
191
- tools_context[safe_name] = tool_callable
192
123
 
193
- for tool in additional_tools:
124
+ for tool in tools + additional_tools:
194
125
  if hasattr(tool, "func") and tool.func is not None:
195
126
  tool_callable = tool.func
196
127
  is_async = False
@@ -207,4 +138,24 @@ def create_default_prompt(
207
138
  if base_prompt and base_prompt.strip():
208
139
  system_prompt += f"Your goal is to perform the following task:\n\n{base_prompt}"
209
140
 
141
+ # Append existing playbook (plan + code) if provided
142
+ try:
143
+ if playbook and hasattr(playbook, "instructions"):
144
+ pb = playbook.instructions or {}
145
+ plan = pb.get("playbookPlan")
146
+ code = pb.get("playbookScript")
147
+ if plan or code:
148
+ system_prompt += "\n\nExisting Playbook Provided:\n"
149
+ if plan:
150
+ if isinstance(plan, list):
151
+ plan_block = "\n".join(f"- {str(s)}" for s in plan)
152
+ else:
153
+ plan_block = str(plan)
154
+ system_prompt += f"Plan Steps:\n{plan_block}\n"
155
+ if code:
156
+ system_prompt += f"\nScript:\n```python\n{str(code)}\n```\n"
157
+ except Exception:
158
+ # Silently ignore formatting issues
159
+ pass
160
+
210
161
  return system_prompt, tools_context
@@ -1,6 +1,15 @@
1
- from typing import Annotated, Any
1
+ from typing import Annotated, Any, List
2
2
 
3
3
  from langgraph.prebuilt.chat_agent_executor import AgentState
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class PlaybookPlan(BaseModel):
8
+ steps: List[str] = Field(description="The steps of the playbook.")
9
+
10
+
11
+ class PlaybookCode(BaseModel):
12
+ code: str = Field(description="The Python code for the playbook.")
4
13
 
5
14
 
6
15
  def _enqueue(left: list, right: list) -> list:
@@ -36,5 +45,5 @@ class CodeActState(AgentState):
36
45
  """State for the playbook agent."""
37
46
  selected_tool_ids: Annotated[list[str], _enqueue]
38
47
  """Queue for tools exported from registry"""
39
- plan: str | None
48
+ plan: list[str] | None
40
49
  """Plan for the playbook agent."""
@@ -4,6 +4,7 @@ from collections import defaultdict
4
4
  from typing import Annotated, Any
5
5
 
6
6
  from langchain_core.tools import tool
7
+ from loguru import logger
7
8
  from pydantic import Field
8
9
  from universal_mcp.agentr.registry import AgentrRegistry
9
10
  from universal_mcp.types import ToolFormat
@@ -224,19 +225,9 @@ def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
224
225
  - answer (str): Generated answer with markdown formatting and citation numbers [1][2]
225
226
  - citations (list[str]): List of source URLs corresponding to citation numbers
226
227
  """
227
- await tool_registry.export_tools(["perplexity__answer_with_search"], ToolFormat.LANGCHAIN)
228
-
229
- response = await tool_registry.call_tool(
230
- "perplexity__answer_with_search",
231
- {
232
- "query": query,
233
- "model": "sonar",
234
- "temperature": 1.0,
235
- "system_prompt": (
236
- "You are a helpful AI assistant that answers questions using real-time information from the web."
237
- ),
238
- },
239
- )
228
+ await tool_registry.export_tools(["exa__answer"], ToolFormat.LANGCHAIN)
229
+ response = await tool_registry.call_tool("exa__answer", {"query": query, "text": True})
230
+ logger.info(f"Web search response: {response}")
240
231
 
241
232
  # Extract only desired fields
242
233
  return {
@@ -5,10 +5,18 @@ from collections.abc import Sequence
5
5
  from typing import Any
6
6
 
7
7
  from langchain_core.messages import BaseMessage
8
+ from universal_mcp.types import ToolConfig
8
9
 
9
10
  MAX_CHARS = 5000
10
11
 
11
12
 
13
+ def add_tools(tool_config: ToolConfig, tools_to_add: ToolConfig):
14
+ for app_id, new_tools in tools_to_add.items():
15
+ all_tools = tool_config.get(app_id, []) + new_tools
16
+ tool_config[app_id] = list(set(all_tools))
17
+ return tool_config
18
+
19
+
12
20
  def light_copy(data):
13
21
  """
14
22
  Deep copy a dict[str, any] or Sequence[any] with string truncation.
@@ -162,11 +162,11 @@ class LlmApp(BaseApplication):
162
162
  top_class: str = Field(..., description="The class with the highest probability.")
163
163
 
164
164
  response = (
165
- model.with_structured_output(schema=ClassificationResult, method="json_mode")
165
+ model.with_structured_output(schema=ClassificationResult)
166
166
  .with_retry(stop_after_attempt=MAX_RETRIES)
167
167
  .invoke(prompt)
168
168
  )
169
- return cast(dict[str, Any], response)
169
+ return response.model_dump_json(indent=2)
170
170
 
171
171
  def extract_data(
172
172
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-agents
3
- Version: 0.1.20rc1
3
+ Version: 0.1.21
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/universal-mcp/applications
6
6
  Project-URL: Repository, https://github.com/universal-mcp/applications
@@ -1,5 +1,5 @@
1
1
  universal_mcp/agents/__init__.py,sha256=Rh6vKqpwuZ2joC9nzLFHUI1G7jzbwwC3p0mgpH5qRgo,1112
2
- universal_mcp/agents/base.py,sha256=wkqa_W2R6sFgjTMXn2bWVfjarHYDsLvGbG8nN-jRp6E,7175
2
+ universal_mcp/agents/base.py,sha256=CEnY8y2as_XR311t9v2iqd4DOCSyhpOPOBDcZKNJMpc,7378
3
3
  universal_mcp/agents/cli.py,sha256=bXdpgxsOMjclm1STHJgx10ocX9EebQ11DrxH0p6KMZk,943
4
4
  universal_mcp/agents/hil.py,sha256=_5PCK6q0goGm8qylJq44aSp2MadP-yCPvhOJYKqWLMo,3808
5
5
  universal_mcp/agents/llm.py,sha256=hVRwjZs3MHl5_3BWedmurs2Jt1oZDfFX0Zj9F8KH7fk,1787
@@ -22,23 +22,23 @@ universal_mcp/agents/builder/prompts.py,sha256=8Xs6uzTUHguDRngVMLak3lkXFkk2VV_uQ
22
22
  universal_mcp/agents/builder/state.py,sha256=7DeWllxfN-yD6cd9wJ3KIgjO8TctkJvVjAbZT8W_zqk,922
23
23
  universal_mcp/agents/codeact0/__init__.py,sha256=8-fvUo1Sm6dURGI-lW-X3Kd78LqySYbb5NMkNJ4NDwg,76
24
24
  universal_mcp/agents/codeact0/__main__.py,sha256=_7qSz97YnRgYJTESkALS5_eBIGHiMjA5rhr3IAeBvVo,896
25
- universal_mcp/agents/codeact0/agent.py,sha256=qUcNniw8DyvEfpEuaWA9LDqDe0eiRPEHrZtysfdKY8k,14238
25
+ universal_mcp/agents/codeact0/agent.py,sha256=KvIoZh2J2JQONfGf80LBuXDdQnnYja4ybZ4DluCKxPM,15192
26
26
  universal_mcp/agents/codeact0/config.py,sha256=H-1woj_nhSDwf15F63WYn723y4qlRefXzGxuH81uYF0,2215
27
27
  universal_mcp/agents/codeact0/langgraph_agent.py,sha256=8nz2wq-LexImx-l1y9_f81fK72IQetnCeljwgnduNGY,420
28
- universal_mcp/agents/codeact0/llm_tool.py,sha256=q-hiqkKtjVmpyNceFoRgo7hvKh4HtQf_I1VudRUEPR0,11075
29
- universal_mcp/agents/codeact0/prompts.py,sha256=IZwiGLYISr_oXeNwhPJLNpE5wYd5zyntEdEUGH21OD8,10720
28
+ universal_mcp/agents/codeact0/llm_tool.py,sha256=-pAz04OrbZ_dJ2ueysT1qZd02DrbLY4EbU0tiuF_UNU,798
29
+ universal_mcp/agents/codeact0/prompts.py,sha256=a5ja7KwSG1SmbCkoZVdyFOI5dz2Ul-OWG-JcakBySDM,9635
30
30
  universal_mcp/agents/codeact0/sandbox.py,sha256=Xw4tbUV_6haYIZZvteJi6lIYsW6ni_3DCRCOkslTKgM,4459
31
- universal_mcp/agents/codeact0/state.py,sha256=B1XF5ni9RvN6p5POHkPTm-KjEx5c8wRB7O1EJM8pbSQ,1297
32
- universal_mcp/agents/codeact0/tools.py,sha256=G580vy-m_BqlHc_j0yvLV5T_2d2G0JrD_eDhvDdJ9JA,13614
33
- universal_mcp/agents/codeact0/utils.py,sha256=jAZItSd3KGDkY9PquSWRIFCj9N26K9Kt0HKQ_jwvvSQ,15944
31
+ universal_mcp/agents/codeact0/state.py,sha256=Z9fUmBTw-29MZTq2GsYYyyjLPyrtE5GDnhX4gSqHhcA,1555
32
+ universal_mcp/agents/codeact0/tools.py,sha256=qGhbJl74xflJBNfOCiy-Ts5zKIxXjE1JVyqvtVGHZNY,13363
33
+ universal_mcp/agents/codeact0/utils.py,sha256=Rustf3MFoFbasdnwBh9YvNrXqasgANkBAWQn9AjqBO4,16240
34
34
  universal_mcp/agents/shared/__main__.py,sha256=XxH5qGDpgFWfq7fwQfgKULXGiUgeTp_YKfcxftuVZq8,1452
35
35
  universal_mcp/agents/shared/prompts.py,sha256=yjP3zbbuKi87qCj21qwTTicz8TqtkKgnyGSeEjMu3ho,3761
36
36
  universal_mcp/agents/shared/tool_node.py,sha256=DC9F-Ri28Pam0u3sXWNODVgmj9PtAEUb5qP1qOoGgfs,9169
37
37
  universal_mcp/applications/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  universal_mcp/applications/filesystem/app.py,sha256=0TRjjm8YnslVRSmfkXI7qQOAlqWlD1eEn8Jm0xBeigs,5561
39
39
  universal_mcp/applications/llm/__init__.py,sha256=_XGRxN3O1--ZS5joAsPf8IlI9Qa6negsJrwJ5VJXno0,46
40
- universal_mcp/applications/llm/app.py,sha256=oqX3byvlFRmeRo4jJJxUBGy-iTDGm2fplMEKA2pcMtw,12743
40
+ universal_mcp/applications/llm/app.py,sha256=g9mK-luOLUshZzBGyQZMOHBeCSXmh2kCKir40YnsGUo,12727
41
41
  universal_mcp/applications/ui/app.py,sha256=c7OkZsO2fRtndgAzAQbKu-1xXRuRp9Kjgml57YD2NR4,9459
42
- universal_mcp_agents-0.1.20rc1.dist-info/METADATA,sha256=49lAde7iqPxxgwPAb7BZCERlIkajP0EoiiuvtA7hy24,881
43
- universal_mcp_agents-0.1.20rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- universal_mcp_agents-0.1.20rc1.dist-info/RECORD,,
42
+ universal_mcp_agents-0.1.21.dist-info/METADATA,sha256=2Nx9bEG_IEb5wHTV-8vXpQEUTNsl0bbQkPR6Ii4R1V0,878
43
+ universal_mcp_agents-0.1.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ universal_mcp_agents-0.1.21.dist-info/RECORD,,