universal-mcp-agents 0.1.2__tar.gz → 0.1.4__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.2 → universal_mcp_agents-0.1.4}/PKG-INFO +4 -4
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/pyproject.toml +4 -4
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/evaluators.py +6 -2
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/run.py +21 -6
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/utils.py +45 -13
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/tests/test_agents.py +107 -24
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/graph.py +30 -11
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/studio.py +1 -7
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/base.py +55 -9
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool/__init__.py +3 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool/graph.py +78 -25
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/__init__.py +3 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/agent.py +2 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/context.py +0 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/graph.py +76 -32
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/__init__.py +6 -2
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/agent.py +2 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/context.py +0 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/graph.py +88 -59
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/prompts.py +29 -0
- universal_mcp_agents-0.1.4/src/universal_mcp/agents/bigtoolcache/tools_all.txt +956 -0
- universal_mcp_agents-0.1.4/src/universal_mcp/agents/bigtoolcache/tools_important.txt +474 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/builder.py +19 -5
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/codeact/__init__.py +16 -4
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/hil.py +16 -4
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/llm.py +5 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/planner/__init__.py +7 -3
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/planner/__main__.py +3 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/planner/graph.py +3 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/react.py +5 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/shared/tool_node.py +24 -8
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/simple.py +8 -1
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/tools.py +9 -3
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/utils.py +35 -7
- universal_mcp_agents-0.1.4/tool_retrieve.py +47 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/uv.lock +978 -15
- universal_mcp_agents-0.1.2/test.py +0 -11
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/.gitignore +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/README.md +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/bump_and_release.sh +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/__init__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/dataset.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/datasets/exact.jsonl +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/datasets/tasks.jsonl +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/evals/test.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/__init__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/__init__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/__main__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/context.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/prompts.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/state.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/autoagent/utils.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool/__main__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool/context.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool/prompts.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool/state.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/__main__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/prompts.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtool2/state.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/__main__.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/bigtoolcache/state.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/cli.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/codeact/sandbox.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/codeact/test.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/codeact/utils.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/planner/prompts.py +0 -0
- {universal_mcp_agents-0.1.2 → universal_mcp_agents-0.1.4}/src/universal_mcp/agents/planner/state.py +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: universal-mcp-agents
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Project-URL: Homepage, https://github.com/universal-mcp/applications
|
|
6
6
|
Project-URL: Repository, https://github.com/universal-mcp/applications
|
|
7
7
|
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
|
8
8
|
License: MIT
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
10
|
Requires-Dist: langchain-anthropic>=0.3.19
|
|
11
11
|
Requires-Dist: langchain-google-genai>=2.1.10
|
|
12
12
|
Requires-Dist: langchain-openai>=0.3.32
|
|
13
13
|
Requires-Dist: langgraph>=0.6.6
|
|
14
|
-
Requires-Dist: universal-mcp
|
|
15
|
-
Requires-Dist: universal-mcp
|
|
14
|
+
Requires-Dist: universal-mcp-applications>=0.1.4
|
|
15
|
+
Requires-Dist: universal-mcp>=0.1.24rc17
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
18
18
|
Requires-Dist: ruff; extra == 'dev'
|
|
@@ -6,20 +6,20 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "universal-mcp-agents"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.4"
|
|
10
10
|
description = "Add your description here"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Manoj Bajaj", email = "manojbajaj95@gmail.com" }
|
|
14
14
|
]
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.11"
|
|
16
16
|
dependencies = [
|
|
17
17
|
"langchain-anthropic>=0.3.19",
|
|
18
18
|
"langchain-google-genai>=2.1.10",
|
|
19
19
|
"langchain-openai>=0.3.32",
|
|
20
20
|
"langgraph>=0.6.6",
|
|
21
|
-
"universal-mcp",
|
|
22
|
-
"universal-mcp-applications>=0.1.
|
|
21
|
+
"universal-mcp >= 0.1.24rc17",
|
|
22
|
+
"universal-mcp-applications>=0.1.4",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[project.license]
|
|
@@ -11,13 +11,17 @@ def exact_match_evaluator(run: Run, example: Example | None = None) -> Evaluatio
|
|
|
11
11
|
and the expected output from the dataset.
|
|
12
12
|
"""
|
|
13
13
|
if example is None or "expected_output" not in example.outputs:
|
|
14
|
-
return EvaluationResult(
|
|
14
|
+
return EvaluationResult(
|
|
15
|
+
key="exact_match", score=0, comment="No expected output provided."
|
|
16
|
+
)
|
|
15
17
|
|
|
16
18
|
# The agent's response might be in a list of messages
|
|
17
19
|
agent_response_raw = run.outputs.get("output", "")
|
|
18
20
|
if isinstance(agent_response_raw, list):
|
|
19
21
|
# Extract text from the last dictionary in the list
|
|
20
|
-
agent_response =
|
|
22
|
+
agent_response = (
|
|
23
|
+
agent_response_raw[-1].get("text", "").strip() if agent_response_raw else ""
|
|
24
|
+
)
|
|
21
25
|
else:
|
|
22
26
|
agent_response = str(agent_response_raw).strip()
|
|
23
27
|
|
|
@@ -34,7 +34,9 @@ def get_agent(agent_name: str) -> BaseAgent:
|
|
|
34
34
|
elif agent_name == "auto":
|
|
35
35
|
return AutoAgent(name="auto-agent", **common_params)
|
|
36
36
|
else:
|
|
37
|
-
raise ValueError(
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Unknown agent: {agent_name}. Available agents: simple, react, auto"
|
|
39
|
+
)
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
# 2. Evaluator Registry
|
|
@@ -50,7 +52,9 @@ def get_evaluator(evaluator_name: str) -> RunEvaluator:
|
|
|
50
52
|
"""
|
|
51
53
|
evaluator = EVALUATORS.get(evaluator_name)
|
|
52
54
|
if evaluator is None:
|
|
53
|
-
raise ValueError(
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Unknown evaluator: {evaluator_name}. Available evaluators: {', '.join(EVALUATORS.keys())}"
|
|
57
|
+
)
|
|
54
58
|
return evaluator
|
|
55
59
|
|
|
56
60
|
|
|
@@ -83,7 +87,9 @@ async def main(agent_name: str, dataset_path: str, evaluator_name: str):
|
|
|
83
87
|
"""
|
|
84
88
|
The main function for the evaluation CLI.
|
|
85
89
|
"""
|
|
86
|
-
print(
|
|
90
|
+
print(
|
|
91
|
+
f"Starting evaluation with agent='{agent_name}', dataset='{dataset_path}', evaluator='{evaluator_name}'"
|
|
92
|
+
)
|
|
87
93
|
|
|
88
94
|
# 1. Get the agent and evaluator
|
|
89
95
|
agent = get_agent(agent_name)
|
|
@@ -104,12 +110,15 @@ async def main(agent_name: str, dataset_path: str, evaluator_name: str):
|
|
|
104
110
|
# If dataset with same name and examples exists, read it.
|
|
105
111
|
# Otherwise, a new one is created.
|
|
106
112
|
dataset = client.create_dataset(
|
|
107
|
-
dataset_name,
|
|
113
|
+
dataset_name,
|
|
114
|
+
description=f"Dataset for {agent_name} evaluation with {evaluator_name}.",
|
|
108
115
|
)
|
|
109
116
|
for example in dataset_examples:
|
|
110
117
|
client.create_example(
|
|
111
118
|
inputs={"user_input": example["user_input"]},
|
|
112
|
-
outputs={"output": example.get("expected_output")}
|
|
119
|
+
outputs={"output": example.get("expected_output")}
|
|
120
|
+
if "expected_output" in example
|
|
121
|
+
else None,
|
|
113
122
|
dataset_id=dataset.id,
|
|
114
123
|
)
|
|
115
124
|
print(f"Created and populated dataset '{dataset_name}' for this run.")
|
|
@@ -146,4 +155,10 @@ if __name__ == "__main__":
|
|
|
146
155
|
)
|
|
147
156
|
args = parser.parse_args()
|
|
148
157
|
|
|
149
|
-
asyncio.run(
|
|
158
|
+
asyncio.run(
|
|
159
|
+
main(
|
|
160
|
+
agent_name=args.agent,
|
|
161
|
+
dataset_path=args.dataset,
|
|
162
|
+
evaluator_name=args.evaluator,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
@@ -8,7 +8,9 @@ from evals.dataset import load_dataset
|
|
|
8
8
|
load_dotenv()
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def upload_runs_to_dataset(
|
|
11
|
+
def upload_runs_to_dataset(
|
|
12
|
+
project_name: str, dataset_name: str, dataset_description: str = ""
|
|
13
|
+
):
|
|
12
14
|
"""
|
|
13
15
|
Uploads runs from a LangSmith project to a dataset.
|
|
14
16
|
|
|
@@ -36,7 +38,9 @@ def upload_runs_to_dataset(project_name: str, dataset_name: str, dataset_descrip
|
|
|
36
38
|
)
|
|
37
39
|
example_count += 1
|
|
38
40
|
|
|
39
|
-
print(
|
|
41
|
+
print(
|
|
42
|
+
f"✅ Successfully uploaded {example_count} runs from project '{project_name}' to dataset '{dataset_name}'."
|
|
43
|
+
)
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
def upload_dataset_from_file(
|
|
@@ -71,7 +75,9 @@ def upload_dataset_from_file(
|
|
|
71
75
|
outputs = {key: example[key] for key in output_keys if key in example}
|
|
72
76
|
client.create_example(inputs=inputs, outputs=outputs, dataset_id=dataset.id)
|
|
73
77
|
|
|
74
|
-
print(
|
|
78
|
+
print(
|
|
79
|
+
f"✅ Successfully uploaded {len(examples)} examples from '{file_path}' to dataset '{dataset_name}'."
|
|
80
|
+
)
|
|
75
81
|
|
|
76
82
|
|
|
77
83
|
if __name__ == "__main__":
|
|
@@ -79,22 +85,48 @@ if __name__ == "__main__":
|
|
|
79
85
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
80
86
|
|
|
81
87
|
# Sub-parser for uploading runs from a project
|
|
82
|
-
parser_runs = subparsers.add_parser(
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
parser_runs = subparsers.add_parser(
|
|
89
|
+
"upload-runs", help="Upload runs from a project to a dataset."
|
|
90
|
+
)
|
|
85
91
|
parser_runs.add_argument(
|
|
86
|
-
"--
|
|
92
|
+
"--project-name", required=True, help="The LangSmith project name."
|
|
93
|
+
)
|
|
94
|
+
parser_runs.add_argument(
|
|
95
|
+
"--dataset-name", required=True, help="The target dataset name."
|
|
96
|
+
)
|
|
97
|
+
parser_runs.add_argument(
|
|
98
|
+
"--dataset-description",
|
|
99
|
+
default="Dataset from project runs.",
|
|
100
|
+
help="Description for the dataset.",
|
|
87
101
|
)
|
|
88
102
|
|
|
89
103
|
# Sub-parser for uploading a dataset from a file
|
|
90
|
-
parser_file = subparsers.add_parser(
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
parser_file = subparsers.add_parser(
|
|
105
|
+
"upload-file", help="Upload a dataset from a local file."
|
|
106
|
+
)
|
|
107
|
+
parser_file.add_argument(
|
|
108
|
+
"--file-path",
|
|
109
|
+
required=True,
|
|
110
|
+
help="Path to the local dataset file (CSV or JSONL).",
|
|
111
|
+
)
|
|
112
|
+
parser_file.add_argument(
|
|
113
|
+
"--dataset-name", required=True, help="The name for the dataset in LangSmith."
|
|
114
|
+
)
|
|
115
|
+
parser_file.add_argument(
|
|
116
|
+
"--dataset-description",
|
|
117
|
+
default="Dataset uploaded from file.",
|
|
118
|
+
help="Description for the dataset.",
|
|
119
|
+
)
|
|
120
|
+
parser_file.add_argument(
|
|
121
|
+
"--input-keys",
|
|
122
|
+
required=True,
|
|
123
|
+
help="Comma-separated list of input column names.",
|
|
124
|
+
)
|
|
93
125
|
parser_file.add_argument(
|
|
94
|
-
"--
|
|
126
|
+
"--output-keys",
|
|
127
|
+
required=True,
|
|
128
|
+
help="Comma-separated list of output column names.",
|
|
95
129
|
)
|
|
96
|
-
parser_file.add_argument("--input-keys", required=True, help="Comma-separated list of input column names.")
|
|
97
|
-
parser_file.add_argument("--output-keys", required=True, help="Comma-separated list of output column names.")
|
|
98
130
|
|
|
99
131
|
args = parser.parse_args()
|
|
100
132
|
|
|
@@ -21,14 +21,26 @@ class MockToolRegistry(ToolRegistry):
|
|
|
21
21
|
def __init__(self, **kwargs: Any):
|
|
22
22
|
"""Initialize the MockToolRegistry."""
|
|
23
23
|
self._apps = [
|
|
24
|
-
{
|
|
25
|
-
|
|
24
|
+
{
|
|
25
|
+
"id": "google-mail",
|
|
26
|
+
"name": "google-mail",
|
|
27
|
+
"description": "Send and manage emails.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "slack",
|
|
31
|
+
"name": "slack",
|
|
32
|
+
"description": "Team communication and messaging.",
|
|
33
|
+
},
|
|
26
34
|
{
|
|
27
35
|
"id": "google-calendar",
|
|
28
36
|
"name": "google-calendar",
|
|
29
37
|
"description": "Schedule and manage calendar events.",
|
|
30
38
|
},
|
|
31
|
-
{
|
|
39
|
+
{
|
|
40
|
+
"id": "jira",
|
|
41
|
+
"name": "jira",
|
|
42
|
+
"description": "Project tracking and issue management.",
|
|
43
|
+
},
|
|
32
44
|
{
|
|
33
45
|
"id": "github",
|
|
34
46
|
"name": "github",
|
|
@@ -38,23 +50,67 @@ class MockToolRegistry(ToolRegistry):
|
|
|
38
50
|
self._connected_apps = ["google-mail", "google-calendar", "github"]
|
|
39
51
|
self._tools = {
|
|
40
52
|
"google-mail": [
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
{
|
|
54
|
+
"id": "send_email",
|
|
55
|
+
"name": "send_email",
|
|
56
|
+
"description": "Send an email to a recipient.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "read_email",
|
|
60
|
+
"name": "read_email",
|
|
61
|
+
"description": "Read emails from inbox.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "create_draft",
|
|
65
|
+
"name": "create_draft",
|
|
66
|
+
"description": "Create a draft email.",
|
|
67
|
+
},
|
|
44
68
|
],
|
|
45
69
|
"slack": [
|
|
46
|
-
{
|
|
47
|
-
|
|
70
|
+
{
|
|
71
|
+
"id": "send_message",
|
|
72
|
+
"name": "send_message",
|
|
73
|
+
"description": "Send a message to a team channel.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "read_channel",
|
|
77
|
+
"name": "read_channel",
|
|
78
|
+
"description": "Read messages from a channel.",
|
|
79
|
+
},
|
|
48
80
|
],
|
|
49
81
|
"google-calendar": [
|
|
50
|
-
{
|
|
51
|
-
|
|
82
|
+
{
|
|
83
|
+
"id": "create_event",
|
|
84
|
+
"name": "create_event",
|
|
85
|
+
"description": "Create a new calendar event.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "find_event",
|
|
89
|
+
"name": "find_event",
|
|
90
|
+
"description": "Find an event in the calendar.",
|
|
91
|
+
},
|
|
52
92
|
],
|
|
53
93
|
"github": [
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
{
|
|
95
|
+
"id": "create_issue",
|
|
96
|
+
"name": "create_issue",
|
|
97
|
+
"description": "Create an issue in a repository.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "get_issue",
|
|
101
|
+
"name": "get_issue",
|
|
102
|
+
"description": "Get details of a specific issue.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "create_pull_request",
|
|
106
|
+
"name": "create_pull_request",
|
|
107
|
+
"description": "Create a pull request.",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "get_repository",
|
|
111
|
+
"name": "get_repository",
|
|
112
|
+
"description": "Get details of a repository.",
|
|
113
|
+
},
|
|
58
114
|
],
|
|
59
115
|
}
|
|
60
116
|
self._tool_mappings = {
|
|
@@ -94,7 +150,11 @@ class MockToolRegistry(ToolRegistry):
|
|
|
94
150
|
) -> list[dict[str, Any]]:
|
|
95
151
|
"""Search for apps by a query."""
|
|
96
152
|
query = query.lower()
|
|
97
|
-
results = [
|
|
153
|
+
results = [
|
|
154
|
+
app
|
|
155
|
+
for app in self._apps
|
|
156
|
+
if query in app["name"].lower() or query in app["description"].lower()
|
|
157
|
+
]
|
|
98
158
|
return results[:limit]
|
|
99
159
|
|
|
100
160
|
async def list_tools(
|
|
@@ -126,7 +186,12 @@ class MockToolRegistry(ToolRegistry):
|
|
|
126
186
|
results = [tool for tool in all_app_tools if tool["name"] in found_tool_names]
|
|
127
187
|
|
|
128
188
|
if not results:
|
|
129
|
-
results = [
|
|
189
|
+
results = [
|
|
190
|
+
{
|
|
191
|
+
"name": "general_purpose_tool",
|
|
192
|
+
"description": "A general purpose tool.",
|
|
193
|
+
}
|
|
194
|
+
]
|
|
130
195
|
|
|
131
196
|
return results[:limit]
|
|
132
197
|
|
|
@@ -145,7 +210,9 @@ class MockToolRegistry(ToolRegistry):
|
|
|
145
210
|
# Return a list of mock tools for the ReAct agent to use
|
|
146
211
|
return [mock_tool_callable]
|
|
147
212
|
|
|
148
|
-
async def call_tool(
|
|
213
|
+
async def call_tool(
|
|
214
|
+
self, tool_name: str, tool_args: dict[str, Any]
|
|
215
|
+
) -> dict[str, Any]:
|
|
149
216
|
"""Call a tool with the given name and arguments."""
|
|
150
217
|
print(f"MockToolRegistry: Called tool '{tool_name}' with args {tool_args}")
|
|
151
218
|
return {"status": f"task has been done by tool {tool_name}"}
|
|
@@ -172,7 +239,9 @@ class TestToolFinderGraph:
|
|
|
172
239
|
"""Test Case 1: Simple case (Connected App)"""
|
|
173
240
|
task = "Send an email to my manager about the project update."
|
|
174
241
|
graph = build_tool_node_graph(llm, registry)
|
|
175
|
-
final_state = await graph.ainvoke(
|
|
242
|
+
final_state = await graph.ainvoke(
|
|
243
|
+
{"task": task, "messages": [HumanMessage(content=task)]}
|
|
244
|
+
)
|
|
176
245
|
assert final_state["apps_required"] is True
|
|
177
246
|
assert "google-mail" in final_state["relevant_apps"]
|
|
178
247
|
assert "google-mail" in final_state["apps_with_tools"]
|
|
@@ -183,7 +252,9 @@ class TestToolFinderGraph:
|
|
|
183
252
|
"""Test Case 2: Multiple apps found"""
|
|
184
253
|
task = "Send a message to my team about the new design."
|
|
185
254
|
graph = build_tool_node_graph(llm, registry)
|
|
186
|
-
final_state = await graph.ainvoke(
|
|
255
|
+
final_state = await graph.ainvoke(
|
|
256
|
+
{"task": task, "messages": [HumanMessage(content=task)]}
|
|
257
|
+
)
|
|
187
258
|
assert final_state["apps_required"] is True
|
|
188
259
|
assert "google-mail" in final_state["relevant_apps"]
|
|
189
260
|
assert "slack" in final_state["relevant_apps"]
|
|
@@ -195,7 +266,9 @@ class TestToolFinderGraph:
|
|
|
195
266
|
"""Test Case 3: No relevant app"""
|
|
196
267
|
task = "Can you create a blog post on my wordpress site?"
|
|
197
268
|
graph = build_tool_node_graph(llm, registry)
|
|
198
|
-
final_state = await graph.ainvoke(
|
|
269
|
+
final_state = await graph.ainvoke(
|
|
270
|
+
{"task": task, "messages": [HumanMessage(content=task)]}
|
|
271
|
+
)
|
|
199
272
|
assert final_state["apps_required"] is True
|
|
200
273
|
assert not final_state["relevant_apps"]
|
|
201
274
|
assert not final_state["apps_with_tools"]
|
|
@@ -205,7 +278,9 @@ class TestToolFinderGraph:
|
|
|
205
278
|
"""Test Case 4: Multiple tools in one app"""
|
|
206
279
|
task = "Create a new issue for a bug in our github repository, and send message on slack about the issue."
|
|
207
280
|
graph = build_tool_node_graph(llm, registry)
|
|
208
|
-
final_state = await graph.ainvoke(
|
|
281
|
+
final_state = await graph.ainvoke(
|
|
282
|
+
{"task": task, "messages": [HumanMessage(content=task)]}
|
|
283
|
+
)
|
|
209
284
|
assert final_state["apps_required"] is True
|
|
210
285
|
assert "github" in final_state["relevant_apps"]
|
|
211
286
|
assert "slack" in final_state["relevant_apps"]
|
|
@@ -219,7 +294,9 @@ class TestToolFinderGraph:
|
|
|
219
294
|
"""Test Case 5: Unavailable App"""
|
|
220
295
|
task = "Create a new design file in Figma."
|
|
221
296
|
graph = build_tool_node_graph(llm, registry)
|
|
222
|
-
final_state = await graph.ainvoke(
|
|
297
|
+
final_state = await graph.ainvoke(
|
|
298
|
+
{"task": task, "messages": [HumanMessage(content=task)]}
|
|
299
|
+
)
|
|
223
300
|
assert final_state["apps_required"] is True
|
|
224
301
|
assert not final_state["relevant_apps"]
|
|
225
302
|
assert not final_state["apps_with_tools"]
|
|
@@ -229,7 +306,9 @@ class TestToolFinderGraph:
|
|
|
229
306
|
"""Test Case 6: No App Needed"""
|
|
230
307
|
task = "hello"
|
|
231
308
|
graph = build_tool_node_graph(llm, registry)
|
|
232
|
-
final_state = await graph.ainvoke(
|
|
309
|
+
final_state = await graph.ainvoke(
|
|
310
|
+
{"task": task, "messages": [HumanMessage(content=task)]}
|
|
311
|
+
)
|
|
233
312
|
assert final_state["apps_required"] is False
|
|
234
313
|
|
|
235
314
|
|
|
@@ -272,7 +351,11 @@ class TestAgents:
|
|
|
272
351
|
assert final_messages, "The agent should have produced at least one message."
|
|
273
352
|
last_message = final_messages[-1]
|
|
274
353
|
|
|
275
|
-
final_response =
|
|
354
|
+
final_response = (
|
|
355
|
+
last_message.content
|
|
356
|
+
if hasattr(last_message, "content")
|
|
357
|
+
else str(last_message)
|
|
358
|
+
)
|
|
276
359
|
|
|
277
360
|
# Print the response for manual verification and for the LLM judge
|
|
278
361
|
print("\n--- Agent's Final Response ---")
|
|
@@ -22,7 +22,9 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
22
22
|
tools_list = []
|
|
23
23
|
if app_ids is not None:
|
|
24
24
|
for app_id in app_ids:
|
|
25
|
-
tools_list.extend(
|
|
25
|
+
tools_list.extend(
|
|
26
|
+
await tool_registry.search_tools(query, limit=10, app_id=app_id)
|
|
27
|
+
)
|
|
26
28
|
else:
|
|
27
29
|
tools_list = await tool_registry.search_tools(query, limit=10)
|
|
28
30
|
tools_list = [f"{tool['id']}: {tool['description']}" for tool in tools_list]
|
|
@@ -48,21 +50,33 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
48
50
|
connections = await tool_registry.list_connected_apps()
|
|
49
51
|
connection_ids = set([connection["app_id"] for connection in connections])
|
|
50
52
|
connected_apps = [app["id"] for app in app_ids if app["id"] in connection_ids]
|
|
51
|
-
unconnected_apps = [
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
unconnected_apps = [
|
|
54
|
+
app["id"] for app in app_ids if app["id"] not in connection_ids
|
|
55
|
+
]
|
|
56
|
+
app_id_descriptions = (
|
|
57
|
+
"These are the apps connected to the user's account:\n"
|
|
58
|
+
+ "\n".join([f"{app}" for app in connected_apps])
|
|
54
59
|
)
|
|
55
60
|
if unconnected_apps:
|
|
56
61
|
app_id_descriptions += "\n\nOther (not connected) apps: " + "\n".join(
|
|
57
62
|
[f"{app}" for app in unconnected_apps]
|
|
58
63
|
)
|
|
59
64
|
|
|
60
|
-
system_prompt = system_prompt.format(
|
|
65
|
+
system_prompt = system_prompt.format(
|
|
66
|
+
system_time=datetime.now(tz=UTC).isoformat(), app_ids=app_id_descriptions
|
|
67
|
+
)
|
|
61
68
|
|
|
62
|
-
messages = [
|
|
69
|
+
messages = [
|
|
70
|
+
{"role": "system", "content": system_prompt + "\n" + instructions},
|
|
71
|
+
*state["messages"],
|
|
72
|
+
]
|
|
63
73
|
model = load_chat_model(runtime.context.model)
|
|
64
|
-
loaded_tools = await tool_registry.export_tools(
|
|
65
|
-
|
|
74
|
+
loaded_tools = await tool_registry.export_tools(
|
|
75
|
+
tools=state["selected_tool_ids"], format=ToolFormat.LANGCHAIN
|
|
76
|
+
)
|
|
77
|
+
model_with_tools = model.bind_tools(
|
|
78
|
+
[search_tools, ask_user, load_tools, *loaded_tools], tool_choice="auto"
|
|
79
|
+
)
|
|
66
80
|
response_raw = model_with_tools.invoke(messages)
|
|
67
81
|
response = cast(AIMessage, response_raw)
|
|
68
82
|
return {"messages": [response]}
|
|
@@ -102,7 +116,8 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
102
116
|
tools = await search_tools.ainvoke(tool_call["args"])
|
|
103
117
|
outputs.append(
|
|
104
118
|
ToolMessage(
|
|
105
|
-
content=json.dumps(tools)
|
|
119
|
+
content=json.dumps(tools)
|
|
120
|
+
+ "\n\nUse the load_tools tool to load the tools you want to use.",
|
|
106
121
|
name=tool_call["name"],
|
|
107
122
|
tool_call_id=tool_call["id"],
|
|
108
123
|
)
|
|
@@ -119,9 +134,13 @@ async def build_graph(tool_registry: ToolRegistry, instructions: str = ""):
|
|
|
119
134
|
)
|
|
120
135
|
)
|
|
121
136
|
else:
|
|
122
|
-
await tool_registry.export_tools(
|
|
137
|
+
await tool_registry.export_tools(
|
|
138
|
+
[tool_call["name"]], ToolFormat.LANGCHAIN
|
|
139
|
+
)
|
|
123
140
|
try:
|
|
124
|
-
tool_result = await tool_registry.call_tool(
|
|
141
|
+
tool_result = await tool_registry.call_tool(
|
|
142
|
+
tool_call["name"], tool_call["args"]
|
|
143
|
+
)
|
|
125
144
|
outputs.append(
|
|
126
145
|
ToolMessage(
|
|
127
146
|
content=json.dumps(tool_result),
|
|
@@ -8,7 +8,6 @@ tool_registry = AgentrRegistry()
|
|
|
8
8
|
tool_manager = ToolManager()
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
async def main():
|
|
13
12
|
instructions = """
|
|
14
13
|
You are a helpful assistant that can use tools to help the user. If a task requires multiple steps, you should perform separate different searches for different actions. Prefer completing one action before searching for another.
|
|
@@ -16,10 +15,5 @@ async def main():
|
|
|
16
15
|
graph = await build_graph(tool_registry, instructions=instructions)
|
|
17
16
|
return graph
|
|
18
17
|
|
|
19
|
-
graph = asyncio.run(main())
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
18
|
|
|
19
|
+
graph = asyncio.run(main())
|
|
@@ -10,7 +10,14 @@ from .utils import RichCLI
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class BaseAgent:
|
|
13
|
-
def __init__(
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
name: str,
|
|
16
|
+
instructions: str,
|
|
17
|
+
model: str,
|
|
18
|
+
memory: BaseCheckpointSaver | None = None,
|
|
19
|
+
**kwargs,
|
|
20
|
+
):
|
|
14
21
|
self.name = name
|
|
15
22
|
self.instructions = instructions
|
|
16
23
|
self.model = model
|
|
@@ -27,12 +34,26 @@ class BaseAgent:
|
|
|
27
34
|
async def _build_graph(self):
|
|
28
35
|
raise NotImplementedError("Subclasses must implement this method")
|
|
29
36
|
|
|
30
|
-
async def stream(self, thread_id: str, user_input: str):
|
|
37
|
+
async def stream(self, thread_id: str, user_input: str, metadata: dict = None):
|
|
31
38
|
await self.ainit()
|
|
32
39
|
aggregate = None
|
|
40
|
+
|
|
41
|
+
run_metadata = {
|
|
42
|
+
"agent_name": self.name,
|
|
43
|
+
"is_background_run": False, # Default to False
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if metadata:
|
|
47
|
+
run_metadata.update(metadata)
|
|
48
|
+
|
|
49
|
+
run_config = {
|
|
50
|
+
"configurable": {"thread_id": thread_id},
|
|
51
|
+
"metadata": run_metadata,
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
async for event, metadata in self._graph.astream(
|
|
34
55
|
{"messages": [{"role": "user", "content": user_input}]},
|
|
35
|
-
config=
|
|
56
|
+
config=run_config,
|
|
36
57
|
context={"system_prompt": self.instructions, "model": self.model},
|
|
37
58
|
stream_mode="messages",
|
|
38
59
|
stream_usage=True,
|
|
@@ -66,12 +87,28 @@ class BaseAgent:
|
|
|
66
87
|
async for event in self.stream(thread_id, user_input):
|
|
67
88
|
stream_updater.update(event.content)
|
|
68
89
|
|
|
69
|
-
async def invoke(
|
|
90
|
+
async def invoke(
|
|
91
|
+
self, user_input: str, thread_id: str = str(uuid4()), metadata: dict = None
|
|
92
|
+
):
|
|
70
93
|
"""Run the agent"""
|
|
71
94
|
await self.ainit()
|
|
95
|
+
|
|
96
|
+
run_metadata = {
|
|
97
|
+
"agent_name": self.name,
|
|
98
|
+
"is_background_run": False, # Default to False
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if metadata:
|
|
102
|
+
run_metadata.update(metadata)
|
|
103
|
+
|
|
104
|
+
run_config = {
|
|
105
|
+
"configurable": {"thread_id": thread_id},
|
|
106
|
+
"metadata": run_metadata,
|
|
107
|
+
}
|
|
108
|
+
|
|
72
109
|
return await self._graph.ainvoke(
|
|
73
110
|
{"messages": [{"role": "user", "content": user_input}]},
|
|
74
|
-
config=
|
|
111
|
+
config=run_config,
|
|
75
112
|
context={"system_prompt": self.instructions, "model": self.model},
|
|
76
113
|
)
|
|
77
114
|
|
|
@@ -85,10 +122,15 @@ class BaseAgent:
|
|
|
85
122
|
# Main loop
|
|
86
123
|
while True:
|
|
87
124
|
try:
|
|
88
|
-
state = self._graph.get_state(
|
|
125
|
+
state = self._graph.get_state(
|
|
126
|
+
config={"configurable": {"thread_id": thread_id}}
|
|
127
|
+
)
|
|
89
128
|
if state.interrupts:
|
|
90
129
|
value = self.cli.handle_interrupt(state.interrupts[0])
|
|
91
|
-
self._graph.invoke(
|
|
130
|
+
self._graph.invoke(
|
|
131
|
+
Command(resume=value),
|
|
132
|
+
config={"configurable": {"thread_id": thread_id}},
|
|
133
|
+
)
|
|
92
134
|
continue
|
|
93
135
|
|
|
94
136
|
user_input = self.cli.get_user_input()
|
|
@@ -99,7 +141,9 @@ class BaseAgent:
|
|
|
99
141
|
if user_input.startswith("/"):
|
|
100
142
|
command = user_input.lower().lstrip("/")
|
|
101
143
|
if command == "about":
|
|
102
|
-
self.cli.display_info(
|
|
144
|
+
self.cli.display_info(
|
|
145
|
+
f"Agent is {self.name}. {self.instructions}"
|
|
146
|
+
)
|
|
103
147
|
continue
|
|
104
148
|
elif command == "exit" or command == "quit" or command == "q":
|
|
105
149
|
self.cli.display_info("Goodbye! 👋")
|
|
@@ -110,7 +154,9 @@ class BaseAgent:
|
|
|
110
154
|
thread_id = str(uuid4())
|
|
111
155
|
continue
|
|
112
156
|
elif command == "help":
|
|
113
|
-
self.cli.display_info(
|
|
157
|
+
self.cli.display_info(
|
|
158
|
+
"Available commands: /about, /exit, /quit, /q, /reset"
|
|
159
|
+
)
|
|
114
160
|
continue
|
|
115
161
|
else:
|
|
116
162
|
self.cli.display_error(f"Unknown command: {command}")
|