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.
- universal_mcp/agents/__init__.py +1 -1
- universal_mcp/agents/base.py +2 -1
- universal_mcp/agents/bigtool/__main__.py +4 -3
- universal_mcp/agents/bigtool/agent.py +1 -0
- universal_mcp/agents/bigtool/graph.py +7 -4
- universal_mcp/agents/bigtool/tools.py +4 -5
- universal_mcp/agents/builder/__main__.py +49 -23
- universal_mcp/agents/builder/builder.py +101 -102
- universal_mcp/agents/builder/helper.py +4 -6
- universal_mcp/agents/builder/prompts.py +92 -39
- universal_mcp/agents/builder/state.py +1 -1
- universal_mcp/agents/codeact0/__init__.py +2 -1
- universal_mcp/agents/codeact0/agent.py +12 -5
- universal_mcp/agents/codeact0/langgraph_agent.py +11 -14
- universal_mcp/agents/codeact0/llm_tool.py +2 -2
- universal_mcp/agents/codeact0/playbook_agent.py +364 -0
- universal_mcp/agents/codeact0/prompts.py +113 -39
- universal_mcp/agents/codeact0/sandbox.py +43 -32
- universal_mcp/agents/codeact0/state.py +29 -3
- universal_mcp/agents/codeact0/tools.py +186 -0
- universal_mcp/agents/codeact0/utils.py +53 -18
- universal_mcp/agents/shared/__main__.py +3 -2
- universal_mcp/agents/shared/prompts.py +1 -1
- universal_mcp/agents/shared/tool_node.py +17 -12
- universal_mcp/agents/utils.py +36 -12
- {universal_mcp_agents-0.1.14.dist-info → universal_mcp_agents-0.1.16.dist-info}/METADATA +3 -3
- universal_mcp_agents-0.1.16.dist-info/RECORD +50 -0
- universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +0 -10
- universal_mcp/agents/codeact0/usecases/11-github.yaml +0 -14
- universal_mcp/agents/codeact0/usecases/2-reddit.yaml +0 -27
- universal_mcp/agents/codeact0/usecases/2.1-instructions.md +0 -81
- universal_mcp/agents/codeact0/usecases/2.2-instructions.md +0 -71
- universal_mcp/agents/codeact0/usecases/3-earnings.yaml +0 -4
- universal_mcp/agents/codeact0/usecases/4-maps.yaml +0 -41
- universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +0 -8
- universal_mcp/agents/codeact0/usecases/6-contract.yaml +0 -6
- universal_mcp/agents/codeact0/usecases/7-overnight.yaml +0 -14
- universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +0 -25
- universal_mcp/agents/codeact0/usecases/9-learning.yaml +0 -9
- universal_mcp/agents/planner/__init__.py +0 -51
- universal_mcp/agents/planner/__main__.py +0 -28
- universal_mcp/agents/planner/graph.py +0 -85
- universal_mcp/agents/planner/prompts.py +0 -14
- universal_mcp/agents/planner/state.py +0 -11
- universal_mcp_agents-0.1.14.dist-info/RECORD +0 -66
- {universal_mcp_agents-0.1.14.dist-info → universal_mcp_agents-0.1.16.dist-info}/WHEEL +0 -0
|
@@ -1,33 +1,112 @@
|
|
|
1
|
-
uneditable_prompt = """
|
|
2
|
-
You are Wingmen, an AI Assistant created by AgentR. You are a creative, straight-forward and direct principal software engineer.
|
|
3
|
-
|
|
4
|
-
Your job is to answer the user's question or perform the task they ask for.
|
|
5
|
-
- 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.
|
|
6
|
-
- For task requiring operations or access to external resources, you should achieve the task by executing Python code snippets.
|
|
7
|
-
- You have access to `execute_ipython_cell` tool that allows you to execute Python code in an IPython notebook cell.
|
|
8
|
-
- In writing or natural language processing tasks DO NOT answer directly. Instead use `execute_ipython_cell` tool with the AI functions provided to you for tasks like summarizing, text generation, classification, data extraction from text or unstructured data, etc.
|
|
9
|
-
- The code you write will be executed in a sandbox environment, and you can use the output of previous executions in your code.
|
|
10
|
-
- 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.
|
|
11
|
-
- If needed, feel free to ask for more information from the user (without using the execute_ipython_cell tool) to clarify the task.
|
|
12
|
-
|
|
13
|
-
GUIDELINES for writing code:
|
|
14
|
-
- Variables defined at the top level of previous code snippets can be referenced in your code.
|
|
15
|
-
- 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.
|
|
16
|
-
- 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.
|
|
17
|
-
- In a single code snippet, try to achieve as much as possible.
|
|
18
|
-
- You can only import libraries that come pre-installed with Python.
|
|
19
|
-
- You must wrap await calls in an async function and call it using `asyncio.run`.
|
|
20
|
-
- 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.
|
|
21
|
-
|
|
22
|
-
"""
|
|
23
1
|
import inspect
|
|
24
2
|
import re
|
|
25
3
|
from collections.abc import Sequence
|
|
26
|
-
from datetime import datetime
|
|
27
4
|
|
|
28
5
|
from langchain_core.tools import StructuredTool
|
|
6
|
+
|
|
29
7
|
from universal_mcp.agents.codeact0.utils import schema_to_signature
|
|
30
8
|
|
|
9
|
+
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.
|
|
11
|
+
|
|
12
|
+
## Responsibilities
|
|
13
|
+
|
|
14
|
+
- **Answer directly** if the task is simple (e.g. print, math, general knowledge).
|
|
15
|
+
- For any task requiring logic, execution, or data handling, use `execute_ipython_cell`.
|
|
16
|
+
- For writing or NLP tasks (summarizing, generating, extracting), always use AI functions via code — never respond directly.
|
|
17
|
+
|
|
18
|
+
## Tool vs. Function: Required Separation
|
|
19
|
+
|
|
20
|
+
You must clearly distinguish between tools (called via the tool calling API) and internal functions (used inside code blocks).
|
|
21
|
+
|
|
22
|
+
### Tools — Must Be Called via Tool Calling API
|
|
23
|
+
|
|
24
|
+
These must be called using **tool calling**, not from inside code blocks:
|
|
25
|
+
|
|
26
|
+
- `execute_ipython_cell` — For running any Python code or logic.
|
|
27
|
+
- `search_functions` — To discover available functions for a task.
|
|
28
|
+
- `load_functions` — To load a specific function by full ID.
|
|
29
|
+
|
|
30
|
+
**Do not attempt to call these inside `python` code.**
|
|
31
|
+
Use tool calling syntax for these operations.
|
|
32
|
+
|
|
33
|
+
### Functions — Must Be Used Inside Code Blocks
|
|
34
|
+
|
|
35
|
+
All other functions, including LLM functions, must always be used within code executed by `execute_ipython_cell`. These include:
|
|
36
|
+
|
|
37
|
+
- `smart_print()` — For inspecting unknown data structures before looping.
|
|
38
|
+
- `asyncio.run()` — For wrapping and executing asynchronous logic. You must not use await outside an async function. And the async function must be called by `asyncio.run()`.
|
|
39
|
+
- Any functions for applications loaded via `load_functions`.
|
|
40
|
+
- Any logic, data handling, writing, NLP, generation, summarization, or extraction functionality of LLMs.
|
|
41
|
+
|
|
42
|
+
These must be called **inside a Python code block**, and that block must be executed using `execute_ipython_cell`.
|
|
43
|
+
|
|
44
|
+
## Tool/Function Usage Policy
|
|
45
|
+
|
|
46
|
+
1. **Always Use Tools/Functions for Required Tasks**
|
|
47
|
+
Any searching, loading, or executing must be done using a tool/function call. Never answer manually if a tool/function is appropriate.
|
|
48
|
+
|
|
49
|
+
2. **Use Existing Functions First**
|
|
50
|
+
Use existing functions if available. Otherwise, use `search_functions` with a concise query describing the task.
|
|
51
|
+
|
|
52
|
+
3. **Load Only Relevant Tools**
|
|
53
|
+
When calling `load_functions`, include only relevant function IDs.
|
|
54
|
+
- Prefer connected applications over unconnected ones.
|
|
55
|
+
- If multiple functions match (i.e. if none are connected, or multiple are connected), ask the user to choose.
|
|
56
|
+
- After loading a tool, you do not need to import/declare it again. It can be called directly in further cells.
|
|
57
|
+
|
|
58
|
+
4. **Follow First Turn Process Strictly**
|
|
59
|
+
On the **first turn**, do only **one** of the following:
|
|
60
|
+
- Handle directly (if trivial)
|
|
61
|
+
- Use a tool/function (`execute_ipython_cell`, `search_functions`, etc.)
|
|
62
|
+
|
|
63
|
+
**Do not extend the conversation on the first message.**
|
|
64
|
+
|
|
65
|
+
## Coding Rules
|
|
66
|
+
|
|
67
|
+
- Use `smart_print()` to inspect unknown structures, especially those received from function outputs, before looping or branching.
|
|
68
|
+
- Validate logic with a single item before processing lists or large inputs.
|
|
69
|
+
- Try to achieve as much as possible in a single code block.
|
|
70
|
+
- Use only pre-installed Python libraries. Do import them once before using.
|
|
71
|
+
- Outer level functions, variables, classes, and imports declared previously can be used in later cells.
|
|
72
|
+
- For all functions, call using keyword arguments only. DO NOT use any positional arguments.
|
|
73
|
+
|
|
74
|
+
### **Async Function Usage — Critical**
|
|
75
|
+
|
|
76
|
+
When calling asynchronous functions:
|
|
77
|
+
- You must define or use an **inner async function**.
|
|
78
|
+
- Use `await` only **inside** that async function.
|
|
79
|
+
- Run it using `asyncio.run(<function_name>())` **without** `await` at the outer level.
|
|
80
|
+
|
|
81
|
+
**Wrong - Using `await` outside an async function**
|
|
82
|
+
```
|
|
83
|
+
result = await some_async_function()
|
|
84
|
+
```
|
|
85
|
+
**Wrong - Attaching await before asyncio.run**.
|
|
86
|
+
`await asyncio.run(main())`
|
|
87
|
+
These will raise SyntaxError: 'await' outside async function
|
|
88
|
+
The correct method is the following-
|
|
89
|
+
```
|
|
90
|
+
import asyncio
|
|
91
|
+
async def some_async_function():
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
result = await some_async_function()
|
|
96
|
+
print(result)
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
#or
|
|
100
|
+
result = asyncio.run(some_async_function(arg1 = <arg1>))
|
|
101
|
+
```
|
|
102
|
+
## Output Formatting
|
|
103
|
+
- All code results must be returned in **Markdown**.
|
|
104
|
+
- The user cannot see raw output, so format results clearly:
|
|
105
|
+
- Use tables for structured data.
|
|
106
|
+
- Provide links for files or images.
|
|
107
|
+
- Be explicit in formatting to ensure readability.
|
|
108
|
+
"""
|
|
109
|
+
|
|
31
110
|
|
|
32
111
|
def make_safe_function_name(name: str) -> str:
|
|
33
112
|
"""Convert a tool name to a valid Python function name."""
|
|
@@ -119,24 +198,22 @@ def indent(text, prefix, predicate=None):
|
|
|
119
198
|
return "".join(prefixed_lines)
|
|
120
199
|
|
|
121
200
|
|
|
122
|
-
|
|
123
201
|
def create_default_prompt(
|
|
124
202
|
tools: Sequence[StructuredTool],
|
|
125
|
-
additional_tools
|
|
203
|
+
additional_tools: Sequence[StructuredTool],
|
|
126
204
|
base_prompt: str | None = None,
|
|
127
205
|
):
|
|
128
|
-
system_prompt = uneditable_prompt.strip()
|
|
129
|
-
"\n\nIn addition to the Python Standard Library, you can use the following external functions
|
|
130
|
-
"\n"
|
|
206
|
+
system_prompt = uneditable_prompt.strip() + (
|
|
207
|
+
"\n\nIn addition to the Python Standard Library, you can use the following external functions:\n"
|
|
131
208
|
)
|
|
132
209
|
tools_context = {}
|
|
133
210
|
for tool in tools:
|
|
134
|
-
if hasattr(tool, "
|
|
135
|
-
tool_callable = tool.coroutine
|
|
136
|
-
is_async = True
|
|
137
|
-
elif hasattr(tool, "func") and tool.func is not None:
|
|
211
|
+
if hasattr(tool, "func") and tool.func is not None:
|
|
138
212
|
tool_callable = tool.func
|
|
139
213
|
is_async = False
|
|
214
|
+
elif hasattr(tool, "coroutine") and tool.coroutine is not None:
|
|
215
|
+
tool_callable = tool.coroutine
|
|
216
|
+
is_async = True
|
|
140
217
|
system_prompt += f'''{"async " if is_async else ""}{schema_to_signature(tool.args, tool.name)}:
|
|
141
218
|
"""{tool.description}"""
|
|
142
219
|
...
|
|
@@ -145,12 +222,12 @@ def create_default_prompt(
|
|
|
145
222
|
tools_context[safe_name] = tool_callable
|
|
146
223
|
|
|
147
224
|
for tool in additional_tools:
|
|
148
|
-
if hasattr(tool, "
|
|
149
|
-
tool_callable = tool.coroutine
|
|
150
|
-
is_async = True
|
|
151
|
-
elif hasattr(tool, "func") and tool.func is not None:
|
|
225
|
+
if hasattr(tool, "func") and tool.func is not None:
|
|
152
226
|
tool_callable = tool.func
|
|
153
227
|
is_async = False
|
|
228
|
+
elif hasattr(tool, "coroutine") and tool.coroutine is not None:
|
|
229
|
+
tool_callable = tool.coroutine
|
|
230
|
+
is_async = True
|
|
154
231
|
system_prompt += f'''{"async " if is_async else ""}def {tool.name} {str(inspect.signature(tool_callable))}:
|
|
155
232
|
"""{tool.description}"""
|
|
156
233
|
...
|
|
@@ -162,6 +239,3 @@ def create_default_prompt(
|
|
|
162
239
|
system_prompt += f"Your goal is to perform the following task:\n\n{base_prompt}"
|
|
163
240
|
|
|
164
241
|
return system_prompt, tools_context
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
@@ -14,55 +14,66 @@ from universal_mcp.agents.codeact0.utils import derive_context
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def eval_unsafe(
|
|
17
|
-
code: str, _locals: dict[str, Any], add_context: dict[str, Any]
|
|
17
|
+
code: str, _locals: dict[str, Any], add_context: dict[str, Any], timeout: int = 180
|
|
18
18
|
) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
19
|
-
|
|
19
|
+
"""
|
|
20
|
+
Execute code safely with a timeout.
|
|
21
|
+
- Returns (output_str, filtered_locals_dict, new_add_context)
|
|
22
|
+
- Errors or timeout are returned as output_str.
|
|
23
|
+
- Previous variables in _locals persist across calls.
|
|
24
|
+
"""
|
|
25
|
+
|
|
20
26
|
EXCLUDE_TYPES = (
|
|
21
|
-
types.ModuleType,
|
|
27
|
+
types.ModuleType,
|
|
22
28
|
type(re.match("", "")),
|
|
23
|
-
type(threading.Lock()),
|
|
24
|
-
type(threading.RLock()),
|
|
25
|
-
threading.Event,
|
|
26
|
-
threading.Condition,
|
|
27
|
-
threading.Semaphore,
|
|
28
|
-
queue.Queue,
|
|
29
|
-
socket.socket,
|
|
30
|
-
io.IOBase,
|
|
29
|
+
type(threading.Lock()),
|
|
30
|
+
type(threading.RLock()),
|
|
31
|
+
threading.Event,
|
|
32
|
+
threading.Condition,
|
|
33
|
+
threading.Semaphore,
|
|
34
|
+
queue.Queue,
|
|
35
|
+
socket.socket,
|
|
36
|
+
io.IOBase,
|
|
31
37
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
|
|
39
|
+
result_container = {"output": "<no output>"}
|
|
40
|
+
|
|
41
|
+
def target():
|
|
42
|
+
try:
|
|
43
|
+
with contextlib.redirect_stdout(io.StringIO()) as f:
|
|
44
|
+
exec(code, _locals, _locals)
|
|
45
|
+
result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
|
|
46
|
+
except Exception as e:
|
|
47
|
+
result_container["output"] = "Error during execution: " + str(e)
|
|
48
|
+
|
|
49
|
+
thread = threading.Thread(target=target)
|
|
50
|
+
thread.start()
|
|
51
|
+
thread.join(timeout)
|
|
52
|
+
|
|
53
|
+
if thread.is_alive():
|
|
54
|
+
result_container["output"] = f"Code timeout: code execution exceeded {timeout} seconds."
|
|
55
|
+
|
|
56
|
+
# Filter locals for picklable/storable variables
|
|
45
57
|
all_vars = {}
|
|
46
58
|
for key, value in _locals.items():
|
|
47
59
|
if key == "__builtins__":
|
|
48
60
|
continue
|
|
49
|
-
|
|
50
|
-
# Skip coroutines, async generators, and coroutine functions
|
|
51
61
|
if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
|
|
52
62
|
continue
|
|
53
63
|
if inspect.isasyncgen(value) or inspect.isasyncgenfunction(value):
|
|
54
64
|
continue
|
|
55
|
-
|
|
56
|
-
# Skip "obviously unpicklable" types
|
|
57
65
|
if isinstance(value, EXCLUDE_TYPES):
|
|
58
66
|
continue
|
|
59
|
-
|
|
60
|
-
# Keep if it's not a callable OR if it has no __name__ attribute
|
|
61
67
|
if not callable(value) or not hasattr(value, "__name__"):
|
|
62
68
|
all_vars[key] = value
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
# Safely derive context
|
|
71
|
+
try:
|
|
72
|
+
new_add_context = derive_context(code, add_context)
|
|
73
|
+
except Exception:
|
|
74
|
+
new_add_context = add_context
|
|
75
|
+
|
|
76
|
+
return result_container["output"], all_vars, new_add_context
|
|
66
77
|
|
|
67
78
|
|
|
68
79
|
@tool(parse_docstring=True)
|
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Annotated, Any
|
|
2
2
|
|
|
3
|
-
from langgraph.
|
|
3
|
+
from langgraph.prebuilt.chat_agent_executor import AgentState
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
def _enqueue(left: list, right: list) -> list:
|
|
7
|
+
"""Treat left as a FIFO queue, append new items from right (preserve order),
|
|
8
|
+
keep items unique, and cap total size to 20 (drop oldest items)."""
|
|
9
|
+
max_size = 30
|
|
10
|
+
preferred_size = 20
|
|
11
|
+
if len(right) > preferred_size:
|
|
12
|
+
preferred_size = min(max_size, len(right))
|
|
13
|
+
queue = list(left or [])
|
|
14
|
+
|
|
15
|
+
for item in right[:preferred_size] or []:
|
|
16
|
+
if item in queue:
|
|
17
|
+
queue.remove(item)
|
|
18
|
+
queue.append(item)
|
|
19
|
+
|
|
20
|
+
if len(queue) > preferred_size:
|
|
21
|
+
queue = queue[-preferred_size:]
|
|
22
|
+
|
|
23
|
+
return queue
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CodeActState(AgentState):
|
|
7
27
|
"""State for CodeAct agent."""
|
|
8
28
|
|
|
9
29
|
context: dict[str, Any]
|
|
10
30
|
"""Dictionary containing the execution context with available tools and variables."""
|
|
11
31
|
add_context: dict[str, Any]
|
|
12
32
|
"""Dictionary containing the additional context (functions, classes, imports) to be added to the execution context."""
|
|
33
|
+
playbook_mode: str | None
|
|
34
|
+
"""State for the playbook agent."""
|
|
35
|
+
selected_tool_ids: Annotated[list[str], _enqueue]
|
|
36
|
+
"""Queue for tools exported from registry"""
|
|
37
|
+
plan: str | None
|
|
38
|
+
"""Plan for the playbook agent."""
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from langchain_core.tools import tool
|
|
6
|
+
from universal_mcp.tools.registry import ToolRegistry
|
|
7
|
+
from universal_mcp.types import ToolFormat
|
|
8
|
+
|
|
9
|
+
MAX_LENGHT=100
|
|
10
|
+
|
|
11
|
+
def enter_playbook_mode():
|
|
12
|
+
"""Call this function to enter playbook mode. Playbook mode is when the user wants to store a repeated task as a script with some inputs for the future."""
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
def exit_playbook_mode():
|
|
16
|
+
"""Call this function to exit playbook mode. Playbook mode is when the user wants to store a repeated task as a script with some inputs for the future."""
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_meta_tools(tool_registry: ToolRegistry) -> dict[str, Any]:
|
|
22
|
+
"""Create the meta tools for searching and loading tools"""
|
|
23
|
+
|
|
24
|
+
@tool
|
|
25
|
+
async def search_functions(queries: list[str]) -> str:
|
|
26
|
+
"""Search for relevant functions given list of queries.
|
|
27
|
+
Each single query should be atomic (doable with a single function).
|
|
28
|
+
For tasks requiring multiple functions, add separate queries for each subtask"""
|
|
29
|
+
try:
|
|
30
|
+
# Fetch all connections
|
|
31
|
+
connections = await tool_registry.list_connected_apps()
|
|
32
|
+
connected_apps = {connection["app_id"] for connection in connections}
|
|
33
|
+
|
|
34
|
+
app_tools = defaultdict(set)
|
|
35
|
+
MAX_LENGTH = 20
|
|
36
|
+
|
|
37
|
+
# Process all queries concurrently
|
|
38
|
+
search_tasks = []
|
|
39
|
+
for query in queries:
|
|
40
|
+
search_tasks.append(_search_query_tools(query))
|
|
41
|
+
|
|
42
|
+
query_results = await asyncio.gather(*search_tasks)
|
|
43
|
+
|
|
44
|
+
# Aggregate results with limit per app and automatic deduplication
|
|
45
|
+
for tools_list in query_results:
|
|
46
|
+
for tool in tools_list:
|
|
47
|
+
app = tool["id"].split("__")[0]
|
|
48
|
+
tool_id = tool["id"]
|
|
49
|
+
|
|
50
|
+
# Check if within limit and add to set (automatically deduplicates)
|
|
51
|
+
if len(app_tools[app]) < MAX_LENGTH:
|
|
52
|
+
cleaned_desc = tool["description"].split("Context:")[0].strip()
|
|
53
|
+
app_tools[app].add(f"{tool_id}: {cleaned_desc}")
|
|
54
|
+
|
|
55
|
+
# Build result string efficiently
|
|
56
|
+
result_parts = []
|
|
57
|
+
for app, tools in app_tools.items():
|
|
58
|
+
app_status = "connected" if app in connected_apps else "NOT connected"
|
|
59
|
+
result_parts.append(f"Tools from {app} (status: {app_status} by user):")
|
|
60
|
+
# Convert set to sorted list for consistent output
|
|
61
|
+
for tool in sorted(tools):
|
|
62
|
+
result_parts.append(f" - {tool}")
|
|
63
|
+
result_parts.append("") # Empty line between apps
|
|
64
|
+
|
|
65
|
+
result_parts.append("Call load_functions to select the required functions only.")
|
|
66
|
+
return "\n".join(result_parts)
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
return f"Error: {e}"
|
|
70
|
+
|
|
71
|
+
async def _search_query_tools(query: str) -> list[dict]:
|
|
72
|
+
"""Helper function to search apps and tools for a single query."""
|
|
73
|
+
# Start both searches concurrently
|
|
74
|
+
tools_search_task = tool_registry.search_tools(query, limit=10)
|
|
75
|
+
apps_search_task = tool_registry.search_apps(query, limit=4)
|
|
76
|
+
|
|
77
|
+
# Wait for both to complete
|
|
78
|
+
tools_from_general_search, apps_list = await asyncio.gather(tools_search_task, apps_search_task)
|
|
79
|
+
|
|
80
|
+
# Create tasks for searching tools from each app
|
|
81
|
+
app_tool_tasks = [tool_registry.search_tools(query, limit=5, app_id=app["id"]) for app in apps_list]
|
|
82
|
+
|
|
83
|
+
# Wait for all app-specific tool searches to complete
|
|
84
|
+
app_tools_results = await asyncio.gather(*app_tool_tasks)
|
|
85
|
+
|
|
86
|
+
# Combine all results
|
|
87
|
+
tools_list = list(tools_from_general_search)
|
|
88
|
+
for app_tools in app_tools_results:
|
|
89
|
+
tools_list.extend(app_tools)
|
|
90
|
+
|
|
91
|
+
return tools_list
|
|
92
|
+
|
|
93
|
+
@tool
|
|
94
|
+
async def load_functions(tool_ids: list[str]) -> str:
|
|
95
|
+
"""Load specific functions by their IDs for use in subsequent steps.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
tool_ids: Function ids in the form 'app__function'. Example: 'google_mail__send_email'
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Confirmation message about loaded functions
|
|
102
|
+
"""
|
|
103
|
+
return f"Successfully loaded {len(tool_ids)} functions: {tool_ids}"
|
|
104
|
+
|
|
105
|
+
@tool
|
|
106
|
+
async def web_search(query: str) -> list:
|
|
107
|
+
"""Search the web for the given query and return structured search results.
|
|
108
|
+
|
|
109
|
+
Do not use for app-specific searches (for example, reddit or linkedin searches
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
list: A list of up to 10 search result dictionaries, each containing:
|
|
113
|
+
- id (str): Unique identifier, typically the URL
|
|
114
|
+
- title (str): The title/headline of the search result
|
|
115
|
+
- url (str): The web URL of the result
|
|
116
|
+
- publishedDate (str): ISO 8601 formatted date (e.g., "2025-01-01T00:00:00.000Z")
|
|
117
|
+
- author (str): Author name (may be empty string)
|
|
118
|
+
- summary (str): Text summary/snippet of the content
|
|
119
|
+
- image (str): URL to associated image (if available)
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
results = await web_search(query="python programming")
|
|
123
|
+
"""
|
|
124
|
+
await tool_registry.export_tools(["exa__search_with_filters"], ToolFormat.LANGCHAIN)
|
|
125
|
+
response = await tool_registry.call_tool(
|
|
126
|
+
"exa__search_with_filters", {"query": query, "contents": {"summary": True}}
|
|
127
|
+
)
|
|
128
|
+
return response["results"]
|
|
129
|
+
|
|
130
|
+
return {"search_functions": search_functions, "load_functions": load_functions, "web_search": web_search}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def get_valid_tools(tool_ids: list[str], registry: ToolRegistry) -> tuple[list[str], list[str]]:
|
|
134
|
+
"""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"""
|
|
135
|
+
correct, incorrect = [], []
|
|
136
|
+
connections = await registry.list_connected_apps()
|
|
137
|
+
connected_apps = {connection["app_id"] for connection in connections}
|
|
138
|
+
unconnected = set()
|
|
139
|
+
unconnected_links = []
|
|
140
|
+
app_tool_list: dict[str, set[str]] = {}
|
|
141
|
+
|
|
142
|
+
# Group tool_ids by app for fewer registry calls
|
|
143
|
+
app_to_tools: dict[str, list[tuple[str, str]]] = {}
|
|
144
|
+
for tool_id in tool_ids:
|
|
145
|
+
if "__" not in tool_id:
|
|
146
|
+
incorrect.append(tool_id)
|
|
147
|
+
continue
|
|
148
|
+
app, tool_name = tool_id.split("__", 1)
|
|
149
|
+
app_to_tools.setdefault(app, []).append((tool_id, tool_name))
|
|
150
|
+
|
|
151
|
+
# Fetch all apps concurrently
|
|
152
|
+
async def fetch_tools(app: str):
|
|
153
|
+
try:
|
|
154
|
+
tools_dict = await registry.list_tools(app)
|
|
155
|
+
return app, {tool_unit["name"] for tool_unit in tools_dict}
|
|
156
|
+
except Exception:
|
|
157
|
+
return app, None
|
|
158
|
+
|
|
159
|
+
results = await asyncio.gather(*(fetch_tools(app) for app in app_to_tools))
|
|
160
|
+
|
|
161
|
+
# Build map of available tools per app
|
|
162
|
+
for app, tools in results:
|
|
163
|
+
if tools is not None:
|
|
164
|
+
app_tool_list[app] = tools
|
|
165
|
+
|
|
166
|
+
# Validate tool_ids
|
|
167
|
+
for app, tool_entries in app_to_tools.items():
|
|
168
|
+
available = app_tool_list.get(app)
|
|
169
|
+
if available is None:
|
|
170
|
+
incorrect.extend(tool_id for tool_id, _ in tool_entries)
|
|
171
|
+
continue
|
|
172
|
+
if app not in connected_apps and app not in unconnected:
|
|
173
|
+
unconnected.add(app)
|
|
174
|
+
text = registry.client.get_authorization_url(app)
|
|
175
|
+
start = text.find(":") + 1
|
|
176
|
+
end = text.find(". R", start)
|
|
177
|
+
url = text[start:end].strip()
|
|
178
|
+
markdown_link = f"[{app}]({url})"
|
|
179
|
+
unconnected_links.append(markdown_link)
|
|
180
|
+
for tool_id, tool_name in tool_entries:
|
|
181
|
+
if tool_name in available:
|
|
182
|
+
correct.append(tool_id)
|
|
183
|
+
else:
|
|
184
|
+
incorrect.append(tool_id)
|
|
185
|
+
|
|
186
|
+
return correct, unconnected_links
|