universal-mcp-agents 0.1.10__py3-none-any.whl → 0.1.12__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 +188 -108
- universal_mcp/agents/codeact/models.py +11 -0
- universal_mcp/agents/codeact/prompts.py +34 -43
- universal_mcp/agents/codeact/sandbox.py +78 -40
- universal_mcp/agents/codeact/state.py +5 -4
- 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/langgraph_graph.py +17 -0
- universal_mcp/agents/codeact0/legacy_codeact.py +104 -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/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.10.dist-info → universal_mcp_agents-0.1.12.dist-info}/METADATA +1 -1
- universal_mcp_agents-0.1.12.dist-info/RECORD +65 -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.10.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.10.dist-info → universal_mcp_agents-0.1.12.dist-info}/WHEEL +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import re
|
|
3
|
-
from collections.abc import
|
|
4
|
-
|
|
5
|
-
from langchain_core.tools import StructuredTool
|
|
3
|
+
from collections.abc import Callable
|
|
6
4
|
|
|
7
5
|
|
|
8
6
|
def make_safe_function_name(name: str) -> str:
|
|
@@ -19,41 +17,52 @@ def make_safe_function_name(name: str) -> str:
|
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
def create_default_prompt(
|
|
22
|
-
tools:
|
|
20
|
+
tools: dict[str, Callable],
|
|
23
21
|
base_prompt: str | None = None,
|
|
24
22
|
):
|
|
25
23
|
"""Create default prompt for the CodeAct agent."""
|
|
26
24
|
prompt = f"{base_prompt}\n\n" if base_prompt else ""
|
|
27
|
-
prompt += """You will be given a task to perform.
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
prompt += """You are a Python programmer. You will be given a task to perform.
|
|
26
|
+
Your goal is to write a self-contained Python script to accomplish the task.
|
|
27
|
+
|
|
28
|
+
In each turn, you will generate a complete Python script. The script will be executed in a fresh, stateless environment.
|
|
29
|
+
You will be given the previous script you generated and the output it produced.
|
|
30
|
+
Your task is to analyze the output to find errors or opportunities for improvement, and then generate a new, improved script.
|
|
31
|
+
You must take the previous script as a starting point and replace it with a new one that moves closer to the final solution.
|
|
32
|
+
Your final script must be a single, complete piece of code that can be executed independently.
|
|
33
|
+
|
|
34
|
+
The script must follow this structure:
|
|
35
|
+
1. All necessary imports at the top.
|
|
36
|
+
2. An `async def main():` function containing the core logic.
|
|
37
|
+
3. Do NOT include any code outside of the `async def main()` function, and do NOT call it. The execution environment handles this.
|
|
38
|
+
|
|
39
|
+
Any output you want to see from the code should be printed to the console from within the `main` function.
|
|
40
|
+
Code should be output in a fenced code block (e.g. ```python ... ```).
|
|
41
|
+
|
|
42
|
+
If you need to ask for more information or provide the final answer, you can output text to be shown directly to the user.
|
|
30
43
|
|
|
31
44
|
In addition to the Python Standard Library, you can use the following functions:"""
|
|
32
45
|
|
|
33
|
-
for
|
|
34
|
-
# Use coroutine if it exists, otherwise use func
|
|
35
|
-
tool_callable = tool.coroutine if hasattr(tool, "coroutine") and tool.coroutine is not None else tool.func
|
|
36
|
-
# Create a safe function name
|
|
37
|
-
safe_name = make_safe_function_name(tool.name)
|
|
46
|
+
for tool_name, tool_callable in tools.items():
|
|
38
47
|
# Determine if it's an async function
|
|
39
48
|
is_async = inspect.iscoroutinefunction(tool_callable)
|
|
40
49
|
# Add appropriate function definition
|
|
41
|
-
prompt += f'''\n{"async " if is_async else ""}def {
|
|
42
|
-
"""{
|
|
50
|
+
prompt += f'''\n{"async " if is_async else ""}def {tool_name}{str(inspect.signature(tool_callable))}:
|
|
51
|
+
"""{tool_callable.__doc__}"""
|
|
43
52
|
...
|
|
44
53
|
'''
|
|
45
54
|
|
|
46
|
-
prompt += """
|
|
47
|
-
|
|
48
|
-
Variables defined at the top level of previous code snippets can be referenced in your code.
|
|
49
|
-
|
|
50
|
-
Always use print() statements to explore data structures and function outputs. Simply returning values will not display them back to you for inspection. For example, use print(result) instead of just 'result'.
|
|
55
|
+
prompt += """\n\n\nAlways use print() statements to explore data structures and function outputs. Simply returning values will not display them back to you for inspection. For example, use print(result) instead of just 'result'.
|
|
51
56
|
|
|
52
57
|
As you don't know the output schema of the additional Python functions you have access to, start from exploring their contents before building a final solution.
|
|
53
58
|
|
|
54
59
|
IMPORTANT CODING STRATEGY:
|
|
55
|
-
1.
|
|
56
|
-
2.
|
|
60
|
+
1. All your code must be inside an `async def main()` function.
|
|
61
|
+
2. Do NOT import `asyncio` or call `main()`. The execution environment handles this.
|
|
62
|
+
3. Since many of the provided tools are async, you must use `await` to call them from within `main()`.
|
|
63
|
+
4. Write code up to the point where you make an API call/tool usage with an output.
|
|
64
|
+
5. Print the type/shape and a sample entry of this output, and using that knowledge proceed to write the further code.
|
|
65
|
+
6. The maximum number of characters that can be printed is 5000. Remove any unnecessary print statements.
|
|
57
66
|
|
|
58
67
|
This means:
|
|
59
68
|
- Write code that makes the API call or tool usage
|
|
@@ -64,28 +73,10 @@ This means:
|
|
|
64
73
|
|
|
65
74
|
Reminder: use Python code snippets to call tools
|
|
66
75
|
|
|
67
|
-
When you have completely finished the task and
|
|
68
|
-
"""
|
|
69
|
-
return prompt
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
REFLECTION_PROMPT = """
|
|
73
|
-
Review the assistant's latest code for as per the quality rules:
|
|
76
|
+
When you have completely finished the task, present the final result from your script to the user in a clean and readable Markdown format. Do not just summarize what you did; provide the actual output. For example, if you were asked to find unsubscribe links and your script found them, your final response should be a Markdown-formatted list of those links.
|
|
74
77
|
|
|
75
|
-
<conversation_history>
|
|
76
|
-
{conversation_history}
|
|
77
|
-
</conversation_history>
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
RETRY_PROMPT = """
|
|
84
|
-
I need you to completely regenerate your previous response based on this feedback:
|
|
85
|
-
|
|
86
|
-
'''
|
|
87
|
-
{reflection_result}
|
|
88
|
-
'''
|
|
89
|
-
|
|
90
|
-
DO NOT reference the feedback directly. Instead, provide a completely new response that addresses the issues.
|
|
79
|
+
Important:
|
|
80
|
+
After you have provided the final output, you MUST set `task_complete` to `True` in your response.
|
|
91
81
|
"""
|
|
82
|
+
return prompt
|
|
@@ -1,51 +1,89 @@
|
|
|
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
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from .models import SandboxOutput
|
|
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
|
+
# (The SAFE_BUILTINS definition remains the same)
|
|
55
|
+
# ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def eval_unsafe(
|
|
59
|
+
code: str, _locals: dict[str, Callable], timeout: int = 10
|
|
60
|
+
) -> tuple[SandboxOutput, dict[str, Any]]:
|
|
61
|
+
"""Executes a string of Python code in a sandboxed environment."""
|
|
62
|
+
original_keys = set(_locals.keys())
|
|
63
|
+
execution_context = _locals.copy()
|
|
64
|
+
execution_context["__builtins__"] = SAFE_BUILTINS
|
|
65
|
+
|
|
66
|
+
stdout_capture = io.StringIO()
|
|
67
|
+
output = SandboxOutput(stdout="")
|
|
7
68
|
|
|
8
|
-
async def eval_unsafe(code: str, _locals: dict[str, Any], timeout: int = 10) -> tuple[str, dict[str, Any]]:
|
|
9
|
-
"""
|
|
10
|
-
Execute code in a non-blocking way and return the output and changed variables.
|
|
11
|
-
"""
|
|
12
|
-
result = f"Executing code...\n{code}\n\nOutput:\n"
|
|
13
|
-
result += "=" * 50 + "\n"
|
|
14
|
-
|
|
15
|
-
# Create a combined globals/locals environment that includes builtins
|
|
16
|
-
# and the provided context. This allows nested functions to access tools.
|
|
17
|
-
execution_env = {**builtins.__dict__, **_locals}
|
|
18
|
-
|
|
19
|
-
def sync_eval_in_thread():
|
|
20
|
-
"""Synchronously execute code and capture output."""
|
|
21
|
-
try:
|
|
22
|
-
with contextlib.redirect_stdout(io.StringIO()) as f:
|
|
23
|
-
exec(code, execution_env)
|
|
24
|
-
output = f.getvalue()
|
|
25
|
-
if not output:
|
|
26
|
-
output = "<code ran, no output printed to stdout>"
|
|
27
|
-
return output
|
|
28
|
-
except Exception as e:
|
|
29
|
-
return f"Error during execution: {repr(e)}"
|
|
30
|
-
|
|
31
|
-
# Run the synchronous exec in a separate thread to avoid blocking the event loop.
|
|
32
69
|
try:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
70
|
+
logger.debug(f"Executing code with timeout {timeout}")
|
|
71
|
+
with contextlib.redirect_stdout(stdout_capture):
|
|
72
|
+
exec(code, execution_context)
|
|
73
|
+
|
|
74
|
+
if "main" in execution_context and asyncio.iscoroutinefunction(execution_context["main"]):
|
|
75
|
+
return_val = await asyncio.wait_for(execution_context["main"](), timeout=timeout)
|
|
76
|
+
output.return_value = return_val
|
|
77
|
+
else:
|
|
78
|
+
output.error = "No `async def main()` function found in the script."
|
|
37
79
|
|
|
38
|
-
|
|
39
|
-
# and were not in the initial _locals, or were changed.
|
|
40
|
-
changed_vars = {}
|
|
41
|
-
builtin_keys = set(builtins.__dict__.keys())
|
|
80
|
+
output.stdout = stdout_capture.getvalue()
|
|
42
81
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
82
|
+
except Exception as e:
|
|
83
|
+
output.error = f"{type(e).__name__}: {e}"
|
|
84
|
+
output.stdout = stdout_capture.getvalue()
|
|
46
85
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
changed_vars[key] = value
|
|
86
|
+
new_keys = set(execution_context.keys()) - original_keys - {"__builtins__"}
|
|
87
|
+
new_vars = {key: execution_context[key] for key in new_keys}
|
|
50
88
|
|
|
51
|
-
return
|
|
89
|
+
return output, new_vars
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
1
|
from langgraph.graph import MessagesState
|
|
2
|
+
from pydantic import Field
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
class CodeActState(MessagesState):
|
|
7
6
|
"""State for CodeAct agent."""
|
|
8
7
|
|
|
9
|
-
script: str | None
|
|
10
|
-
"
|
|
8
|
+
script: str | None = Field(default=None, description="The Python code script to be executed.")
|
|
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
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
2
|
+
from universal_mcp.agentr.registry import AgentrRegistry
|
|
3
|
+
|
|
4
|
+
from universal_mcp.agents.codeact0.agent import CodeActAgent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def agent():
|
|
8
|
+
memory = MemorySaver()
|
|
9
|
+
agent_object = await CodeActAgent(
|
|
10
|
+
name="CodeAct Agent",
|
|
11
|
+
instructions="Be very concise in your answers.",
|
|
12
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
13
|
+
tools={"google_mail": ["list_messages"]},
|
|
14
|
+
registry=AgentrRegistry(),
|
|
15
|
+
memory=memory,
|
|
16
|
+
)._build_graph()
|
|
17
|
+
return agent_object
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
import yaml
|
|
12
|
+
from langchain.chat_models import init_chat_model
|
|
13
|
+
from langchain_anthropic import ChatAnthropic
|
|
14
|
+
from langchain_core.runnables import RunnableConfig
|
|
15
|
+
from langchain_google_vertexai.model_garden import ChatAnthropicVertex
|
|
16
|
+
from universal_mcp.agentr import Agentr
|
|
17
|
+
from universal_mcp.types import ToolFormat
|
|
18
|
+
|
|
19
|
+
from universal_mcp.agents.codeact0 import create_codeact
|
|
20
|
+
from universal_mcp.agents.codeact0.config import ContextSchema
|
|
21
|
+
from universal_mcp.agents.codeact0.utils import derive_context
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def eval(code: str, _locals: dict[str, Any], add_context: dict[str, Any]) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
25
|
+
# print(_locals)
|
|
26
|
+
EXCLUDE_TYPES = (
|
|
27
|
+
types.ModuleType, # modules
|
|
28
|
+
type(re.match("", "")),
|
|
29
|
+
type(threading.Lock()), # instead of threading.Lock
|
|
30
|
+
type(threading.RLock()), # reentrant lock
|
|
31
|
+
threading.Event, # events
|
|
32
|
+
threading.Condition, # condition vars
|
|
33
|
+
threading.Semaphore, # semaphores
|
|
34
|
+
queue.Queue, # thread-safe queues
|
|
35
|
+
socket.socket, # network sockets
|
|
36
|
+
io.IOBase, # file handles (and StringIO/BytesIO)
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
with contextlib.redirect_stdout(io.StringIO()) as f:
|
|
40
|
+
# Execute the code in the provided locals context
|
|
41
|
+
# Using exec to allow dynamic code execution
|
|
42
|
+
# This is a simplified version; in production, consider security implications
|
|
43
|
+
exec(code, _locals, _locals)
|
|
44
|
+
result = f.getvalue()
|
|
45
|
+
if not result:
|
|
46
|
+
result = "<code ran, no output printed to stdout>"
|
|
47
|
+
except Exception as e:
|
|
48
|
+
result = f"Error during execution: {repr(e)}"
|
|
49
|
+
|
|
50
|
+
# Return all variables in locals except __builtins__ and unpicklable objects (including tools)
|
|
51
|
+
all_vars = {}
|
|
52
|
+
for key, value in _locals.items():
|
|
53
|
+
if key == "__builtins__":
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
# Skip coroutines, async generators, and coroutine functions
|
|
57
|
+
if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
|
|
58
|
+
continue
|
|
59
|
+
if inspect.isasyncgen(value) or inspect.isasyncgenfunction(value):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Skip "obviously unpicklable" types
|
|
63
|
+
if isinstance(value, EXCLUDE_TYPES):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# Keep if it's not a callable OR if it has no __name__ attribute
|
|
67
|
+
if not callable(value) or not hasattr(value, "__name__"):
|
|
68
|
+
all_vars[key] = value
|
|
69
|
+
|
|
70
|
+
new_add_context = derive_context(code, add_context)
|
|
71
|
+
return result, all_vars, new_add_context
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def agent(config: RunnableConfig):
|
|
75
|
+
cfg = ContextSchema(**config.get("configurable", {}))
|
|
76
|
+
|
|
77
|
+
if cfg.json_prompt_name and cfg.json_prompt_name.strip():
|
|
78
|
+
with open(f"usecases/{cfg.json_prompt_name}.yaml", encoding="utf-8") as f:
|
|
79
|
+
content = f.read()
|
|
80
|
+
data = yaml.safe_load(content)
|
|
81
|
+
if cfg.base_prompt and cfg.base_prompt.strip():
|
|
82
|
+
pass
|
|
83
|
+
else:
|
|
84
|
+
cfg.base_prompt = data["base_prompt"]
|
|
85
|
+
cfg.tool_names = data["tools"]
|
|
86
|
+
agentr = Agentr()
|
|
87
|
+
agentr.load_tools(cfg.tool_names)
|
|
88
|
+
tools = [] # can add custom tools here like get_weather, get_simple_weather, etc.
|
|
89
|
+
|
|
90
|
+
tools_agentr = agentr.list_tools(format=ToolFormat.NATIVE)
|
|
91
|
+
tools.extend(tools_agentr)
|
|
92
|
+
|
|
93
|
+
if cfg.model_provider == "google_anthropic_vertex":
|
|
94
|
+
# For Google Anthropic Vertex, we need to use the specific model initialization due to location
|
|
95
|
+
model = ChatAnthropicVertex(model=cfg.model, temperature=0.2, location="asia-east1")
|
|
96
|
+
elif cfg.model == "claude-4-sonnet-20250514":
|
|
97
|
+
model = ChatAnthropic(
|
|
98
|
+
model=cfg.model, temperature=1, thinking={"type": "enabled", "budget_tokens": 2048}, max_tokens=4096
|
|
99
|
+
) # pyright: ignore[reportCallIssue]
|
|
100
|
+
else:
|
|
101
|
+
model = init_chat_model(model=cfg.model, model_provider=cfg.model_provider, temperature=0.2)
|
|
102
|
+
|
|
103
|
+
code_act = create_codeact(model, cfg.base_prompt, tools, eval)
|
|
104
|
+
return code_act.compile()
|