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.
Files changed (73) hide show
  1. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/PKG-INFO +2 -2
  2. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/README.md +1 -1
  3. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/pyproject.toml +1 -1
  4. agentify_toolkit-0.21.0/src/agentify/agent.py +533 -0
  5. agentify_toolkit-0.21.0/src/agentify/cli.py +131 -0
  6. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/__init__.py +3 -1
  7. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/agent.py +1 -1
  8. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/deploy.py +1 -1
  9. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/gateway.py +1 -1
  10. agentify_toolkit-0.21.0/src/agentify/commands/mcp.py +168 -0
  11. agentify_toolkit-0.21.0/src/agentify/commands/provider.py +106 -0
  12. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/run.py +4 -4
  13. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/runtime.py +4 -4
  14. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/serve.py +1 -1
  15. agentify_toolkit-0.21.0/src/agentify/commands/tool.py +166 -0
  16. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/gateway/server.py +8 -3
  17. agentify_toolkit-0.21.0/src/agentify/headers.py +45 -0
  18. agentify_toolkit-0.21.0/src/agentify/mcp/__init__.py +2 -0
  19. agentify_toolkit-0.21.0/src/agentify/mcp/builtin_tools.py +47 -0
  20. agentify_toolkit-0.21.0/src/agentify/mcp/client.py +150 -0
  21. agentify_toolkit-0.21.0/src/agentify/mcp/registry.py +20 -0
  22. agentify_toolkit-0.21.0/src/agentify/mcp/server.py +178 -0
  23. agentify_toolkit-0.21.0/src/agentify/mcp_client.py +23 -0
  24. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/__init__.py +2 -1
  25. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/agentify.py +7 -3
  26. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/anthropic.py +20 -1
  27. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/bedrock.py +9 -2
  28. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/deepseek.py +7 -2
  29. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/github.py +16 -2
  30. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/google.py +14 -1
  31. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/mistral.py +15 -1
  32. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/ollama.py +7 -1
  33. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/ollama_local.py +7 -1
  34. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/openai.py +15 -1
  35. agentify_toolkit-0.21.0/src/agentify/providers/rate_card.py +60 -0
  36. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/x.py +14 -1
  37. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/runtime/server.py +4 -3
  38. agentify_toolkit-0.21.0/src/agentify/tool.py +215 -0
  39. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/utils/env_manager.py +4 -4
  40. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/PKG-INFO +2 -2
  41. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/SOURCES.txt +9 -1
  42. agentify_toolkit-0.18.2/src/agentify/_cli.py +0 -662
  43. agentify_toolkit-0.18.2/src/agentify/agent.py +0 -251
  44. agentify_toolkit-0.18.2/src/agentify/cli.py +0 -32
  45. agentify_toolkit-0.18.2/src/agentify/commands/provider.py +0 -51
  46. agentify_toolkit-0.18.2/src/agentify/commands/tool.py +0 -72
  47. agentify_toolkit-0.18.2/src/agentify/tool.py +0 -147
  48. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/LICENSE +0 -0
  49. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/NOTICE +0 -0
  50. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/setup.cfg +0 -0
  51. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/__init__.py +0 -0
  52. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/cli_config.py +0 -0
  53. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/cli_ui.py +0 -0
  54. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/commands/config.py +0 -0
  55. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/providers/backplane.py +0 -0
  56. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/runtime/__init__.py +0 -0
  57. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/runtime_client.py +0 -0
  58. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/server/__init__.py +0 -0
  59. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/server/server.py +0 -0
  60. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/specs.py +0 -0
  61. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/agent_list.css +0 -0
  62. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/agent_list.html +0 -0
  63. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/basic-chat.css +0 -0
  64. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/chat.css +0 -0
  65. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/chat.html +0 -0
  66. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/fun_agent_list.html +0 -0
  67. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/htmx.min.js +0 -0
  68. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/retro-chat.css +0 -0
  69. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify/ui/runtime_chat.html +0 -0
  70. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/dependency_links.txt +0 -0
  71. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/entry_points.txt +0 -0
  72. {agentify_toolkit-0.18.2 → agentify_toolkit-0.21.0}/src/agentify_toolkit.egg-info/requires.txt +0 -0
  73. {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.18.2
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
  ![Agentify Toolkit Logo](https://raw.githubusercontent.com/backplane-cloud/agentify-toolkit/main/agentify-logo-lg.png)
144
144
 
145
- ![agent](https://raw.githubusercontent.com/backplane-cloud/agentify-toolkit/main/agent.png)
145
+ ![agent](https://raw.githubusercontent.com/backplane-cloud/agentify-toolkit/main/cli.png)
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
  ![Agentify Toolkit Logo](https://raw.githubusercontent.com/backplane-cloud/agentify-toolkit/main/agentify-logo-lg.png)
11
11
 
12
- ![agent](https://raw.githubusercontent.com/backplane-cloud/agentify-toolkit/main/agent.png)
12
+ ![agent](https://raw.githubusercontent.com/backplane-cloud/agentify-toolkit/main/cli.png)
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentify_toolkit"
7
- version = "0.18.2"
7
+ version = "0.21.0"
8
8
  description = "Python Toolkit for Declarative AI Agent Development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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