universal-mcp-agents 0.1.11__py3-none-any.whl → 0.1.13__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 +17 -19
- universal_mcp/agents/base.py +10 -7
- universal_mcp/agents/{bigtoolcache → bigtool}/__init__.py +2 -2
- universal_mcp/agents/{bigtoolcache → bigtool}/__main__.py +0 -1
- universal_mcp/agents/{bigtoolcache → bigtool}/agent.py +0 -1
- universal_mcp/agents/{bigtoolcache → bigtool}/graph.py +6 -5
- universal_mcp/agents/builder/__main__.py +125 -0
- universal_mcp/agents/builder/builder.py +225 -0
- universal_mcp/agents/builder/prompts.py +173 -0
- universal_mcp/agents/builder/state.py +24 -0
- universal_mcp/agents/cli.py +3 -2
- universal_mcp/agents/codeact/__main__.py +2 -4
- universal_mcp/agents/codeact/agent.py +166 -64
- universal_mcp/agents/codeact/models.py +11 -0
- universal_mcp/agents/codeact/prompts.py +12 -12
- universal_mcp/agents/codeact/sandbox.py +69 -23
- universal_mcp/agents/codeact/state.py +2 -0
- universal_mcp/agents/codeact0/__init__.py +3 -0
- universal_mcp/agents/codeact0/__main__.py +35 -0
- universal_mcp/agents/codeact0/agent.py +136 -0
- universal_mcp/agents/codeact0/config.py +77 -0
- universal_mcp/agents/codeact0/llm_tool.py +379 -0
- universal_mcp/agents/codeact0/prompts.py +156 -0
- universal_mcp/agents/codeact0/sandbox.py +90 -0
- universal_mcp/agents/codeact0/state.py +12 -0
- universal_mcp/agents/codeact0/usecases/1-unsubscribe.yaml +4 -0
- universal_mcp/agents/codeact0/usecases/10-reddit2.yaml +10 -0
- universal_mcp/agents/codeact0/usecases/11-github.yaml +13 -0
- universal_mcp/agents/codeact0/usecases/2-reddit.yaml +27 -0
- universal_mcp/agents/codeact0/usecases/2.1-instructions.md +81 -0
- universal_mcp/agents/codeact0/usecases/2.2-instructions.md +71 -0
- universal_mcp/agents/codeact0/usecases/3-earnings.yaml +4 -0
- universal_mcp/agents/codeact0/usecases/4-maps.yaml +41 -0
- universal_mcp/agents/codeact0/usecases/5-gmailreply.yaml +8 -0
- universal_mcp/agents/codeact0/usecases/6-contract.yaml +6 -0
- universal_mcp/agents/codeact0/usecases/7-overnight.yaml +14 -0
- universal_mcp/agents/codeact0/usecases/8-sheets_chart.yaml +25 -0
- universal_mcp/agents/codeact0/usecases/9-learning.yaml +9 -0
- universal_mcp/agents/codeact0/utils.py +374 -0
- universal_mcp/agents/hil.py +4 -4
- universal_mcp/agents/planner/__init__.py +7 -1
- universal_mcp/agents/react.py +11 -3
- universal_mcp/agents/shared/tool_node.py +1 -34
- universal_mcp/agents/simple.py +12 -2
- universal_mcp/agents/utils.py +17 -0
- universal_mcp/applications/llm/__init__.py +3 -0
- universal_mcp/applications/llm/app.py +158 -0
- universal_mcp/applications/ui/app.py +118 -144
- {universal_mcp_agents-0.1.11.dist-info → universal_mcp_agents-0.1.13.dist-info}/METADATA +1 -1
- universal_mcp_agents-0.1.13.dist-info/RECORD +63 -0
- universal_mcp/agents/bigtool2/__init__.py +0 -67
- universal_mcp/agents/bigtool2/__main__.py +0 -23
- universal_mcp/agents/bigtool2/agent.py +0 -13
- universal_mcp/agents/bigtool2/graph.py +0 -155
- universal_mcp/agents/bigtool2/meta_tools.py +0 -120
- universal_mcp/agents/bigtool2/prompts.py +0 -15
- universal_mcp/agents/bigtoolcache/state.py +0 -27
- universal_mcp/agents/builder.py +0 -204
- universal_mcp_agents-0.1.11.dist-info/RECORD +0 -42
- /universal_mcp/agents/{bigtoolcache → bigtool}/context.py +0 -0
- /universal_mcp/agents/{bigtoolcache → bigtool}/prompts.py +0 -0
- /universal_mcp/agents/{bigtool2 → bigtool}/state.py +0 -0
- /universal_mcp/agents/{bigtoolcache → bigtool}/tools.py +0 -0
- {universal_mcp_agents-0.1.11.dist-info → universal_mcp_agents-0.1.13.dist-info}/WHEEL +0 -0
|
@@ -1,39 +1,85 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import builtins
|
|
3
2
|
import contextlib
|
|
4
3
|
import io
|
|
4
|
+
from collections.abc import Callable
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from loguru import logger
|
|
8
8
|
|
|
9
|
+
from .models import SandboxOutput
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
# Define a whitelist of safe built-in functions
|
|
12
|
+
SAFE_BUILTINS = {
|
|
13
|
+
"abs": abs,
|
|
14
|
+
"all": all,
|
|
15
|
+
"any": any,
|
|
16
|
+
"bool": bool,
|
|
17
|
+
"callable": callable,
|
|
18
|
+
"chr": chr,
|
|
19
|
+
"dict": dict,
|
|
20
|
+
"divmod": divmod,
|
|
21
|
+
"enumerate": enumerate,
|
|
22
|
+
"filter": filter,
|
|
23
|
+
"float": float,
|
|
24
|
+
"getattr": getattr,
|
|
25
|
+
"hasattr": hasattr,
|
|
26
|
+
"hash": hash,
|
|
27
|
+
"id": id,
|
|
28
|
+
"int": int,
|
|
29
|
+
"isinstance": isinstance,
|
|
30
|
+
"iter": iter,
|
|
31
|
+
"len": len,
|
|
32
|
+
"list": list,
|
|
33
|
+
"max": max,
|
|
34
|
+
"min": min,
|
|
35
|
+
"next": next,
|
|
36
|
+
"ord": ord,
|
|
37
|
+
"pow": pow,
|
|
38
|
+
"print": print,
|
|
39
|
+
"range": range,
|
|
40
|
+
"repr": repr,
|
|
41
|
+
"reversed": reversed,
|
|
42
|
+
"round": round,
|
|
43
|
+
"set": set,
|
|
44
|
+
"slice": slice,
|
|
45
|
+
"sorted": sorted,
|
|
46
|
+
"str": str,
|
|
47
|
+
"sum": sum,
|
|
48
|
+
"tuple": tuple,
|
|
49
|
+
"type": type,
|
|
50
|
+
"zip": zip,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def eval_unsafe(
|
|
55
|
+
code: str, _locals: dict[str, Callable], timeout: int = 10
|
|
56
|
+
) -> tuple[SandboxOutput, dict[str, Any]]:
|
|
11
57
|
"""Executes a string of Python code in a sandboxed environment."""
|
|
12
|
-
# Store original keys before execution
|
|
13
58
|
original_keys = set(_locals.keys())
|
|
14
|
-
|
|
15
|
-
|
|
59
|
+
execution_context = _locals.copy()
|
|
60
|
+
execution_context["__builtins__"] = __builtins__ # TODO: Use SAFE_BUILTINS instead of __builtins__
|
|
61
|
+
|
|
62
|
+
stdout_capture = io.StringIO()
|
|
63
|
+
output = SandboxOutput(stdout="")
|
|
64
|
+
|
|
16
65
|
try:
|
|
17
66
|
logger.debug(f"Executing code with timeout {timeout}")
|
|
18
|
-
with contextlib.redirect_stdout(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Run the main async function
|
|
25
|
-
await asyncio.wait_for(_locals["main"](), timeout=timeout)
|
|
67
|
+
with contextlib.redirect_stdout(stdout_capture):
|
|
68
|
+
exec(code, execution_context)
|
|
69
|
+
|
|
70
|
+
if "main" in execution_context and asyncio.iscoroutinefunction(execution_context["main"]):
|
|
71
|
+
return_val = await asyncio.wait_for(execution_context["main"](), timeout=timeout)
|
|
72
|
+
output.return_value = return_val
|
|
26
73
|
else:
|
|
27
|
-
|
|
74
|
+
output.error = "No `async def main()` function found in the script."
|
|
75
|
+
|
|
76
|
+
output.stdout = stdout_capture.getvalue()
|
|
28
77
|
|
|
29
|
-
output = f.getvalue()
|
|
30
|
-
result += output
|
|
31
|
-
if not output:
|
|
32
|
-
result += "<code ran, no output printed to stdout>"
|
|
33
78
|
except Exception as e:
|
|
34
|
-
|
|
79
|
+
output.error = f"{type(e).__name__}: {e}"
|
|
80
|
+
output.stdout = stdout_capture.getvalue()
|
|
81
|
+
|
|
82
|
+
new_keys = set(execution_context.keys()) - original_keys - {"__builtins__"}
|
|
83
|
+
new_vars = {key: execution_context[key] for key in new_keys}
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
new_keys = set(_locals.keys()) - original_keys
|
|
38
|
-
new_vars = {key: _locals[key] for key in new_keys}
|
|
39
|
-
return result, new_vars
|
|
85
|
+
return output, new_vars
|
|
@@ -7,3 +7,5 @@ class CodeActState(MessagesState):
|
|
|
7
7
|
|
|
8
8
|
script: str | None = Field(default=None, description="The Python code script to be executed.")
|
|
9
9
|
sandbox_output: str | None = Field(default=None, description="The output of the Python code script execution.")
|
|
10
|
+
syntax_error: str | None = Field(default=None, description="The syntax error from the last script validation.")
|
|
11
|
+
task_complete: bool = Field(default=False, description="Whether the task is complete.")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
4
|
+
from rich import print
|
|
5
|
+
from universal_mcp.agentr.registry import AgentrRegistry
|
|
6
|
+
|
|
7
|
+
from universal_mcp.agents.codeact0.agent import CodeActAgent
|
|
8
|
+
from universal_mcp.agents.utils import messages_to_list
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
memory = MemorySaver()
|
|
13
|
+
agent = CodeActAgent(
|
|
14
|
+
name="CodeAct Agent",
|
|
15
|
+
instructions="Be very concise in your answers.",
|
|
16
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
17
|
+
tools={"google_mail": ["list_messages"]},
|
|
18
|
+
registry=AgentrRegistry(),
|
|
19
|
+
memory=memory,
|
|
20
|
+
)
|
|
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
|
+
result = await agent.invoke(
|
|
29
|
+
user_input="Fetch unsubscribe links from my Gmail inbox for promo emails I have received in the last 7 days"
|
|
30
|
+
)
|
|
31
|
+
print(messages_to_list(result["messages"]))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Literal, cast
|
|
4
|
+
|
|
5
|
+
from langchain_core.messages import AIMessage, ToolMessage
|
|
6
|
+
from langchain_core.tools import StructuredTool
|
|
7
|
+
from langchain_core.tools import tool as create_tool
|
|
8
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
9
|
+
from langgraph.graph import START, StateGraph
|
|
10
|
+
from langgraph.types import Command, RetryPolicy
|
|
11
|
+
from universal_mcp.tools.registry import ToolRegistry
|
|
12
|
+
from universal_mcp.types import ToolConfig, ToolFormat
|
|
13
|
+
|
|
14
|
+
from universal_mcp.agents.base import BaseAgent
|
|
15
|
+
from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
|
|
16
|
+
from universal_mcp.agents.codeact0.prompts import (
|
|
17
|
+
create_default_prompt,
|
|
18
|
+
)
|
|
19
|
+
from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
|
|
20
|
+
from universal_mcp.agents.codeact0.state import CodeActState
|
|
21
|
+
from universal_mcp.agents.codeact0.utils import filter_retry_on, inject_context
|
|
22
|
+
from universal_mcp.agents.llm import load_chat_model
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CodeActAgent(BaseAgent):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
name: str,
|
|
29
|
+
instructions: str,
|
|
30
|
+
model: str,
|
|
31
|
+
memory: BaseCheckpointSaver | None = None,
|
|
32
|
+
tools: ToolConfig | None = None,
|
|
33
|
+
registry: ToolRegistry | None = None,
|
|
34
|
+
sandbox_timeout: int = 20,
|
|
35
|
+
**kwargs,
|
|
36
|
+
):
|
|
37
|
+
super().__init__(
|
|
38
|
+
name=name,
|
|
39
|
+
instructions=instructions,
|
|
40
|
+
model=model,
|
|
41
|
+
memory=memory,
|
|
42
|
+
**kwargs,
|
|
43
|
+
)
|
|
44
|
+
self.model_instance = load_chat_model(model, thinking=True)
|
|
45
|
+
self.tools_config = tools or {}
|
|
46
|
+
self.registry = registry
|
|
47
|
+
self.eval_fn = eval_unsafe
|
|
48
|
+
self.sandbox_timeout = sandbox_timeout
|
|
49
|
+
self.processed_tools: list[StructuredTool | Callable] = []
|
|
50
|
+
|
|
51
|
+
# TODO(manoj): Use toolformat native instead of langchain
|
|
52
|
+
# TODO(manoj, later): Add better sandboxing
|
|
53
|
+
# Old Nishant TODO s:
|
|
54
|
+
# - Make codeact faster by calling upto API call (this done but should be tested)
|
|
55
|
+
# - Add support for async eval_fn
|
|
56
|
+
# - Throw Error if code snippet is too long (> 1000 characters) and suggest to split it into smaller parts
|
|
57
|
+
# - Multiple models from config
|
|
58
|
+
|
|
59
|
+
async def _build_graph(self):
|
|
60
|
+
exported_tools = []
|
|
61
|
+
if self.tools_config:
|
|
62
|
+
if not self.registry:
|
|
63
|
+
raise ValueError("Tools are configured but no registry is provided")
|
|
64
|
+
# Langchain tools are fine
|
|
65
|
+
exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
|
|
66
|
+
exported_tools.extend([smart_print, data_extractor, ai_classify, call_llm])
|
|
67
|
+
self.processed_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in exported_tools]
|
|
68
|
+
self.instructions, self.tools_context = create_default_prompt(self.processed_tools, self.instructions)
|
|
69
|
+
|
|
70
|
+
def call_model(state: CodeActState) -> Command[Literal["sandbox"]]:
|
|
71
|
+
messages = [{"role": "system", "content": self.instructions}] + state["messages"]
|
|
72
|
+
|
|
73
|
+
# Run the model and potentially loop for reflection
|
|
74
|
+
model_with_tools = self.model_instance.bind_tools(tools=[execute_ipython_cell], tool_choice="auto")
|
|
75
|
+
response = cast(AIMessage, model_with_tools.invoke(messages))
|
|
76
|
+
|
|
77
|
+
if response.tool_calls:
|
|
78
|
+
if len(response.tool_calls) > 1:
|
|
79
|
+
raise Exception("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
|
|
80
|
+
if response.tool_calls[0]["name"] != "execute_ipython_cell":
|
|
81
|
+
raise Exception(
|
|
82
|
+
f"Unexpected tool call: {response.tool_calls[0]['name']}. Expected 'execute_ipython_cell'."
|
|
83
|
+
)
|
|
84
|
+
if (
|
|
85
|
+
response.tool_calls[0]["args"].get("snippet") is None
|
|
86
|
+
or not response.tool_calls[0]["args"]["snippet"].strip()
|
|
87
|
+
):
|
|
88
|
+
raise Exception("Tool call 'execute_ipython_cell' requires a non-empty 'snippet' argument.")
|
|
89
|
+
return Command(goto="sandbox", update={"messages": [response]})
|
|
90
|
+
else:
|
|
91
|
+
return Command(update={"messages": [response]})
|
|
92
|
+
|
|
93
|
+
# If eval_fn is a async, we define async node function.
|
|
94
|
+
if inspect.iscoroutinefunction(self.eval_fn):
|
|
95
|
+
raise ValueError("eval_fn must be a synchronous function, not a coroutine.")
|
|
96
|
+
# async def sandbox(state: StateSchema):
|
|
97
|
+
# existing_context = state.get("context", {})
|
|
98
|
+
# context = {**existing_context, **tools_context}
|
|
99
|
+
# # Execute the script in the sandbox
|
|
100
|
+
# output, new_vars = await eval_fn(state["script"], context)
|
|
101
|
+
# new_context = {**existing_context, **new_vars}
|
|
102
|
+
# return {
|
|
103
|
+
# "messages": [{"role": "user", "content": output}],
|
|
104
|
+
# "context": new_context,
|
|
105
|
+
# }
|
|
106
|
+
else:
|
|
107
|
+
|
|
108
|
+
def sandbox(state: CodeActState) -> Command[Literal["call_model"]]:
|
|
109
|
+
tool_call = state["messages"][-1].tool_calls[0] # type: ignore
|
|
110
|
+
code = tool_call["args"]["snippet"]
|
|
111
|
+
previous_add_context = state.get("add_context", {})
|
|
112
|
+
add_context = inject_context(previous_add_context, self.tools_context)
|
|
113
|
+
existing_context = state.get("context", {})
|
|
114
|
+
context = {**existing_context, **add_context}
|
|
115
|
+
# Execute the script in the sandbox
|
|
116
|
+
output, new_context, new_add_context = self.eval_fn(code, context, previous_add_context)
|
|
117
|
+
return Command(
|
|
118
|
+
goto="call_model",
|
|
119
|
+
update={
|
|
120
|
+
"messages": [
|
|
121
|
+
ToolMessage(
|
|
122
|
+
content=output,
|
|
123
|
+
name=tool_call["name"],
|
|
124
|
+
tool_call_id=tool_call["id"],
|
|
125
|
+
)
|
|
126
|
+
],
|
|
127
|
+
"context": new_context,
|
|
128
|
+
"add_context": new_add_context,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
agent = StateGraph(state_schema=CodeActState)
|
|
133
|
+
agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
|
|
134
|
+
agent.add_node(sandbox)
|
|
135
|
+
agent.add_edge(START, "call_model")
|
|
136
|
+
return agent.compile(checkpointer=self.memory)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Annotated, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
# Literal type for all available usecase filenames
|
|
6
|
+
UseCaseName = Literal[
|
|
7
|
+
" ",
|
|
8
|
+
"1-unsubscribe",
|
|
9
|
+
"2-reddit",
|
|
10
|
+
"2.1-reddit",
|
|
11
|
+
"3-earnings",
|
|
12
|
+
"4-maps",
|
|
13
|
+
"4.1-maps",
|
|
14
|
+
"5-gmailreply",
|
|
15
|
+
"6-contract",
|
|
16
|
+
"7-overnight",
|
|
17
|
+
"8-sheets_chart",
|
|
18
|
+
"9-learning",
|
|
19
|
+
"10-reddit2",
|
|
20
|
+
"11-github",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ContextSchema(BaseModel):
|
|
25
|
+
"""The configuration for the agent."""
|
|
26
|
+
|
|
27
|
+
base_prompt: str = Field(
|
|
28
|
+
default=" ",
|
|
29
|
+
description="The base prompt to use for the agent's interactions. Leave blank if using a JSON prompt from the dropdown.",
|
|
30
|
+
)
|
|
31
|
+
model_provider: Annotated[
|
|
32
|
+
Literal[
|
|
33
|
+
"openai",
|
|
34
|
+
"anthropic",
|
|
35
|
+
"azure_openai",
|
|
36
|
+
"azure_ai",
|
|
37
|
+
"google_vertexai",
|
|
38
|
+
"google_genai",
|
|
39
|
+
"bedrock",
|
|
40
|
+
"bedrock_converse",
|
|
41
|
+
"cohere",
|
|
42
|
+
"fireworks",
|
|
43
|
+
"together",
|
|
44
|
+
"mistralai",
|
|
45
|
+
"huggingface",
|
|
46
|
+
"groq",
|
|
47
|
+
"ollama",
|
|
48
|
+
"google_anthropic_vertex",
|
|
49
|
+
"deepseek",
|
|
50
|
+
"ibm",
|
|
51
|
+
"nvidia",
|
|
52
|
+
"xai",
|
|
53
|
+
"perplexity",
|
|
54
|
+
],
|
|
55
|
+
{"__template_metadata__": {"kind": "provider"}},
|
|
56
|
+
] = Field(
|
|
57
|
+
default="anthropic",
|
|
58
|
+
description="The name of the model provider to use for the agent's main interactions. ",
|
|
59
|
+
)
|
|
60
|
+
model: Annotated[
|
|
61
|
+
Literal[
|
|
62
|
+
"claude-4-sonnet-20250514",
|
|
63
|
+
"claude-sonnet-4@20250514",
|
|
64
|
+
],
|
|
65
|
+
{"__template_metadata__": {"kind": "llm"}},
|
|
66
|
+
] = Field(
|
|
67
|
+
default="claude-4-sonnet-20250514",
|
|
68
|
+
description="The name of the language model to use for the agent's main interactions. ",
|
|
69
|
+
)
|
|
70
|
+
tool_names: list[str] = Field(
|
|
71
|
+
default=[],
|
|
72
|
+
description="The names of the tools to use for the agent's main interactions. Leave blank if using a JSON prompt from the dropdown.",
|
|
73
|
+
)
|
|
74
|
+
json_prompt_name: UseCaseName = Field(
|
|
75
|
+
default=" ",
|
|
76
|
+
description="The name of the JSON prompt to use for the agent's main interactions, instead of providing a base prompt and tool names. ",
|
|
77
|
+
)
|