universal-mcp-agents 0.1.23__py3-none-any.whl → 0.1.24rc3__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 +11 -2
- universal_mcp/agents/base.py +3 -6
- universal_mcp/agents/codeact0/agent.py +14 -17
- universal_mcp/agents/codeact0/prompts.py +9 -3
- universal_mcp/agents/codeact0/sandbox.py +2 -2
- universal_mcp/agents/codeact0/tools.py +2 -2
- universal_mcp/agents/codeact0/utils.py +48 -0
- universal_mcp/agents/codeact00/__init__.py +3 -0
- universal_mcp/agents/codeact00/__main__.py +26 -0
- universal_mcp/agents/codeact00/agent.py +578 -0
- universal_mcp/agents/codeact00/config.py +77 -0
- universal_mcp/agents/codeact00/langgraph_agent.py +14 -0
- universal_mcp/agents/codeact00/llm_tool.py +25 -0
- universal_mcp/agents/codeact00/prompts.py +364 -0
- universal_mcp/agents/codeact00/sandbox.py +135 -0
- universal_mcp/agents/codeact00/state.py +66 -0
- universal_mcp/agents/codeact00/tools.py +525 -0
- universal_mcp/agents/codeact00/utils.py +678 -0
- universal_mcp/agents/codeact01/__init__.py +3 -0
- universal_mcp/agents/codeact01/__main__.py +26 -0
- universal_mcp/agents/codeact01/agent.py +413 -0
- universal_mcp/agents/codeact01/config.py +77 -0
- universal_mcp/agents/codeact01/langgraph_agent.py +14 -0
- universal_mcp/agents/codeact01/llm_tool.py +25 -0
- universal_mcp/agents/codeact01/prompts.py +246 -0
- universal_mcp/agents/codeact01/sandbox.py +162 -0
- universal_mcp/agents/codeact01/state.py +58 -0
- universal_mcp/agents/codeact01/tools.py +648 -0
- universal_mcp/agents/codeact01/utils.py +552 -0
- universal_mcp/agents/llm.py +7 -3
- universal_mcp/applications/llm/app.py +66 -15
- {universal_mcp_agents-0.1.23.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/METADATA +1 -1
- universal_mcp_agents-0.1.24rc3.dist-info/RECORD +66 -0
- universal_mcp_agents-0.1.23.dist-info/RECORD +0 -44
- {universal_mcp_agents-0.1.23.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
from langchain_core.tools import tool
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from universal_mcp.agentr.client import AgentrClient
|
|
10
|
+
from universal_mcp.agentr.registry import AgentrRegistry
|
|
11
|
+
from universal_mcp.applications.markitdown.app import MarkitdownApp
|
|
12
|
+
from universal_mcp.types import ToolFormat
|
|
13
|
+
from langgraph.types import StreamWriter
|
|
14
|
+
|
|
15
|
+
from universal_mcp.agents.codeact01.prompts import build_tool_definitions
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def enter_agent_builder_mode():
|
|
20
|
+
"""Call this function to enter agent builder mode. Agent builder mode is when the user wants to store a repeated task as a script with some inputs for the future."""
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_meta_tools(tool_registry: AgentrRegistry) -> dict[str, Any]:
|
|
25
|
+
"""Create the meta tools for searching and loading tools"""
|
|
26
|
+
|
|
27
|
+
@tool
|
|
28
|
+
async def search_functions(
|
|
29
|
+
queries: Annotated[
|
|
30
|
+
list[list[str]] | None,
|
|
31
|
+
Field(
|
|
32
|
+
description="A list of query lists. Each inner list contains one or more search terms that will be used together to find relevant tools."
|
|
33
|
+
),
|
|
34
|
+
] = None,
|
|
35
|
+
app_ids: Annotated[
|
|
36
|
+
list[str] | None,
|
|
37
|
+
Field(description="The ID or list of IDs (common names) of specific applications to search within."),
|
|
38
|
+
] = None,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Searches for relevant functions based on queries and/or applications. This function
|
|
42
|
+
operates in three powerful modes with support for multi-query searches:
|
|
43
|
+
|
|
44
|
+
1. **Global Search** (`queries` only as List[List[str]]):
|
|
45
|
+
- Searches all functions across all applications.
|
|
46
|
+
- Supports multiple independent searches in parallel.
|
|
47
|
+
- Each inner list represents a separate search query.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
- Single global search:
|
|
51
|
+
`search_functions(queries=[["create presentation"]])`
|
|
52
|
+
|
|
53
|
+
- Multiple independent global searches:
|
|
54
|
+
`search_functions(queries=[["send email"], ["schedule meeting"]])`
|
|
55
|
+
|
|
56
|
+
- Multi-term search for comprehensive results:
|
|
57
|
+
`search_functions(queries=[["send email", "draft email", "compose email"]])`
|
|
58
|
+
|
|
59
|
+
2. **App Discovery** (`app_ids` only as List[str]):
|
|
60
|
+
- Returns ALL available functions for one or more specific applications.
|
|
61
|
+
- Use this to explore the complete capability set of an application.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
- Single app discovery:
|
|
65
|
+
`search_functions(app_ids=["Gmail"])`
|
|
66
|
+
|
|
67
|
+
- Multiple app discovery:
|
|
68
|
+
`search_functions(app_ids=["Gmail", "Google Calendar", "Slack"])`
|
|
69
|
+
|
|
70
|
+
3. **Scoped Search** (`queries` as List[List[str]] and `app_ids` as List[str]):
|
|
71
|
+
- Performs targeted searches within specific applications in parallel.
|
|
72
|
+
- The number of app_ids must match the number of inner query lists.
|
|
73
|
+
- Each query list is searched within its corresponding app_id.
|
|
74
|
+
- Supports multiple search terms per app for comprehensive discovery.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
- Basic scoped search (one query per app):
|
|
78
|
+
`search_functions(queries=[["find email"], ["share file"]], app_ids=["Gmail", "Google_Drive"])`
|
|
79
|
+
|
|
80
|
+
- Multi-term scoped search (multiple queries per app):
|
|
81
|
+
`search_functions(
|
|
82
|
+
queries=[
|
|
83
|
+
["send email", "draft email", "compose email", "reply to email"],
|
|
84
|
+
["create event", "schedule meeting", "find free time"],
|
|
85
|
+
["upload file", "share file", "create folder", "search files"]
|
|
86
|
+
],
|
|
87
|
+
app_ids=["Gmail", "Google Calendar", "Google_Drive"]
|
|
88
|
+
)`
|
|
89
|
+
|
|
90
|
+
- Mixed complexity (some apps with single query, others with multiple):
|
|
91
|
+
`search_functions(
|
|
92
|
+
queries=[
|
|
93
|
+
["list messages"],
|
|
94
|
+
["create event", "delete event", "update event"]
|
|
95
|
+
],
|
|
96
|
+
app_ids=["Gmail", "Google Calendar"]
|
|
97
|
+
)`
|
|
98
|
+
|
|
99
|
+
**Pro Tips:**
|
|
100
|
+
- Use multiple search terms in a single query list to cast a wider net and discover related functionality
|
|
101
|
+
- Multi-term searches are more efficient than separate calls
|
|
102
|
+
- Scoped searches return more focused results than global searches
|
|
103
|
+
- The function returns connection status for each app (connected vs NOT connected)
|
|
104
|
+
- All searches within a single call execute in parallel for maximum efficiency
|
|
105
|
+
|
|
106
|
+
**Parameters:**
|
|
107
|
+
- `queries` (List[List[str]], optional): A list of query lists. Each inner list contains one or more
|
|
108
|
+
search terms that will be used together to find relevant tools.
|
|
109
|
+
- `app_ids` (List[str], optional): A list of application IDs to search within or discover.
|
|
110
|
+
|
|
111
|
+
**Returns:**
|
|
112
|
+
- A structured response containing:
|
|
113
|
+
- Matched tools with their descriptions
|
|
114
|
+
- Connection status for each app
|
|
115
|
+
- Recommendations for which tools to load next
|
|
116
|
+
"""
|
|
117
|
+
registry = tool_registry
|
|
118
|
+
|
|
119
|
+
TOOL_THRESHOLD = 0.75
|
|
120
|
+
APP_THRESHOLD = 0.7
|
|
121
|
+
|
|
122
|
+
# --- Helper Functions for Different Search Modes ---
|
|
123
|
+
|
|
124
|
+
async def _handle_global_search(queries: list[str]) -> list[list[dict[str, Any]]]:
|
|
125
|
+
"""Performs a broad search across all apps to find relevant tools and apps."""
|
|
126
|
+
# 1. Perform initial broad searches for tools and apps concurrently.
|
|
127
|
+
initial_tool_tasks = [registry.search_tools(query=q, distance_threshold=TOOL_THRESHOLD) for q in queries]
|
|
128
|
+
app_search_tasks = [registry.search_apps(query=q, distance_threshold=APP_THRESHOLD) for q in queries]
|
|
129
|
+
|
|
130
|
+
initial_tool_results, app_search_results = await asyncio.gather(
|
|
131
|
+
asyncio.gather(*initial_tool_tasks), asyncio.gather(*app_search_tasks)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# 2. Create a prioritized list of app IDs for the final search.
|
|
135
|
+
app_ids_from_apps = {app["id"] for result_list in app_search_results for app in result_list}
|
|
136
|
+
prioritized_app_id_list = list(app_ids_from_apps)
|
|
137
|
+
|
|
138
|
+
app_ids_from_tools = {tool["app_id"] for result_list in initial_tool_results for tool in result_list}
|
|
139
|
+
for tool_app_id in app_ids_from_tools:
|
|
140
|
+
if tool_app_id not in app_ids_from_apps:
|
|
141
|
+
prioritized_app_id_list.append(tool_app_id)
|
|
142
|
+
|
|
143
|
+
if not prioritized_app_id_list:
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
# 3. Perform the final, comprehensive tool search across the prioritized apps.
|
|
147
|
+
final_tool_search_tasks = [
|
|
148
|
+
registry.search_tools(query=query, app_id=app_id_to_search, distance_threshold=TOOL_THRESHOLD)
|
|
149
|
+
for app_id_to_search in prioritized_app_id_list
|
|
150
|
+
for query in queries
|
|
151
|
+
]
|
|
152
|
+
return await asyncio.gather(*final_tool_search_tasks)
|
|
153
|
+
|
|
154
|
+
async def _handle_scoped_search(app_ids: list[str], queries: list[list[str]]) -> list[list[dict[str, Any]]]:
|
|
155
|
+
"""Performs targeted searches for specific queries within specific applications."""
|
|
156
|
+
if len(app_ids) != len(queries):
|
|
157
|
+
raise ValueError("The number of app_ids must match the number of query lists.")
|
|
158
|
+
|
|
159
|
+
tasks = []
|
|
160
|
+
for app_id, query_list in zip(app_ids, queries):
|
|
161
|
+
for query in query_list:
|
|
162
|
+
# Create a search task for each query in the list for the corresponding app
|
|
163
|
+
tasks.append(registry.search_tools(query=query, app_id=app_id, distance_threshold=TOOL_THRESHOLD))
|
|
164
|
+
|
|
165
|
+
return await asyncio.gather(*tasks)
|
|
166
|
+
|
|
167
|
+
async def _handle_app_discovery(app_ids: list[str]) -> list[list[dict[str, Any]]]:
|
|
168
|
+
"""Fetches all tools for a list of applications."""
|
|
169
|
+
tasks = [registry.search_tools(query="", app_id=app_id, limit=20) for app_id in app_ids]
|
|
170
|
+
return await asyncio.gather(*tasks)
|
|
171
|
+
|
|
172
|
+
# --- Helper Functions for Structuring and Formatting Results ---
|
|
173
|
+
|
|
174
|
+
def _format_response(structured_results: list[dict[str, Any]]) -> str:
|
|
175
|
+
"""Builds the final, user-facing formatted string response from structured data."""
|
|
176
|
+
if not structured_results:
|
|
177
|
+
return "No relevant functions were found."
|
|
178
|
+
|
|
179
|
+
result_parts = []
|
|
180
|
+
apps_in_results = {app["app_id"] for app in structured_results}
|
|
181
|
+
connected_apps_in_results = {
|
|
182
|
+
app["app_id"] for app in structured_results if app["connection_status"] == "connected"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for app in structured_results:
|
|
186
|
+
app_id = app["app_id"]
|
|
187
|
+
app_status = "connected" if app["connection_status"] == "connected" else "NOT connected"
|
|
188
|
+
result_parts.append(f"Tools from {app_id} (status: {app_status} by user):")
|
|
189
|
+
|
|
190
|
+
for tool in app["tools"]:
|
|
191
|
+
result_parts.append(f" - {tool['id']}: {tool['description']}")
|
|
192
|
+
result_parts.append("") # Empty line for readability
|
|
193
|
+
|
|
194
|
+
# Add summary connection status messages
|
|
195
|
+
if not connected_apps_in_results and len(apps_in_results) > 1:
|
|
196
|
+
result_parts.append(
|
|
197
|
+
"Connection Status: None of the apps in the results are connected. "
|
|
198
|
+
"You must ask the user to choose the application."
|
|
199
|
+
)
|
|
200
|
+
elif len(connected_apps_in_results) > 1:
|
|
201
|
+
connected_list = ", ".join(sorted(list(connected_apps_in_results)))
|
|
202
|
+
result_parts.append(
|
|
203
|
+
f"Connection Status: Multiple apps are connected ({connected_list}). "
|
|
204
|
+
"You must ask the user to select which application they want to use."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
result_parts.append("Call load_functions to select the required functions only.")
|
|
208
|
+
if 0 <= len(connected_apps_in_results) < len(apps_in_results):
|
|
209
|
+
result_parts.append(
|
|
210
|
+
"Unconnected app functions can also be loaded if asked for by the user, they will generate a connection link"
|
|
211
|
+
"but prefer connected ones. Ask the user to choose the app if none of the "
|
|
212
|
+
"relevant apps are connected."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return "\n".join(result_parts)
|
|
216
|
+
|
|
217
|
+
def _structure_tool_results(
|
|
218
|
+
raw_tool_lists: list[list[dict[str, Any]]], connected_app_ids: set[str]
|
|
219
|
+
) -> list[dict[str, Any]]:
|
|
220
|
+
"""
|
|
221
|
+
Converts raw search results into a structured format, handling duplicates,
|
|
222
|
+
cleaning descriptions, and adding connection status.
|
|
223
|
+
"""
|
|
224
|
+
aggregated_tools = defaultdict(dict)
|
|
225
|
+
# Use a list to maintain the order of apps as they are found.
|
|
226
|
+
ordered_app_ids = []
|
|
227
|
+
|
|
228
|
+
for tool_list in raw_tool_lists:
|
|
229
|
+
for tool in tool_list:
|
|
230
|
+
app_id = tool.get("app_id", "unknown")
|
|
231
|
+
tool_id = tool.get("id")
|
|
232
|
+
|
|
233
|
+
if not tool_id:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if app_id not in aggregated_tools:
|
|
237
|
+
ordered_app_ids.append(app_id)
|
|
238
|
+
|
|
239
|
+
if tool_id not in aggregated_tools[app_id]:
|
|
240
|
+
aggregated_tools[app_id][tool_id] = {
|
|
241
|
+
"id": tool_id,
|
|
242
|
+
"description": _clean_tool_description(tool.get("description", "")),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Build the final results list respecting the discovery order.
|
|
246
|
+
found_tools_result = []
|
|
247
|
+
for app_id in ordered_app_ids:
|
|
248
|
+
if app_id in aggregated_tools and aggregated_tools[app_id]:
|
|
249
|
+
found_tools_result.append(
|
|
250
|
+
{
|
|
251
|
+
"app_id": app_id,
|
|
252
|
+
"connection_status": "connected" if app_id in connected_app_ids else "not_connected",
|
|
253
|
+
"tools": list(aggregated_tools[app_id].values()),
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
return found_tools_result
|
|
257
|
+
|
|
258
|
+
def _clean_tool_description(description: str) -> str:
|
|
259
|
+
"""Consistently formats tool descriptions by removing implementation details."""
|
|
260
|
+
return description.split("Context:")[0].strip()
|
|
261
|
+
|
|
262
|
+
# Main Function Logic
|
|
263
|
+
|
|
264
|
+
if not queries and not app_ids:
|
|
265
|
+
raise ValueError("You must provide 'queries', 'app_ids', or both.")
|
|
266
|
+
|
|
267
|
+
# --- Initialization and Input Normalization ---
|
|
268
|
+
connections = await registry.list_connected_apps()
|
|
269
|
+
connected_app_ids = {connection["app_id"] for connection in connections}
|
|
270
|
+
|
|
271
|
+
canonical_app_ids = []
|
|
272
|
+
if app_ids:
|
|
273
|
+
# Concurrently search for all provided app names
|
|
274
|
+
app_search_tasks = [
|
|
275
|
+
registry.search_apps(query=app_name, distance_threshold=APP_THRESHOLD) for app_name in app_ids
|
|
276
|
+
]
|
|
277
|
+
app_search_results = await asyncio.gather(*app_search_tasks)
|
|
278
|
+
|
|
279
|
+
# Process results and build the list of canonical IDs, handling not found errors
|
|
280
|
+
for app_name, result_list in zip(app_ids, app_search_results):
|
|
281
|
+
if not result_list:
|
|
282
|
+
raise ValueError(f"Application '{app_name}' could not be found.")
|
|
283
|
+
# Assume the first result is the correct one
|
|
284
|
+
canonical_app_ids.append(result_list[0]["id"])
|
|
285
|
+
|
|
286
|
+
# --- Mode Dispatching ---
|
|
287
|
+
raw_results = []
|
|
288
|
+
|
|
289
|
+
if canonical_app_ids and queries:
|
|
290
|
+
raw_results = await _handle_scoped_search(canonical_app_ids, queries)
|
|
291
|
+
elif canonical_app_ids:
|
|
292
|
+
raw_results = await _handle_app_discovery(canonical_app_ids)
|
|
293
|
+
elif queries:
|
|
294
|
+
# Flatten list of lists to list of strings for global search
|
|
295
|
+
flat_queries = (
|
|
296
|
+
[q for sublist in queries for q in sublist] if queries and not isinstance(queries[0], str) else queries
|
|
297
|
+
)
|
|
298
|
+
raw_results = await _handle_global_search(flat_queries)
|
|
299
|
+
|
|
300
|
+
# --- Structuring and Formatting ---
|
|
301
|
+
structured_data = _structure_tool_results(raw_results, connected_app_ids)
|
|
302
|
+
return _format_response(structured_data)
|
|
303
|
+
|
|
304
|
+
@tool
|
|
305
|
+
async def load_functions(tool_ids: list[str]) -> str:
|
|
306
|
+
"""
|
|
307
|
+
Loads specified functions and returns their Python signatures and docstrings.
|
|
308
|
+
This makes the functions available for use inside the 'execute_ipython_cell' tool.
|
|
309
|
+
The agent MUST use the returned information to understand how to call the functions correctly.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
tool_ids: A list of function IDs in the format 'app__function'. Example: ['google_mail__send_email']
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
A string containing the signatures and docstrings of the successfully loaded functions,
|
|
316
|
+
ready for the agent to use in its code.
|
|
317
|
+
"""
|
|
318
|
+
if not tool_ids:
|
|
319
|
+
return "No tool IDs provided to load."
|
|
320
|
+
|
|
321
|
+
# Step 1: Validate which tools are usable and get login links for others.
|
|
322
|
+
valid_tools, unconnected_links = await get_valid_tools(tool_ids=tool_ids, registry=tool_registry)
|
|
323
|
+
|
|
324
|
+
if not valid_tools:
|
|
325
|
+
response_string = "Error: None of the provided tool IDs could be validated or loaded."
|
|
326
|
+
return response_string, {}, [], ""
|
|
327
|
+
|
|
328
|
+
# Step 2: Export the schemas of the valid tools.
|
|
329
|
+
await tool_registry.load_tools(valid_tools)
|
|
330
|
+
exported_tools = await tool_registry.export_tools(
|
|
331
|
+
valid_tools, ToolFormat.NATIVE
|
|
332
|
+
) # Get definition for only the new tools
|
|
333
|
+
|
|
334
|
+
# Step 3: Build the informational string for the agent.
|
|
335
|
+
tool_definitions, new_tools_context = build_tool_definitions(exported_tools)
|
|
336
|
+
|
|
337
|
+
result_parts = [
|
|
338
|
+
f"Successfully loaded {len(exported_tools)} functions. They are now available for use inside `execute_ipython_cell`:",
|
|
339
|
+
"\n".join(tool_definitions),
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
response_string = "\n\n".join(result_parts)
|
|
343
|
+
unconnected_links = "\n".join(unconnected_links)
|
|
344
|
+
|
|
345
|
+
return response_string, new_tools_context, valid_tools, unconnected_links
|
|
346
|
+
|
|
347
|
+
async def web_search(query: str) -> dict:
|
|
348
|
+
"""
|
|
349
|
+
Get an LLM answer to a question informed by Exa search results. Useful when you need information from a wide range of real-time sources on the web. Do not use this when you need to access contents of a specific webpage.
|
|
350
|
+
|
|
351
|
+
This tool performs an Exa `/answer` request, which:
|
|
352
|
+
1. Provides a **direct answer** for factual queries (e.g., "What is the capital of France?" → "Paris")
|
|
353
|
+
2. Generates a **summary with citations** for open-ended questions
|
|
354
|
+
(e.g., "What is the state of AI in healthcare?" → A detailed summary with source links)
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
query (str): The question or topic to answer.
|
|
358
|
+
Returns:
|
|
359
|
+
dict: A structured response containing only:
|
|
360
|
+
- answer (str): Generated answer
|
|
361
|
+
- citations (list[dict]): List of cited sources
|
|
362
|
+
"""
|
|
363
|
+
await tool_registry.export_tools(["exa__answer"], ToolFormat.LANGCHAIN)
|
|
364
|
+
response = await tool_registry.call_tool("exa__answer", {"query": query, "text": True})
|
|
365
|
+
|
|
366
|
+
# Extract only desired fields
|
|
367
|
+
return {
|
|
368
|
+
"answer": response.get("answer"),
|
|
369
|
+
"citations": response.get("citations", []),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async def read_file(uri: str) -> str:
|
|
373
|
+
"""
|
|
374
|
+
Asynchronously reads a local file or uri and returns the content as a markdown string.
|
|
375
|
+
|
|
376
|
+
This tool aims to extract the main text content from various sources.
|
|
377
|
+
It automatically prepends 'file://' to the input string if it appears
|
|
378
|
+
to be a local path without a specified scheme (like http, https, data, file).
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
uri (str): The URI pointing to the resource or a local file path.
|
|
382
|
+
Supported schemes:
|
|
383
|
+
- http:// or https:// (Web pages, feeds, APIs)
|
|
384
|
+
- file:// (Local or accessible network files)
|
|
385
|
+
- data: (Embedded data)
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
A string containing the markdown representation of the content at the specified URI
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
ValueError: If the URI is invalid, empty, or uses an unsupported scheme
|
|
392
|
+
after automatic prefixing.
|
|
393
|
+
|
|
394
|
+
Tags:
|
|
395
|
+
convert, markdown, async, uri, transform, document, important
|
|
396
|
+
"""
|
|
397
|
+
markitdown = MarkitdownApp()
|
|
398
|
+
response = await markitdown.convert_to_markdown(uri)
|
|
399
|
+
return response
|
|
400
|
+
|
|
401
|
+
def save_file(file_name: str, content: str) -> dict:
|
|
402
|
+
"""
|
|
403
|
+
Saves a file to the local filesystem.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
file_name (str): The name of the file to save.
|
|
407
|
+
content (str): The content to save to the file.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
dict: A dictionary containing the result of the save operation with the following fields:
|
|
411
|
+
- status (str): "success" if the save succeeded, "error" otherwise.
|
|
412
|
+
- message (str): A message returned by the server, typically indicating success or providing error details.
|
|
413
|
+
"""
|
|
414
|
+
with Path(file_name).open("w") as f:
|
|
415
|
+
f.write(content)
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
"status": "success",
|
|
419
|
+
"message": f"File {file_name} saved successfully",
|
|
420
|
+
"file_path": Path(file_name).absolute(),
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
def upload_file(file_name: str, mime_type: str, base64_data: str) -> dict:
|
|
424
|
+
"""
|
|
425
|
+
Uploads a file to the server.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
file_name (str): The name of the file to upload.
|
|
429
|
+
mime_type (str): The MIME type of the file.
|
|
430
|
+
base64_data (str): The file content encoded as a base64 string.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
dict: A dictionary containing the result of the upload operation with the following fields:
|
|
434
|
+
- status (str): "success" if the upload succeeded, "error" otherwise.
|
|
435
|
+
- message (str): A message returned by the server, typically indicating success or providing error details.
|
|
436
|
+
- signed_url (str or None): The signed URL to access the uploaded file if successful, None otherwise.
|
|
437
|
+
"""
|
|
438
|
+
client: AgentrClient = tool_registry.client
|
|
439
|
+
bytes_data = base64.b64decode(base64_data)
|
|
440
|
+
response = client._upload_file(file_name, mime_type, bytes_data)
|
|
441
|
+
if response.get("status") != "success":
|
|
442
|
+
return {
|
|
443
|
+
"status": "error",
|
|
444
|
+
"message": response.get("message"),
|
|
445
|
+
"signed_url": None,
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
"status": "success",
|
|
449
|
+
"message": response.get("message"),
|
|
450
|
+
"signed_url": response.get("signed_url"),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
"search_functions": search_functions,
|
|
455
|
+
"load_functions": load_functions,
|
|
456
|
+
"web_search": web_search,
|
|
457
|
+
"read_file": read_file,
|
|
458
|
+
"upload_file": upload_file,
|
|
459
|
+
"save_file": save_file,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
def create_agent_builder_tools() -> dict[str, Any]:
|
|
463
|
+
"""Create tools for agent plan and code creation, saving, modifying"""
|
|
464
|
+
@tool
|
|
465
|
+
async def create_agent_plan(steps: list[str]):
|
|
466
|
+
""" Call this tool to create a draft of a reusable agent plan, that will be used to create a corresponding Python script in the next step if approved by the user in conversation.
|
|
467
|
+
Args:
|
|
468
|
+
steps (list[str]):- A list of strings. Each string is a step in the agent plan, obeying the following rules-
|
|
469
|
+
|
|
470
|
+
Rules:
|
|
471
|
+
- Do NOT include the searching or loading of functions for applications. Assume that the functions have already been loaded.
|
|
472
|
+
- The plan is a sequence of steps corresponding to the key logical steps taken to achieve the user's task in the conversation history, without focusing on technical specifics.
|
|
473
|
+
- Identify user-provided information as variables that should become the main agent input parameters using `variable_name` syntax, enclosed by backticks `...`. Intermediate variables should be highlighted using italics, i.e. *...*, NEVER `...`
|
|
474
|
+
- Keep the logic generic and reusable. Avoid hardcoding any names/constants. However, do try to keep them as variables with defaults, especially if used in the conversation history. They should be represented as `variable_name(default = default_value)`.
|
|
475
|
+
- Have a human-friendly plan and inputs format. That is, it must not use internal IDs or keys used by APIs as either inputs or outputs to the overall plan; using them internally is okay.
|
|
476
|
+
- Be as concise as possible, especially for internal processing steps.
|
|
477
|
+
- For steps where the assistant's intelligence was used outside of the code to infer/decide/analyse something, replace it with the use of *llm__* functions in the plan if required.
|
|
478
|
+
|
|
479
|
+
Example Conversation History:
|
|
480
|
+
User Message: "Create an image using Gemini for Marvel Cinematic Universe in comic style"
|
|
481
|
+
Code snippet: image_result = await google_gemini__generate_image(prompt=prompt)
|
|
482
|
+
Assistant Message: "The image has been successfully generated [image_result]."
|
|
483
|
+
User Message: "Save the image in my OneDrive"
|
|
484
|
+
Code snippet: image_data = base64.b64decode(image_result['data'])
|
|
485
|
+
temp_file_path = tempfile.mktemp(suffix='.png')
|
|
486
|
+
with open(temp_file_path, 'wb') as f:
|
|
487
|
+
f.write(image_data)
|
|
488
|
+
# Upload the image to OneDrive with a descriptive filename
|
|
489
|
+
onedrive_filename = "Marvel_Cinematic_Universe_Comic_Style.png"
|
|
490
|
+
|
|
491
|
+
print(f"Uploading to OneDrive as: {onedrive_filename}")
|
|
492
|
+
|
|
493
|
+
# Upload to OneDrive root folder
|
|
494
|
+
upload_result = onedrive__upload_file(
|
|
495
|
+
file_path=temp_file_path,
|
|
496
|
+
parent_id='root',
|
|
497
|
+
file_name=onedrive_filename
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
Generated Steps:
|
|
501
|
+
"steps": [
|
|
502
|
+
"Generate an image using Gemini model with `image_prompt` and `style(default = 'comic')`",
|
|
503
|
+
"Upload the obtained image to OneDrive using `onedrive_filename(default = 'generated_image.png')` and `onedrive_parent_folder(default = 'root')`",
|
|
504
|
+
"Return confirmation of upload including file name and destination path, and link to the upload"
|
|
505
|
+
]
|
|
506
|
+
Note that internal variables like upload_result, image_result are not highlighted in the plan, and intermediate processing details are skipped.
|
|
507
|
+
Now create a plan based on the conversation history. Do not include any other text or explanation in your response. Just the JSON object.
|
|
508
|
+
"""
|
|
509
|
+
return steps
|
|
510
|
+
@tool
|
|
511
|
+
async def modify_agent_plan(modifications: list[str]):
|
|
512
|
+
""" Call this tool to modify a plan created using create_agent_plan.
|
|
513
|
+
Args:
|
|
514
|
+
steps (list[str]):- A list of strings. Each string can be one of the following-
|
|
515
|
+
- <nochange> carries the corresponding step from the most recently created plan using create_agent_plan.
|
|
516
|
+
- <new>content</new> adds a step to the previous plan, shifting the further steps of the plan one step ahead.
|
|
517
|
+
- <modify>content</modify> rewrites an entire step and replaces it with content.
|
|
518
|
+
- <delete> deletes the corresponding step.
|
|
519
|
+
Follow the same rules for steps as create_agent_plan.
|
|
520
|
+
You must call this before save_agent_code if there is any change requested by the user to the plan.
|
|
521
|
+
"""
|
|
522
|
+
return modifications
|
|
523
|
+
@tool
|
|
524
|
+
async def save_agent_code(agent_name: str, agent_description: str, python_code: str):
|
|
525
|
+
"""
|
|
526
|
+
Call this tool to save reusable Python code for the agent.
|
|
527
|
+
Args:
|
|
528
|
+
-agent_name: 3-6 words, Title Case, no punctuation except hyphens if needed
|
|
529
|
+
-agent_description: Single sentence, <= 140 characters, clearly states what the agent does
|
|
530
|
+
-python_code(str): The python code in string form, obeying the following rules-
|
|
531
|
+
It should be granular, reusable Python code for an agent based on the final confirmed plan and the conversation history (user messages, assistant messages, and code executions).
|
|
532
|
+
Produce a set of small, single-purpose functions—typically one function per plan step—plus one top-level orchestrator function that calls the step functions in order to complete the task.
|
|
533
|
+
|
|
534
|
+
Rules-
|
|
535
|
+
- Do NOT include the searching and loading of functions. Assume required functions have already been loaded. Include imports you need.
|
|
536
|
+
- Your response must be **ONLY Python code**. No markdown or explanations.
|
|
537
|
+
- Define multiple top-level functions:
|
|
538
|
+
1) One small, clear function for each plan step (as granular as practical), with an underscore as the first character in its name.
|
|
539
|
+
2) One top-level orchestrator function that calls the step functions in sequence to achieve the plan objectives.
|
|
540
|
+
- The orchestrator function's parameters **must exactly match the external variables** in the agent plan (the ones marked with backticks `` `variable_name` ``). Provide defaults exactly as specified in the plan when present. Variables in italics (i.e. enclosed in *...*) are internal and must not be orchestrator parameters.
|
|
541
|
+
- The orchestrator function MUST be declared with `def` or `async def` and be directly runnable with a single Python command (e.g., `image_generator(...)`). If it is async, assume the caller will `await` it.
|
|
542
|
+
- NEVER use asyncio or asyncio.run(). The code is executed in a ipython environment, so using await is enough.
|
|
543
|
+
- Step functions should accept only the inputs they need, return explicit outputs, and pass intermediate results forward via return values—not globals.
|
|
544
|
+
- Name functions in snake_case derived from their purpose/step. Use keyword arguments in calls; avoid positional-only calls.
|
|
545
|
+
- Keep the code self-contained and executable. Put imports at the top of the code. Do not nest functions unless strictly necessary.
|
|
546
|
+
- If previously executed code snippets exist, adapt and reuse their validated logic inside the appropriate step functions.
|
|
547
|
+
- Do not print the final output; return it from the orchestrator.
|
|
548
|
+
|
|
549
|
+
Example:
|
|
550
|
+
|
|
551
|
+
If the plan has:
|
|
552
|
+
|
|
553
|
+
"steps": [
|
|
554
|
+
"Receive creative description as image_prompt",
|
|
555
|
+
"Generate image using Gemini with style(default = 'comic')",
|
|
556
|
+
"Save temporary image internally as *temp_file_path*",
|
|
557
|
+
"Upload *temp_file_path* to OneDrive folder onedrive_parent_folder(default = 'root')"
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
Then the functions should look like:
|
|
561
|
+
|
|
562
|
+
```python
|
|
563
|
+
from typing import Dict
|
|
564
|
+
|
|
565
|
+
def _generate_image(image_prompt: str, style: str = "comic") -> Dict:
|
|
566
|
+
# previously validated code to call Gemini
|
|
567
|
+
...
|
|
568
|
+
|
|
569
|
+
def _save_temp_image(image_result: Dict) -> str:
|
|
570
|
+
# previously validated code to write bytes to a temp file
|
|
571
|
+
...
|
|
572
|
+
|
|
573
|
+
def _upload_to_onedrive(temp_file_path: str, onedrive_parent_folder: str = "root") -> Dict:
|
|
574
|
+
# previously validated code to upload
|
|
575
|
+
...
|
|
576
|
+
|
|
577
|
+
def image_generator(image_prompt: str, style: str = "comic", onedrive_parent_folder: str = "root") -> Dict:
|
|
578
|
+
image_result = generate_image(image_prompt=image_prompt, style=style)
|
|
579
|
+
temp_file_path = save_temp_image(image_result=image_result)
|
|
580
|
+
upload_result = upload_to_onedrive(temp_file_path=temp_file_path, onedrive_parent_folder=onedrive_parent_folder)
|
|
581
|
+
return upload_result
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Use this convention consistently to generate the final agent
|
|
585
|
+
"""
|
|
586
|
+
return agent_name, agent_description, python_code
|
|
587
|
+
return {
|
|
588
|
+
"create_agent_plan": create_agent_plan,
|
|
589
|
+
"save_agent_code": save_agent_code,
|
|
590
|
+
"modify_agent_plan": modify_agent_plan,
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
async def get_valid_tools(tool_ids: list[str], registry: AgentrRegistry) -> tuple[list[str], list[str]]:
|
|
596
|
+
"""For a given list of tool_ids, validates the tools and returns a list of links for the apps that have not been logged in"""
|
|
597
|
+
correct, incorrect = [], []
|
|
598
|
+
connections = await registry.list_connected_apps()
|
|
599
|
+
connected_apps = {connection["app_id"] for connection in connections}
|
|
600
|
+
unconnected = set()
|
|
601
|
+
unconnected_links = []
|
|
602
|
+
app_tool_list: dict[str, set[str]] = {}
|
|
603
|
+
|
|
604
|
+
# Group tool_ids by app for fewer registry calls
|
|
605
|
+
app_to_tools: dict[str, list[tuple[str, str]]] = {}
|
|
606
|
+
for tool_id in tool_ids:
|
|
607
|
+
if "__" not in tool_id:
|
|
608
|
+
incorrect.append(tool_id)
|
|
609
|
+
continue
|
|
610
|
+
app, tool_name = tool_id.split("__", 1)
|
|
611
|
+
app_to_tools.setdefault(app, []).append((tool_id, tool_name))
|
|
612
|
+
|
|
613
|
+
# Fetch all apps concurrently
|
|
614
|
+
async def fetch_tools(app: str):
|
|
615
|
+
try:
|
|
616
|
+
tools_dict = await registry.list_tools(app)
|
|
617
|
+
return app, {tool_unit["name"] for tool_unit in tools_dict}
|
|
618
|
+
except Exception:
|
|
619
|
+
return app, None
|
|
620
|
+
|
|
621
|
+
results = await asyncio.gather(*(fetch_tools(app) for app in app_to_tools))
|
|
622
|
+
|
|
623
|
+
# Build map of available tools per app
|
|
624
|
+
for app, tools in results:
|
|
625
|
+
if tools is not None:
|
|
626
|
+
app_tool_list[app] = tools
|
|
627
|
+
|
|
628
|
+
# Validate tool_ids
|
|
629
|
+
for app, tool_entries in app_to_tools.items():
|
|
630
|
+
available = app_tool_list.get(app)
|
|
631
|
+
if available is None:
|
|
632
|
+
incorrect.extend(tool_id for tool_id, _ in tool_entries)
|
|
633
|
+
continue
|
|
634
|
+
if app not in connected_apps and app not in unconnected:
|
|
635
|
+
unconnected.add(app)
|
|
636
|
+
text = await registry.authorise_app(app_id=app)
|
|
637
|
+
start = text.find(":") + 1
|
|
638
|
+
end = text.find(". R", start)
|
|
639
|
+
url = text[start:end].strip()
|
|
640
|
+
markdown_link = f"[Connect to {app.capitalize()}]({url})"
|
|
641
|
+
unconnected_links.append(markdown_link)
|
|
642
|
+
for tool_id, tool_name in tool_entries:
|
|
643
|
+
if tool_name in available:
|
|
644
|
+
correct.append(tool_id)
|
|
645
|
+
else:
|
|
646
|
+
incorrect.append(tool_id)
|
|
647
|
+
|
|
648
|
+
return correct, unconnected_links
|