agentify-toolkit 0.18.2__tar.gz → 0.21.0__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.
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/PKG-INFO +2 -2
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/README.md +1 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/pyproject.toml +1 -1
- agentify_toolkit-0.21.0/src/agentify/agent.py +533 -0
- agentify_toolkit-0.21.0/src/agentify/cli.py +131 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/__init__.py +3 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/agent.py +1 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/deploy.py +1 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/gateway.py +1 -1
- agentify_toolkit-0.21.0/src/agentify/commands/mcp.py +168 -0
- agentify_toolkit-0.21.0/src/agentify/commands/provider.py +106 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/run.py +4 -4
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/runtime.py +4 -4
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/serve.py +1 -1
- agentify_toolkit-0.21.0/src/agentify/commands/tool.py +166 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/gateway/server.py +8 -3
- agentify_toolkit-0.21.0/src/agentify/headers.py +45 -0
- agentify_toolkit-0.21.0/src/agentify/mcp/__init__.py +2 -0
- agentify_toolkit-0.21.0/src/agentify/mcp/builtin_tools.py +47 -0
- agentify_toolkit-0.21.0/src/agentify/mcp/client.py +150 -0
- agentify_toolkit-0.21.0/src/agentify/mcp/registry.py +20 -0
- agentify_toolkit-0.21.0/src/agentify/mcp/server.py +178 -0
- agentify_toolkit-0.21.0/src/agentify/mcp_client.py +23 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/__init__.py +2 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/agentify.py +7 -3
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/anthropic.py +20 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/bedrock.py +9 -2
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/deepseek.py +7 -2
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/github.py +16 -2
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/google.py +14 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/mistral.py +15 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/ollama.py +7 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/ollama_local.py +7 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/openai.py +15 -1
- agentify_toolkit-0.21.0/src/agentify/providers/rate_card.py +60 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/x.py +14 -1
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/runtime/server.py +4 -3
- agentify_toolkit-0.21.0/src/agentify/tool.py +215 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/utils/env_manager.py +4 -4
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/PKG-INFO +2 -2
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/SOURCES.txt +9 -1
- agentify_toolkit-0.18.2/src/agentify/_cli.py +0 -662
- agentify_toolkit-0.18.2/src/agentify/agent.py +0 -251
- agentify_toolkit-0.18.2/src/agentify/cli.py +0 -32
- agentify_toolkit-0.18.2/src/agentify/commands/provider.py +0 -51
- agentify_toolkit-0.18.2/src/agentify/commands/tool.py +0 -72
- agentify_toolkit-0.18.2/src/agentify/tool.py +0 -147
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/LICENSE +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/NOTICE +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/setup.cfg +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/__init__.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/cli_config.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/cli_ui.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/config.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/backplane.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/runtime/__init__.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/runtime_client.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/server/__init__.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/server/server.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/specs.py +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/agent_list.css +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/agent_list.html +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/basic-chat.css +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/chat.css +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/chat.html +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/fun_agent_list.html +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/htmx.min.js +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/retro-chat.css +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/runtime_chat.html +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/dependency_links.txt +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/entry_points.txt +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/requires.txt +0 -0
- {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentify_toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Python Toolkit for Declarative AI Agent Development
|
|
5
5
|
Author-email: Lewis Sheridan <lewis@backplane.dev>
|
|
6
6
|
License: Apache License
|
|
@@ -142,7 +142,7 @@ Dynamic: license-file
|
|
|
142
142
|
|
|
143
143
|

|
|
144
144
|
|
|
145
|
-

|
|
146
146
|
|
|
147
147
|
Agentify is a lightweight, declarative-first toolkit for prototyping AI agents. It lets you define agents as YAML specs and test them rapidly from the CLI or Python, without committing to a framework or model provider.
|
|
148
148
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
12
|
-

|
|
13
13
|
|
|
14
14
|
Agentify is a lightweight, declarative-first toolkit for prototyping AI agents. It lets you define agents as YAML specs and test them rapidly from the CLI or Python, without committing to a framework or model provider.
|
|
15
15
|
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# Copyright 2026 Backplane Software
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
# from agentify import Agent
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import json
|
|
9
|
+
from .specs import load_tool_spec
|
|
10
|
+
from .tool import create_tool
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Import MCP Client
|
|
14
|
+
# from agentify.mcp_client import MCPClient
|
|
15
|
+
from agentify.mcp.client import MCPClientHTTP
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Agent:
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
provider: str
|
|
23
|
+
model_id: str
|
|
24
|
+
|
|
25
|
+
role: str
|
|
26
|
+
|
|
27
|
+
input_tokens: int = 0
|
|
28
|
+
output_tokens: int = 0
|
|
29
|
+
token_cost: float = 0.0
|
|
30
|
+
|
|
31
|
+
version: Optional[str] = field(default="0.0.0")
|
|
32
|
+
|
|
33
|
+
tool_names: list = field(default_factory=list)
|
|
34
|
+
tools: dict = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
# mcp_client: Optional["MCPClientHTTP"] = None
|
|
37
|
+
mcp_clients: list[MCPClientHTTP] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
agent_file: Path | None = None
|
|
40
|
+
_tools_loaded: bool = field(default=False, init=False)
|
|
41
|
+
|
|
42
|
+
conversation_history: list = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
def _get_total_tokens(self):
|
|
45
|
+
"""Returns input_tokens + output_tokens"""
|
|
46
|
+
return self.input_tokens + self.output_tokens
|
|
47
|
+
|
|
48
|
+
# def _get_total_tokens_cost(self):
|
|
49
|
+
# """Returns cost of input and output tokens based on 1:6 aggregated averages"""
|
|
50
|
+
# # Token cost based on input: 0.00002 USD output: 0.00012 USD
|
|
51
|
+
# input_token_cost = 0.00002 * self.input_tokens
|
|
52
|
+
# output_token_cost = 0.00012 * self.output_tokens
|
|
53
|
+
# return round(input_token_cost + output_token_cost, 2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_tools(self, tool_path_override: str | Path | None = None):
|
|
57
|
+
"""
|
|
58
|
+
Load tools for this agent.
|
|
59
|
+
|
|
60
|
+
- Defaults to <agent_file parent>/tools
|
|
61
|
+
- Optional override via tool_path_override
|
|
62
|
+
- Users do not need to worry about paths
|
|
63
|
+
"""
|
|
64
|
+
if self._tools_loaded:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Determine tools directory
|
|
68
|
+
if tool_path_override:
|
|
69
|
+
tools_dir = Path(tool_path_override).resolve()
|
|
70
|
+
elif self.agent_file:
|
|
71
|
+
tools_dir = Path(self.agent_file).resolve().parent / "tools"
|
|
72
|
+
else:
|
|
73
|
+
# fallback only if agent_file not set
|
|
74
|
+
tools_dir = Path.cwd() / "tools"
|
|
75
|
+
|
|
76
|
+
if not tools_dir.exists():
|
|
77
|
+
raise FileNotFoundError(f"Tools directory does not exist: {tools_dir}")
|
|
78
|
+
|
|
79
|
+
# Load each tool
|
|
80
|
+
self.tools = {} # reset
|
|
81
|
+
for tool_name in self.tool_names or []:
|
|
82
|
+
tool_file = tools_dir / f"{tool_name}.yaml"
|
|
83
|
+
if not tool_file.exists():
|
|
84
|
+
raise FileNotFoundError(f"Tool '{tool_name}' not found at {tool_file}")
|
|
85
|
+
|
|
86
|
+
spec = load_tool_spec(tool_file) # your YAML loader
|
|
87
|
+
tool = create_tool(spec, tool_file) # your tool factory
|
|
88
|
+
self.tools[tool.name] = tool
|
|
89
|
+
|
|
90
|
+
self._tools_loaded = True
|
|
91
|
+
|
|
92
|
+
def get_model(self) -> str:
|
|
93
|
+
return self.model_id
|
|
94
|
+
|
|
95
|
+
def get_tools(self) -> list[str]:
|
|
96
|
+
return list(self.tools.keys())
|
|
97
|
+
|
|
98
|
+
def run(self, user_prompt: str) -> dict:
|
|
99
|
+
from agentify.providers import run_openai, run_anthropic, run_google, run_bedrock, run_github, run_x, run_deepseek, run_mistral, run_ollama, run_ollama_local, run_gateway_http
|
|
100
|
+
|
|
101
|
+
match self.provider.lower():
|
|
102
|
+
case "openai":
|
|
103
|
+
return run_openai(self.model_id, user_prompt)
|
|
104
|
+
case "anthropic":
|
|
105
|
+
return run_anthropic(self.model_id, user_prompt)
|
|
106
|
+
case "google":
|
|
107
|
+
return run_google(self.model_id, user_prompt)
|
|
108
|
+
case "bedrock":
|
|
109
|
+
return run_bedrock(self.model_id, user_prompt)
|
|
110
|
+
case "github":
|
|
111
|
+
return run_github(self.model_id, user_prompt)
|
|
112
|
+
case "agentify":
|
|
113
|
+
return run_gateway_http(self.model_id, user_prompt)
|
|
114
|
+
case "xai":
|
|
115
|
+
return run_x(self.model_id, user_prompt)
|
|
116
|
+
case "deepseek":
|
|
117
|
+
return run_deepseek(self.model_id, user_prompt)
|
|
118
|
+
case "mistral":
|
|
119
|
+
return run_mistral(self.model_id, user_prompt)
|
|
120
|
+
case "ollama":
|
|
121
|
+
return run_ollama(self.model_id, user_prompt)
|
|
122
|
+
case "ollama_local":
|
|
123
|
+
return run_ollama_local(self.model_id, user_prompt)
|
|
124
|
+
case _:
|
|
125
|
+
raise ValueError(f"Unsupported provider: {self.provider}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def chat(self, debug: bool = False, toolprompt: bool = False):
|
|
129
|
+
from rich.console import Console, Group
|
|
130
|
+
from rich.panel import Panel
|
|
131
|
+
from rich.prompt import Prompt
|
|
132
|
+
from rich.text import Text
|
|
133
|
+
from rich.pretty import Pretty
|
|
134
|
+
|
|
135
|
+
# Load Tools from local Files in tools/
|
|
136
|
+
if self.tool_names and not self._tools_loaded:
|
|
137
|
+
self.load_tools()
|
|
138
|
+
|
|
139
|
+
# MCP Tool loader v2
|
|
140
|
+
mcp_tools = []
|
|
141
|
+
mcp_tool_names = []
|
|
142
|
+
|
|
143
|
+
for client in self.mcp_clients:
|
|
144
|
+
client.initialize()
|
|
145
|
+
tools = client.list_tools()
|
|
146
|
+
# mcp_tools += tools
|
|
147
|
+
# mcp_tool_names += [t["name"] for t in tools]
|
|
148
|
+
|
|
149
|
+
for tool in tools:
|
|
150
|
+
namespaced_tool = tool.copy()
|
|
151
|
+
|
|
152
|
+
original_name = tool["name"]
|
|
153
|
+
namespaced_name = f"{client.name}.{original_name}"
|
|
154
|
+
|
|
155
|
+
namespaced_tool["name"] = namespaced_name
|
|
156
|
+
|
|
157
|
+
mcp_tools.append(namespaced_tool)
|
|
158
|
+
mcp_tool_names.append(namespaced_name)
|
|
159
|
+
|
|
160
|
+
console = Console()
|
|
161
|
+
|
|
162
|
+
# Print agent header
|
|
163
|
+
console.print(Panel(
|
|
164
|
+
f"[bold cyan]{self.name.upper()}[/bold cyan] [dim]{self.version}[/dim]\n"
|
|
165
|
+
f"Role: {self.role}\n"
|
|
166
|
+
f"Using [yellow]{self.model_id}[/yellow] by {self.provider}\n"
|
|
167
|
+
f"Agent Tools: {self.tool_names}\n"
|
|
168
|
+
f"MCP Server Tools: {mcp_tool_names}\n"
|
|
169
|
+
f"Tool Prompt enabled: {toolprompt}",
|
|
170
|
+
border_style="cyan"
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Load Local Tool Schemas
|
|
175
|
+
tool_schemas = [tool.to_schema() for tool in self.tools.values()] if self.tools else None
|
|
176
|
+
tools_block = ""
|
|
177
|
+
if tool_schemas:
|
|
178
|
+
# tools_block = "\n\nLOCAL TOOLS:\n" + json.dumps(tool_schemas, indent=2)
|
|
179
|
+
tools_block = "\n\nLOCAL TOOLS:\n" + json.dumps(tool_schemas)
|
|
180
|
+
|
|
181
|
+
# Load MCP Server Tools
|
|
182
|
+
if self.mcp_clients:
|
|
183
|
+
# tools_block += "\n\nMCP TOOLS:\n" + json.dumps(mcp_tools, indent=2)
|
|
184
|
+
tools_block += "\n\nMCP TOOLS:\n" + json.dumps(mcp_tools)
|
|
185
|
+
|
|
186
|
+
if debug:
|
|
187
|
+
console.print(
|
|
188
|
+
Panel.fit(
|
|
189
|
+
Group(
|
|
190
|
+
"[bold cyan]LOCAL TOOLS[/bold cyan]",
|
|
191
|
+
json.dumps(tool_schemas, indent=2, sort_keys=True) if tool_schemas else "[dim]None[/dim]",
|
|
192
|
+
"",
|
|
193
|
+
"[bold magenta]MCP TOOLS[/bold magenta]",
|
|
194
|
+
json.dumps(mcp_tools, indent=2, sort_keys=True) if self.mcp_clients else "[dim]None[/dim]"
|
|
195
|
+
),
|
|
196
|
+
title="[bold white]Debug[/bold white]",
|
|
197
|
+
border_style="white",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Lightweight tool index for prompt (name + description only) // Reduces 'in' tokens
|
|
202
|
+
tool_index = [
|
|
203
|
+
{"name": tool.name, "description": tool.description}
|
|
204
|
+
for tool in self.tools.values()
|
|
205
|
+
]
|
|
206
|
+
mcp_tool_index = [
|
|
207
|
+
{"name": tool["name"], "description": tool["description"]}
|
|
208
|
+
for tool in mcp_tools
|
|
209
|
+
]
|
|
210
|
+
index = tool_index + mcp_tool_index
|
|
211
|
+
compact_index = json.dumps(index)
|
|
212
|
+
|
|
213
|
+
while True:
|
|
214
|
+
prompt = Prompt.ask("\nEnter your prompt ('/exit' to quit)")
|
|
215
|
+
if prompt.lower() in ["/exit", "quit"]:
|
|
216
|
+
console.print(f"[yellow]Total tokens used in this session:[/yellow] {self._get_total_tokens()}, Cost: {round(self.token_cost, 7)} USD")
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
# Add user input to conversation history
|
|
220
|
+
self.conversation_history.append({"role": "user", "content": prompt})
|
|
221
|
+
|
|
222
|
+
# Build full prompt from last N turns (e.g., last 6)
|
|
223
|
+
full_prompt = f"You must assume the role of {self.role} when responding to these prompts:\n\n"
|
|
224
|
+
for turn in self.conversation_history[-6:]:
|
|
225
|
+
role = turn["role"]
|
|
226
|
+
content = turn["content"]
|
|
227
|
+
full_prompt += f"Conversation History\n {role.upper()}: {content}\n"
|
|
228
|
+
|
|
229
|
+
# Inject tool rules if tools exist
|
|
230
|
+
if tool_schemas:
|
|
231
|
+
full_prompt += """
|
|
232
|
+
Respond to user requests naturally, but if a tool must be invoked, respond with ONLY raw JSON that is parseable, with no code fences, no extra labels, and no commentary.
|
|
233
|
+
|
|
234
|
+
Rules:
|
|
235
|
+
|
|
236
|
+
1. If the request is "list tools", respond with a numbered list of all tools in this pattern:
|
|
237
|
+
1. <tool name>: <description>: <arguments>: <type> (local or remote)
|
|
238
|
+
|
|
239
|
+
2. When invoking a tool, respond ONLY with the JSON object in this exact format:
|
|
240
|
+
{
|
|
241
|
+
"tool": "<tool name>",
|
|
242
|
+
"action": "<action name>",
|
|
243
|
+
"args": {...}
|
|
244
|
+
}
|
|
245
|
+
Do NOT wrap this in markdown, code blocks, backticks, or any extra text. The JSON must be parseable directly.
|
|
246
|
+
|
|
247
|
+
3. Only use a tool if necessary. If no tool is needed, respond in plain language.
|
|
248
|
+
|
|
249
|
+
4. Use the tool arguments exactly as defined in the schema.
|
|
250
|
+
|
|
251
|
+
When a user requests a tool action, produce only the JSON object following the above format.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
# Expensive sending Tool Schemas on each Prompt
|
|
255
|
+
full_prompt += tools_block
|
|
256
|
+
|
|
257
|
+
# TOKEN OPTIMISATION - Cheaper to send tool index
|
|
258
|
+
# full_prompt += compact_index
|
|
259
|
+
|
|
260
|
+
# TOKEN OPTIMISATION - Removing whitespace to compact prompt
|
|
261
|
+
import textwrap
|
|
262
|
+
compact_prompt = " ".join(
|
|
263
|
+
line.strip() for line in textwrap.dedent(full_prompt).splitlines() if line.strip()
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if debug:
|
|
268
|
+
console.print(
|
|
269
|
+
Panel.fit(
|
|
270
|
+
Group(compact_prompt),
|
|
271
|
+
title=f"[bold white]Debug - Sent to {self.provider}/{self.model_id}[/bold white]",
|
|
272
|
+
border_style="white",
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Send prompt to model
|
|
277
|
+
with console.status(f"[green]{self.name.title()} is thinking...[/green]", spinner="dots"):
|
|
278
|
+
response = self.run(compact_prompt)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Try parsing JSON (tool invocation)
|
|
282
|
+
try:
|
|
283
|
+
# Clean JSON
|
|
284
|
+
text = response.get("text") if isinstance(response, dict) else response
|
|
285
|
+
cleaned = text.strip() if isinstance(text, str) else ""
|
|
286
|
+
|
|
287
|
+
if cleaned.startswith('```'):
|
|
288
|
+
cleaned = cleaned.split('```')[1]
|
|
289
|
+
if cleaned.startswith('json'):
|
|
290
|
+
cleaned = cleaned[4:]
|
|
291
|
+
cleaned = cleaned.strip()
|
|
292
|
+
|
|
293
|
+
# LLM JSON RESPONSE
|
|
294
|
+
data = json.loads(cleaned)
|
|
295
|
+
tool_name = data.get("tool")
|
|
296
|
+
action_name = data.get("action")
|
|
297
|
+
args = data.get("args", {})
|
|
298
|
+
tool = self.tools.get(tool_name)
|
|
299
|
+
|
|
300
|
+
if toolprompt:
|
|
301
|
+
panel_content = Group(
|
|
302
|
+
Text(f"Tool: {tool_name}", style="white"),
|
|
303
|
+
Text("Arguments:"),
|
|
304
|
+
Pretty(args)
|
|
305
|
+
)
|
|
306
|
+
console.print(
|
|
307
|
+
Panel.fit(
|
|
308
|
+
panel_content,
|
|
309
|
+
title="[bold yellow]Tool Invocation Requested[/bold yellow]",
|
|
310
|
+
border_style="yellow",
|
|
311
|
+
style="on rgb(40,30,0)"
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
prompt = Prompt.ask("Do you Approve? (y/n)", default="n")
|
|
315
|
+
|
|
316
|
+
if prompt.lower() in ["n", "no"]:
|
|
317
|
+
response = "Tool Invocation was cancelled"
|
|
318
|
+
else:
|
|
319
|
+
if tool:
|
|
320
|
+
# TOOL HANDLING
|
|
321
|
+
if tool.type == "internal":
|
|
322
|
+
# Local Function Tool
|
|
323
|
+
console.print(f"USING LOCAL TOOL: '{tool_name}' with args: {args}", style="white on green")
|
|
324
|
+
|
|
325
|
+
# Note: Need to call LLM with Tool Schema to obtain the proper invocation Args
|
|
326
|
+
|
|
327
|
+
tool_result = tool.invoke(args=args)
|
|
328
|
+
else:
|
|
329
|
+
# Local API Tool
|
|
330
|
+
console.print(f"USING LOCAL TOOL: '{tool_name}' action '{action_name}' with args: {args}", style="bold black on yellow")
|
|
331
|
+
|
|
332
|
+
# Note: Need to call LLM with Tool Schema to obtain the proper invocation Args
|
|
333
|
+
|
|
334
|
+
tool_result = tool.invoke(action_name, args)
|
|
335
|
+
else:
|
|
336
|
+
# Check MCP
|
|
337
|
+
if tool_name in mcp_tool_names:
|
|
338
|
+
# MCP SERVER TOOL
|
|
339
|
+
console.print(f"USING MCP SERVER TOOL: '{tool_name}' with args: {args}", style="bold black on yellow")
|
|
340
|
+
# tool_result = self.mcp_client.call_tool(tool_name, args)
|
|
341
|
+
server_name, actual_tool = tool_name.split(".", 1)
|
|
342
|
+
|
|
343
|
+
client = next(
|
|
344
|
+
(c for c in self.mcp_clients if c.name == server_name),
|
|
345
|
+
None
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if not client:
|
|
349
|
+
raise Exception(f"No MCP client found for server '{server_name}'")
|
|
350
|
+
|
|
351
|
+
# Note: Need to call LLM with Tool Schema to obtain the proper invocation Args
|
|
352
|
+
|
|
353
|
+
tool_result = client.call_tool(actual_tool, args)
|
|
354
|
+
|
|
355
|
+
# Minify JSON to avoid confusing model in next prompt
|
|
356
|
+
tool_result_str = json.dumps(tool_result, separators=(',', ':'))
|
|
357
|
+
|
|
358
|
+
# Add tool output to conversation history
|
|
359
|
+
self.conversation_history.append({"role": "tool", "content": tool_result_str})
|
|
360
|
+
|
|
361
|
+
# Ask model to display tool output naturally
|
|
362
|
+
analysis_prompt = "Display the following tool data in natural language:\n" + tool_result_str
|
|
363
|
+
with console.status(f"{self.name.title()} is synthesising tool response...", spinner="dots"):
|
|
364
|
+
response = self.run(analysis_prompt)
|
|
365
|
+
|
|
366
|
+
# Store agent response in history
|
|
367
|
+
if isinstance(response, dict):
|
|
368
|
+
self.conversation_history.append(
|
|
369
|
+
{"role": "agent", "content": response["text"]}
|
|
370
|
+
)
|
|
371
|
+
else:
|
|
372
|
+
self.conversation_history.append(
|
|
373
|
+
{"role": "agent", "content": response}
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
else:
|
|
377
|
+
if tool:
|
|
378
|
+
# TOOL HANDLING
|
|
379
|
+
if tool.type == "internal":
|
|
380
|
+
# Local Function Tool
|
|
381
|
+
console.print(f"USING LOCAL TOOL: '{tool_name}' with args: {args}", style="white on green")
|
|
382
|
+
|
|
383
|
+
# Note: Need to call LLM with Tool Schema to obtain the proper invocation Args
|
|
384
|
+
|
|
385
|
+
tool_result = tool.invoke(args=args)
|
|
386
|
+
else:
|
|
387
|
+
# Local API Tool
|
|
388
|
+
console.print(f"USING LOCAL TOOL: '{tool_name}' action '{action_name}' with args: {args}", style="bold black on yellow")
|
|
389
|
+
|
|
390
|
+
# Note: Need to call LLM with Tool Schema to obtain the proper invocation Args
|
|
391
|
+
|
|
392
|
+
tool_result = tool.invoke(action_name, args)
|
|
393
|
+
else:
|
|
394
|
+
# Check MCP
|
|
395
|
+
if tool_name in mcp_tool_names:
|
|
396
|
+
# MCP SERVER TOOL
|
|
397
|
+
console.print(f"USING MCP SERVER TOOL: '{tool_name}' with args: {args}", style="bold black on yellow")
|
|
398
|
+
# tool_result = self.mcp_client.call_tool(tool_name, args)
|
|
399
|
+
server_name, actual_tool = tool_name.split(".", 1)
|
|
400
|
+
|
|
401
|
+
client = next(
|
|
402
|
+
(c for c in self.mcp_clients if c.name == server_name),
|
|
403
|
+
None
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if not client:
|
|
407
|
+
raise Exception(f"No MCP client found for server '{server_name}'")
|
|
408
|
+
|
|
409
|
+
# Note: Need to call LLM with Tool Schema to obtain the proper invocation Args
|
|
410
|
+
|
|
411
|
+
tool_result = client.call_tool(actual_tool, args)
|
|
412
|
+
|
|
413
|
+
# Minify JSON to avoid confusing model in next prompt
|
|
414
|
+
tool_result_str = json.dumps(tool_result, separators=(',', ':'))
|
|
415
|
+
|
|
416
|
+
# Add tool output to conversation history
|
|
417
|
+
self.conversation_history.append({"role": "tool", "content": tool_result_str})
|
|
418
|
+
|
|
419
|
+
# Ask model to display tool output naturally
|
|
420
|
+
analysis_prompt = "Display the following tool data in natural language:\n" + tool_result_str
|
|
421
|
+
with console.status(f"{self.name.title()} is synthesising tool response...", spinner="dots"):
|
|
422
|
+
response = self.run(analysis_prompt)
|
|
423
|
+
|
|
424
|
+
# Store agent response in history
|
|
425
|
+
# self.conversation_history.append({"role": "agent", "content": response})
|
|
426
|
+
if isinstance(response, dict):
|
|
427
|
+
self.conversation_history.append(
|
|
428
|
+
{"role": "agent", "content": response["text"]}
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
self.conversation_history.append(
|
|
432
|
+
{"role": "agent", "content": response}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if isinstance(response, dict):
|
|
436
|
+
# Turn Token Usage
|
|
437
|
+
input_tokens = response["input_tokens"]
|
|
438
|
+
output_tokens = response["output_tokens"]
|
|
439
|
+
token_cost = response["token_cost"]
|
|
440
|
+
total = input_tokens + output_tokens
|
|
441
|
+
|
|
442
|
+
# Update Agent for cumulative input and output tokens
|
|
443
|
+
self.input_tokens += input_tokens
|
|
444
|
+
self.output_tokens += output_tokens
|
|
445
|
+
self.token_cost += token_cost
|
|
446
|
+
|
|
447
|
+
console.print(Panel.fit(response["text"], title="Agent Response", border_style="green"))
|
|
448
|
+
console.print(f"Token Usage: In: {response["input_tokens"]} Out: {response["output_tokens"]} Total: {total} Session Total: {self._get_total_tokens()} Cost: {round(self.token_cost,7)} USD")
|
|
449
|
+
else:
|
|
450
|
+
console.print(Panel.fit(response, title="Agent Response", border_style="green"))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
except (json.JSONDecodeError, ValueError):
|
|
454
|
+
# Treat as normal chat response
|
|
455
|
+
# self.conversation_history.append({"role": "agent", "content": response})
|
|
456
|
+
if isinstance(response, dict):
|
|
457
|
+
self.conversation_history.append(
|
|
458
|
+
{"role": "agent", "content": response["text"]}
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
self.conversation_history.append(
|
|
462
|
+
{"role": "agent", "content": response}
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
if isinstance(response, dict):
|
|
467
|
+
# Turn Token Usage
|
|
468
|
+
input_tokens = response["input_tokens"]
|
|
469
|
+
output_tokens = response["output_tokens"]
|
|
470
|
+
token_cost = response["token_cost"]
|
|
471
|
+
total = input_tokens + output_tokens
|
|
472
|
+
|
|
473
|
+
# Update Agent for cumulative input and output tokens
|
|
474
|
+
self.input_tokens += input_tokens
|
|
475
|
+
self.output_tokens += output_tokens
|
|
476
|
+
self.token_cost += token_cost
|
|
477
|
+
|
|
478
|
+
console.print(Panel.fit(response["text"], title="Agent Response", border_style="green"))
|
|
479
|
+
# console.print(f"Token Usage: In: {response["input_tokens"]} Out: {response["output_tokens"]} Total: {total} Session Total: {self._get_total_tokens()}")
|
|
480
|
+
console.print(f"Token Usage: In: {response["input_tokens"]} Out: {response["output_tokens"]} Total: {total} Session Total: {self._get_total_tokens()} Cost: {round(self.token_cost,7)} USD")
|
|
481
|
+
|
|
482
|
+
else:
|
|
483
|
+
console.print(Panel.fit(response, title="Agent Response", border_style="green"))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def create_agents(specs: list) -> dict[str, Agent]:
|
|
487
|
+
agents = {}
|
|
488
|
+
for spec in specs:
|
|
489
|
+
agent = create_agent(spec)
|
|
490
|
+
agents[agent.name] = agent
|
|
491
|
+
return agents
|
|
492
|
+
|
|
493
|
+
def create_agent(spec: dict, provider: str = None, model: str = None, agent_file: Path | None = None) -> Agent:
|
|
494
|
+
"""
|
|
495
|
+
Create an Agent from a YAML/spec dictionary, optionally overriding model or provider.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
name = spec.get("name")
|
|
499
|
+
description = spec.get("description")
|
|
500
|
+
version = spec.get("version")
|
|
501
|
+
role = spec.get("role")
|
|
502
|
+
|
|
503
|
+
model_spec = spec.get("model", {})
|
|
504
|
+
model_id = model or model_spec.get("id")
|
|
505
|
+
provider = provider or model_spec.get("provider")
|
|
506
|
+
api_key_env = model_spec.get("api_key_env")
|
|
507
|
+
|
|
508
|
+
if api_key_env:
|
|
509
|
+
api_key = os.getenv(api_key_env)
|
|
510
|
+
|
|
511
|
+
# Local Tools via tool.yaml or tool.yaml/tool.py within tools/
|
|
512
|
+
tool_names = spec.get("tools")
|
|
513
|
+
|
|
514
|
+
# Load MCP Server clients
|
|
515
|
+
mcp_clients = []
|
|
516
|
+
|
|
517
|
+
mcp_spec = spec.get("mcp", {})
|
|
518
|
+
servers = mcp_spec.get("servers",[])
|
|
519
|
+
|
|
520
|
+
for server in servers:
|
|
521
|
+
server_name = server.get("name")
|
|
522
|
+
endpoint = server.get("endpoint")
|
|
523
|
+
|
|
524
|
+
if endpoint:
|
|
525
|
+
client = MCPClientHTTP(
|
|
526
|
+
name=server_name,
|
|
527
|
+
endpoint=endpoint
|
|
528
|
+
)
|
|
529
|
+
mcp_clients.append(client)
|
|
530
|
+
|
|
531
|
+
agent = Agent(name=name, provider=provider, model_id=model_id, role=role, description=description, version=version, tool_names=tool_names, agent_file=agent_file, mcp_clients=mcp_clients)
|
|
532
|
+
|
|
533
|
+
return agent
|