universal-mcp-agents 0.1.18__tar.gz → 0.1.19__tar.gz
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-0.1.18 → universal_mcp_agents-0.1.19}/PKG-INFO +1 -1
- universal_mcp_agents-0.1.19/bech.py +38 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/pyproject.toml +2 -2
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/__init__.py +3 -1
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/base.py +3 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/cli.py +0 -3
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/__main__.py +0 -6
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/llm_tool.py +1 -103
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/playbook_agent.py +7 -2
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/prompts.py +4 -1
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/tools.py +10 -0
- universal_mcp_agents-0.1.19/src/universal_mcp/agents/sandbox.py +90 -0
- universal_mcp_agents-0.1.19/src/universal_mcp/applications/filesystem/__init__.py +0 -0
- universal_mcp_agents-0.1.19/src/universal_mcp/applications/filesystem/app.py +160 -0
- universal_mcp_agents-0.1.19/src/universal_mcp/applications/llm/__init__.py +3 -0
- universal_mcp_agents-0.1.19/src/universal_mcp/applications/llm/app.py +300 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/uv.lock +69 -69
- universal_mcp_agents-0.1.18/src/universal_mcp/applications/llm/__init__.py +0 -3
- universal_mcp_agents-0.1.18/src/universal_mcp/applications/llm/app.py +0 -158
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/.github/workflows/evals.yml +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/.github/workflows/lint.yml +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/.github/workflows/release-please.yml +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/.github/workflows/tests.yml +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/.gitignore +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/.pre-commit-config.yaml +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/GEMINI.md +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/PROMPTS.md +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/README.md +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/bump_and_release.sh +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/__init__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/dataset.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/datasets/codeact.jsonl +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/datasets/exact.jsonl +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/datasets/tasks.jsonl +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/evaluators.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/prompts.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/run.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/evals/utils.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/tests/test_agents.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/__init__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/__main__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/agent.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/context.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/graph.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/prompts.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/state.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/bigtool/tools.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/builder/__main__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/builder/builder.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/builder/helper.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/builder/prompts.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/builder/state.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/__init__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/__main__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/agent.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/models.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/prompts.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/sandbox.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/state.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact/utils.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/__init__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/agent.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/config.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/langgraph_agent.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/sandbox.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/state.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/codeact0/utils.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/hil.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/llm.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/react.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/shared/__main__.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/shared/prompts.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/shared/tool_node.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/simple.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/utils.py +0 -0
- {universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/applications/ui/app.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from universal_mcp.agentr.registry import AgentrRegistry
|
|
2
|
+
from universal_mcp.agents import get_agent
|
|
3
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
4
|
+
from universal_mcp.agents.utils import messages_to_list
|
|
5
|
+
import time
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
start_time = time.time()
|
|
11
|
+
memory = MemorySaver()
|
|
12
|
+
logger.info(f"Checkpointer: Time consumed: {time.time() - start_time}")
|
|
13
|
+
agent_cls = get_agent("codeact-repl")
|
|
14
|
+
logger.info(f"Get class: Time consumed: {time.time() - start_time}")
|
|
15
|
+
registry = AgentrRegistry()
|
|
16
|
+
logger.info(f"Init Registry: Time consumed: {time.time() - start_time}")
|
|
17
|
+
agent = agent_cls(
|
|
18
|
+
name="CodeAct Agent",
|
|
19
|
+
instructions="Be very concise in your answers.",
|
|
20
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
21
|
+
tools={},
|
|
22
|
+
registry=registry,
|
|
23
|
+
memory=memory,
|
|
24
|
+
)
|
|
25
|
+
logger.info(f"Create agent: Time consumed: {time.time() - start_time}")
|
|
26
|
+
print("Init agent...")
|
|
27
|
+
await agent.ainit()
|
|
28
|
+
logger.info(f"Init Agent: Time consumed: {time.time() - start_time}")
|
|
29
|
+
print("Starting agent...")
|
|
30
|
+
async for e in agent.stream(user_input="hi"):
|
|
31
|
+
logger.info(f"First token: Time consumed: {time.time() - start_time}")
|
|
32
|
+
print(e)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "universal-mcp-agents"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.19"
|
|
10
10
|
description = "Add your description here"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [
|
|
@@ -70,7 +70,7 @@ lint.ignore = [
|
|
|
70
70
|
|
|
71
71
|
[tool.ruff.lint.pylint]
|
|
72
72
|
max-args = 10
|
|
73
|
-
max-statements =
|
|
73
|
+
max-statements = 118
|
|
74
74
|
max-returns = 10
|
|
75
75
|
max-branches = 37
|
|
76
76
|
|
{universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/__init__.py
RENAMED
|
@@ -9,7 +9,9 @@ from universal_mcp.agents.react import ReactAgent
|
|
|
9
9
|
from universal_mcp.agents.simple import SimpleAgent
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def get_agent(
|
|
12
|
+
def get_agent(
|
|
13
|
+
agent_name: Literal["react", "simple", "builder", "bigtool", "codeact-script", "codeact-repl"],
|
|
14
|
+
):
|
|
13
15
|
if agent_name == "react":
|
|
14
16
|
return ReactAgent
|
|
15
17
|
elif agent_name == "simple":
|
{universal_mcp_agents-0.1.18 → universal_mcp_agents-0.1.19}/src/universal_mcp/agents/base.py
RENAMED
|
@@ -49,8 +49,11 @@ class BaseAgent:
|
|
|
49
49
|
run_metadata.update(metadata)
|
|
50
50
|
|
|
51
51
|
run_config = {
|
|
52
|
+
"recursion_limit": 25,
|
|
52
53
|
"configurable": {"thread_id": thread_id},
|
|
53
54
|
"metadata": run_metadata,
|
|
55
|
+
"run_id": thread_id,
|
|
56
|
+
"run_name": self.name,
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
async for event, meta in self._graph.astream(
|
|
@@ -28,9 +28,6 @@ def run(name: str = "react"):
|
|
|
28
28
|
"model": "anthropic/claude-sonnet-4-20250514",
|
|
29
29
|
"registry": AgentrRegistry(client=client),
|
|
30
30
|
"memory": MemorySaver(),
|
|
31
|
-
"tools": {
|
|
32
|
-
"google_mail": ["send_email"],
|
|
33
|
-
},
|
|
34
31
|
}
|
|
35
32
|
agent_cls = get_agent(name)
|
|
36
33
|
agent = agent_cls(name=name, **params)
|
|
@@ -19,12 +19,6 @@ async def main():
|
|
|
19
19
|
memory=memory,
|
|
20
20
|
)
|
|
21
21
|
print("Starting agent...")
|
|
22
|
-
# await agent.ainit()
|
|
23
|
-
# await agent.run_interactive()
|
|
24
|
-
# async for event in agent.stream(
|
|
25
|
-
# user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
|
|
26
|
-
# ):
|
|
27
|
-
# print(event.content, end="")
|
|
28
22
|
result = await agent.invoke(
|
|
29
23
|
user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
|
|
30
24
|
)
|
|
@@ -27,7 +27,7 @@ def smart_print(data: Any) -> None:
|
|
|
27
27
|
Args:
|
|
28
28
|
data: Either a dictionary with string keys, or a list of such dictionaries
|
|
29
29
|
"""
|
|
30
|
-
print(light_copy(data)) # noqa
|
|
30
|
+
print(light_copy(data)) # noqa: T201
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def creative_writer(
|
|
@@ -275,105 +275,3 @@ def data_extractor(
|
|
|
275
275
|
.invoke(prompt)
|
|
276
276
|
)
|
|
277
277
|
return cast(dict[str, Any], response)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
# news_articles_schema = {
|
|
281
|
-
# "type": "object",
|
|
282
|
-
# "properties": {
|
|
283
|
-
# "articles": {
|
|
284
|
-
# "type": "array",
|
|
285
|
-
# "title": "Articles",
|
|
286
|
-
# "description": "List of news articles",
|
|
287
|
-
# "items": {
|
|
288
|
-
# "type": "object",
|
|
289
|
-
# "properties": {
|
|
290
|
-
# "headline": {
|
|
291
|
-
# "type": "string",
|
|
292
|
-
# "title": "Headline",
|
|
293
|
-
# "description": "The headline of the news article"
|
|
294
|
-
# },
|
|
295
|
-
# "url": {
|
|
296
|
-
# "type": "string",
|
|
297
|
-
# "title": "URL",
|
|
298
|
-
# "description": "The URL of the news article"
|
|
299
|
-
# }
|
|
300
|
-
# },
|
|
301
|
-
# "required": ["headline", "url"],
|
|
302
|
-
# }
|
|
303
|
-
# }
|
|
304
|
-
# },
|
|
305
|
-
# "required": ["articles"],
|
|
306
|
-
# }
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# news_articles_schema = {
|
|
310
|
-
# "title": "NewsArticleList",
|
|
311
|
-
# "description": "A list of news articles with headlines and URLs",
|
|
312
|
-
# "type": "object",
|
|
313
|
-
# "properties": {
|
|
314
|
-
# "articles": {
|
|
315
|
-
# "type": "array",
|
|
316
|
-
# "items": {
|
|
317
|
-
# "type": "object",
|
|
318
|
-
# "properties": {
|
|
319
|
-
# "headline": {
|
|
320
|
-
# "type": "string"
|
|
321
|
-
# },
|
|
322
|
-
# "url": {
|
|
323
|
-
# "type": "string"
|
|
324
|
-
# }
|
|
325
|
-
# },
|
|
326
|
-
# "required": ["headline", "url"]
|
|
327
|
-
# }
|
|
328
|
-
# }
|
|
329
|
-
# },
|
|
330
|
-
# "required": ["articles"]
|
|
331
|
-
# }
|
|
332
|
-
# model = init_chat_model(model="claude-4-sonnet-20250514", temperature=0)
|
|
333
|
-
# structured_model = model.with_structured_output(news_articles_schema)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
# class TwitterComment(BaseModel):
|
|
337
|
-
# skip: bool
|
|
338
|
-
# reason: str
|
|
339
|
-
# comment: str
|
|
340
|
-
|
|
341
|
-
# twitter_comment_schema = {
|
|
342
|
-
# "title": "TwitterComment",
|
|
343
|
-
# "description": "A twitter comment to engage with followers",
|
|
344
|
-
# "type": "object",
|
|
345
|
-
# "properties": {
|
|
346
|
-
# "skip": {
|
|
347
|
-
# "type": "boolean"
|
|
348
|
-
# },
|
|
349
|
-
# "reason": {
|
|
350
|
-
# "type": "string"
|
|
351
|
-
# },
|
|
352
|
-
# "comment": {
|
|
353
|
-
# "type": "string"
|
|
354
|
-
# },
|
|
355
|
-
# "tagged_profiles": {
|
|
356
|
-
# "type": "array",
|
|
357
|
-
# "items": {
|
|
358
|
-
# "type": "string"
|
|
359
|
-
# }
|
|
360
|
-
# }
|
|
361
|
-
# },
|
|
362
|
-
# "required": ["skip", "reason"]
|
|
363
|
-
# }
|
|
364
|
-
|
|
365
|
-
# comment = {
|
|
366
|
-
# "tweet_id": "08109402",
|
|
367
|
-
# "handle": "@iamnishant",
|
|
368
|
-
# "text": "Hey really loved this tweet! Well said 💯"
|
|
369
|
-
# }
|
|
370
|
-
|
|
371
|
-
# comment_instructions = (
|
|
372
|
-
# "Goal is to engage with my twitter followers who have commented on my tweets."
|
|
373
|
-
# "Please generate a single line, context-aware, conversational reply for the given comment."
|
|
374
|
-
# "- Use social media language (can use hinglish)."
|
|
375
|
-
# "- Skip the reply, if the comment is too generic."
|
|
376
|
-
# "- Also tag relevant people in the reply."
|
|
377
|
-
# )
|
|
378
|
-
|
|
379
|
-
# my_reply = call_llm(comment_instructions, comment, twitter_comment_schema)
|
|
@@ -81,7 +81,7 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
81
81
|
memory=memory,
|
|
82
82
|
**kwargs,
|
|
83
83
|
)
|
|
84
|
-
self.model_instance = load_chat_model(model
|
|
84
|
+
self.model_instance = load_chat_model(model)
|
|
85
85
|
self.tools_config = tools or []
|
|
86
86
|
self.registry = registry
|
|
87
87
|
self.playbook_registry = playbook_registry
|
|
@@ -184,8 +184,13 @@ class CodeActPlaybookAgent(BaseAgent):
|
|
|
184
184
|
ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {links} "
|
|
185
185
|
elif tool_call["name"] == "search_functions":
|
|
186
186
|
tool_result = await meta_tools["search_functions"].ainvoke(tool_call["args"])
|
|
187
|
+
else:
|
|
188
|
+
raise Exception(
|
|
189
|
+
f"Unexpected tool call: {tool_call['name']}. "
|
|
190
|
+
"tool calls must be one of 'enter_playbook_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'"
|
|
191
|
+
)
|
|
187
192
|
except Exception as e:
|
|
188
|
-
tool_result =
|
|
193
|
+
tool_result = str(e)
|
|
189
194
|
|
|
190
195
|
tool_message = ToolMessage(
|
|
191
196
|
content=json.dumps(tool_result),
|
|
@@ -13,7 +13,10 @@ Your job is to answer the user's question or perform the task they ask for.
|
|
|
13
13
|
- 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.
|
|
14
14
|
- For task requiring operations or access to external resources, you should achieve the task by executing Python code snippets.
|
|
15
15
|
- You have access to `execute_ipython_cell` tool that allows you to execute Python code in an IPython notebook cell.
|
|
16
|
-
- You also have access to two tools for finding and loading more python functions- `search_functions` and `load_functions`, which you must use for finding functions for using different external applications.
|
|
16
|
+
- You also have access to two tools for finding and loading more python functions- `search_functions` and `load_functions`, which you must use for finding functions for using different external applications.
|
|
17
|
+
- Prefer pre-loaded or functions already available when possible.
|
|
18
|
+
- Prioritize connected applications over unconnected ones from the output of `search_functions`.
|
|
19
|
+
- When multiple apps are connected, or none of the apps are connected, ask the user to choose the application(s).
|
|
17
20
|
- 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. Avoid hardcoded approaches to classification, data extraction.
|
|
18
21
|
- 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.
|
|
19
22
|
- 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.
|
|
@@ -55,6 +55,9 @@ def create_meta_tools(tool_registry: ToolRegistry) -> dict[str, Any]:
|
|
|
55
55
|
|
|
56
56
|
# Build result string efficiently
|
|
57
57
|
result_parts = []
|
|
58
|
+
apps_in_results = set(app_tools.keys())
|
|
59
|
+
connected_apps_in_results = apps_in_results.intersection(connected_apps)
|
|
60
|
+
|
|
58
61
|
for app, tools in app_tools.items():
|
|
59
62
|
app_status = "connected" if app in connected_apps else "NOT connected"
|
|
60
63
|
result_parts.append(f"Tools from {app} (status: {app_status} by user):")
|
|
@@ -63,6 +66,13 @@ def create_meta_tools(tool_registry: ToolRegistry) -> dict[str, Any]:
|
|
|
63
66
|
result_parts.append(f" - {tool}")
|
|
64
67
|
result_parts.append("") # Empty line between apps
|
|
65
68
|
|
|
69
|
+
# Add connection status information
|
|
70
|
+
if len(connected_apps_in_results) == 0 and len(apps_in_results) > 0:
|
|
71
|
+
result_parts.append("Connection Status: None of the apps in the results are connected. You must ask the user to choose the application.")
|
|
72
|
+
elif len(connected_apps_in_results) > 1:
|
|
73
|
+
connected_list = ", ".join(connected_apps_in_results)
|
|
74
|
+
result_parts.append(f"Connection Status: Multiple apps are connected ({connected_list}). You must ask the user to select which application they want to use.")
|
|
75
|
+
|
|
66
76
|
result_parts.append("Call load_functions to select the required functions only.")
|
|
67
77
|
return "\n".join(result_parts)
|
|
68
78
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import inspect
|
|
3
|
+
import io
|
|
4
|
+
import queue
|
|
5
|
+
import re
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import types
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from universal_mcp.agents.codeact0.utils import derive_context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Sandbox:
|
|
15
|
+
"""
|
|
16
|
+
A class to execute code safely in a sandboxed environment with a timeout.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, timeout: int = 180):
|
|
20
|
+
"""
|
|
21
|
+
Initializes the Sandbox.
|
|
22
|
+
Args:
|
|
23
|
+
timeout: The timeout for code execution in seconds.
|
|
24
|
+
"""
|
|
25
|
+
self.timeout = timeout
|
|
26
|
+
self._locals: dict[str, Any] = {}
|
|
27
|
+
self.add_context: dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
def run(self, code: str) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
30
|
+
"""
|
|
31
|
+
Execute code safely with a timeout.
|
|
32
|
+
- Returns (output_str, filtered_locals_dict, new_add_context)
|
|
33
|
+
- Errors or timeout are returned as output_str.
|
|
34
|
+
- Previous variables in _locals persist across calls.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
EXCLUDE_TYPES = (
|
|
38
|
+
types.ModuleType,
|
|
39
|
+
type(re.match("", "")),
|
|
40
|
+
type(threading.Lock()),
|
|
41
|
+
type(threading.RLock()),
|
|
42
|
+
threading.Event,
|
|
43
|
+
threading.Condition,
|
|
44
|
+
threading.Semaphore,
|
|
45
|
+
queue.Queue,
|
|
46
|
+
socket.socket,
|
|
47
|
+
io.IOBase,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
result_container = {"output": "<no output>"}
|
|
51
|
+
|
|
52
|
+
def target():
|
|
53
|
+
try:
|
|
54
|
+
with contextlib.redirect_stdout(io.StringIO()) as f:
|
|
55
|
+
exec(code, self._locals, self._locals)
|
|
56
|
+
result_container["output"] = f.getvalue() or "<code ran, no output printed to stdout>"
|
|
57
|
+
except Exception as e:
|
|
58
|
+
result_container["output"] = "Error during execution: " + str(e)
|
|
59
|
+
|
|
60
|
+
thread = threading.Thread(target=target)
|
|
61
|
+
thread.start()
|
|
62
|
+
thread.join(self.timeout)
|
|
63
|
+
|
|
64
|
+
if thread.is_alive():
|
|
65
|
+
result_container["output"] = f"Code timeout: code execution exceeded {self.timeout} seconds."
|
|
66
|
+
|
|
67
|
+
# Filter locals for picklable/storable variables
|
|
68
|
+
all_vars = {}
|
|
69
|
+
for key, value in self._locals.items():
|
|
70
|
+
if key == "__builtins__":
|
|
71
|
+
continue
|
|
72
|
+
if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
|
|
73
|
+
continue
|
|
74
|
+
if inspect.isasyncgen(value) or inspect.isasyncgenfunction(value):
|
|
75
|
+
continue
|
|
76
|
+
if isinstance(value, EXCLUDE_TYPES):
|
|
77
|
+
continue
|
|
78
|
+
if not callable(value) or not hasattr(value, "__name__"):
|
|
79
|
+
all_vars[key] = value
|
|
80
|
+
|
|
81
|
+
self._locals = all_vars
|
|
82
|
+
|
|
83
|
+
# Safely derive context
|
|
84
|
+
try:
|
|
85
|
+
self.add_context = derive_context(code, self.add_context)
|
|
86
|
+
except Exception:
|
|
87
|
+
# Keep the old context if derivation fails
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
return result_container["output"], self._locals, self.add_context
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from universal_mcp.applications.application import BaseApplication
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileSystemApp(BaseApplication):
|
|
12
|
+
"""
|
|
13
|
+
A class to safely interact with the filesystem within a specified working directory.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, working_dir: str | None = None, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Initializes the FileSystemApp with a working directory.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
working_dir: The absolute path to the directory where all operations will be performed.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(name="Filesystem")
|
|
24
|
+
|
|
25
|
+
self.set_working_dir(working_dir or f"/tmp/{uuid.uuid4()}")
|
|
26
|
+
|
|
27
|
+
def set_working_dir(self, working_dir: str):
|
|
28
|
+
self.working_dir = pathlib.Path(working_dir).absolute()
|
|
29
|
+
# Create dir if not exists
|
|
30
|
+
self.working_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
def _is_safe_path(self, path: str) -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Checks if the given path is within the working directory.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
path: The path to check.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if the path is safe, False otherwise.
|
|
41
|
+
"""
|
|
42
|
+
common_path = os.path.commonpath([self.working_dir, path])
|
|
43
|
+
return common_path == str(self.working_dir)
|
|
44
|
+
|
|
45
|
+
def create_file(self, path: str, content: str = "") -> None:
|
|
46
|
+
"""
|
|
47
|
+
Creates a file with the given content.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: The relative path to the file to create.
|
|
51
|
+
content: The content to write to the file.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the path is outside the working directory.
|
|
55
|
+
"""
|
|
56
|
+
if not self._is_safe_path(path):
|
|
57
|
+
error = f"Path is outside the working directory: {path} vs {self.working_dir}"
|
|
58
|
+
logger.error(error)
|
|
59
|
+
raise ValueError(error)
|
|
60
|
+
|
|
61
|
+
full_path = os.path.join(self.working_dir, path)
|
|
62
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
63
|
+
with open(full_path, "w") as f:
|
|
64
|
+
f.write(content)
|
|
65
|
+
|
|
66
|
+
def read_file(self, path: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Reads the content of a file.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: The relative path to the file to read.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The content of the file.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If the path is outside the working directory.
|
|
78
|
+
FileNotFoundError: If the file does not exist.
|
|
79
|
+
"""
|
|
80
|
+
if not self._is_safe_path(path):
|
|
81
|
+
raise ValueError("Path is outside the working directory.")
|
|
82
|
+
|
|
83
|
+
full_path = os.path.join(self.working_dir, path)
|
|
84
|
+
if not os.path.exists(full_path):
|
|
85
|
+
raise FileNotFoundError(f"File not found: {full_path}")
|
|
86
|
+
|
|
87
|
+
with open(full_path) as f:
|
|
88
|
+
return f.read()
|
|
89
|
+
|
|
90
|
+
def list_files(self, path: str = ".", recursive: bool = False) -> list[str]:
|
|
91
|
+
"""
|
|
92
|
+
Lists files in a directory.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
path: The relative path to the directory to list.
|
|
96
|
+
recursive: Whether to list files recursively.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A list of file paths.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If the path is outside the working directory.
|
|
103
|
+
"""
|
|
104
|
+
if not self._is_safe_path(path):
|
|
105
|
+
raise ValueError("Path is outside the working directory.")
|
|
106
|
+
|
|
107
|
+
full_path = os.path.join(self.working_dir, path)
|
|
108
|
+
if not os.path.isdir(full_path):
|
|
109
|
+
raise ValueError(f"Path '{path}' is not a directory.")
|
|
110
|
+
|
|
111
|
+
files = []
|
|
112
|
+
if recursive:
|
|
113
|
+
for root, _, filenames in os.walk(full_path):
|
|
114
|
+
for filename in filenames:
|
|
115
|
+
files.append(os.path.relpath(os.path.join(root, filename), self.working_dir))
|
|
116
|
+
else:
|
|
117
|
+
for item in os.listdir(full_path):
|
|
118
|
+
item_path = os.path.join(full_path, item)
|
|
119
|
+
if os.path.isfile(item_path):
|
|
120
|
+
files.append(os.path.relpath(item_path, self.working_dir))
|
|
121
|
+
return files
|
|
122
|
+
|
|
123
|
+
def grep(self, pattern: str, path: str = ".", file_pattern: str = "*") -> list[str]:
|
|
124
|
+
"""
|
|
125
|
+
Searches for a pattern in files.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
pattern: The regex pattern to search for.
|
|
129
|
+
path: The relative path to the directory to search in.
|
|
130
|
+
file_pattern: A glob pattern to filter files to search.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A list of strings with "file:line_number:line" for each match.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If the path is outside the working directory.
|
|
137
|
+
"""
|
|
138
|
+
if not self._is_safe_path(path):
|
|
139
|
+
raise ValueError("Path is outside the working directory.")
|
|
140
|
+
|
|
141
|
+
full_path = os.path.join(self.working_dir, path)
|
|
142
|
+
if not os.path.isdir(full_path):
|
|
143
|
+
raise ValueError(f"Path '{path}' is not a directory.")
|
|
144
|
+
|
|
145
|
+
matches = []
|
|
146
|
+
for root, _, filenames in os.walk(full_path):
|
|
147
|
+
for filename in fnmatch.filter(filenames, file_pattern):
|
|
148
|
+
file_path = os.path.join(root, filename)
|
|
149
|
+
try:
|
|
150
|
+
with open(file_path, errors="ignore") as f:
|
|
151
|
+
for i, line in enumerate(f, 1):
|
|
152
|
+
if re.search(pattern, line):
|
|
153
|
+
relative_path = os.path.relpath(file_path, self.working_dir)
|
|
154
|
+
matches.append(f"{relative_path}:{i}:{line.strip()}")
|
|
155
|
+
except OSError:
|
|
156
|
+
continue # Skip files that can't be opened
|
|
157
|
+
return matches
|
|
158
|
+
|
|
159
|
+
def list_tools(self):
|
|
160
|
+
return [self.create_file, self.grep, self.list_files, self.read_file]
|