agentify-toolkit 0.1.0__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.
agentify/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ Copyright 2026 Backplane Software
3
+ Author: Lewis Sheridan
4
+ License: Apache License, Version 2.0
5
+ Description: Lightweight Python toolkit to build multi-model AI agents.
6
+ """
7
+
8
+ from .agentify import Agent
9
+ from .agents import create_agent, create_agents
10
+ from .specs import load_agent_specs
11
+ from .cli_ui import show_agent_menu
12
+ from .web import run_web_ui
13
+
14
+ __all__ = [
15
+ "Agent",
16
+ "load_agent_specs",
17
+ "create_agent",
18
+ "create_agents",
19
+ "show_agent_menu",
20
+ "run_web_ui"
21
+ ]
agentify/agentify.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Copyright 2026 Backplane Software
3
+ Licensed under the Apache License, Version 2.0
4
+ Author: Lewis Sheridan
5
+ Description: Agentify class to build multi-model AI Agents
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Optional
10
+
11
+ from agentify.providers import run_openai, run_anthropic, run_google, run_bedrock, run_x
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.prompt import Prompt
15
+
16
+
17
+ @dataclass
18
+ class Agent:
19
+ name: str
20
+ description: str
21
+ provider: str
22
+ model_id: str
23
+ role: str
24
+ version: Optional[str] = field(default="0.0.0")
25
+
26
+
27
+ def get_model(self) -> str:
28
+ return self.model_id
29
+
30
+ def run(self, user_prompt: str) -> str:
31
+ match self.provider.lower():
32
+ case "openai":
33
+ return run_openai(self.model_id, user_prompt)
34
+ case "anthropic":
35
+ return run_anthropic(self.model_id, user_prompt)
36
+ case "google":
37
+ return run_google(self.model_id, user_prompt)
38
+ case "bedrock":
39
+ return run_bedrock(self.model_id, user_prompt)
40
+ case "x":
41
+ return run_x(self.model_id, user_prompt)
42
+ case _:
43
+ raise ValueError(f"Unsupported provider: {self.provider}")
44
+
45
+ def chat(agent: Agent):
46
+ console = Console()
47
+ console.print(Panel(
48
+ f"[bold cyan][/bold cyan]\n[bold cyan]{agent.name.upper()} [/bold cyan] [dim]{agent.version}[/dim]\nRole: {agent.description}\nUsing [yellow]{agent.model_id}[/yellow] by {agent.provider}",
49
+ border_style="cyan"
50
+ ))
51
+ while True:
52
+ prompt = Prompt.ask("\nEnter your prompt ('/exit' to quit)").strip()
53
+ if prompt.lower() in ["/exit", "quit"]:
54
+ console.print("[yellow]Exiting. Goodbye![/yellow]")
55
+ break
56
+
57
+
58
+ full_prompt = f"You must assume the role of {agent.role} when responding to this prompt:\n\n{prompt}"
59
+ with console.status(f"[blue]Sending prompt to model... {agent.name} is thinking...[/blue]", spinner="dots"):
60
+ response = agent.run(full_prompt)
61
+ console.print(Panel.fit(response, title="Agent Response", border_style="green"))
agentify/agents.py ADDED
@@ -0,0 +1,37 @@
1
+ # Copyright 2026 Backplane Software
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ from agentify import Agent
5
+ import os
6
+
7
+ def create_agents(specs: list) -> dict[str, Agent]:
8
+ agents = {}
9
+ for spec in specs:
10
+ agent = create_agent(spec)
11
+ agents[agent.name] = agent
12
+ return agents
13
+
14
+ def create_agent(spec: dict, provider: str = None, model: str = None) -> Agent:
15
+ """
16
+ Create an Agent from a YAML/spec dictionary, optionally overriding model or provider.
17
+ """
18
+ name = spec.get("name")
19
+ description = spec.get("description")
20
+ version = spec.get("version")
21
+ role = spec.get("role")
22
+
23
+ model_spec = spec.get("model", {})
24
+ model_id = model or model_spec.get("id")
25
+ provider = provider or model_spec.get("provider")
26
+ api_key_env = model_spec.get("api_key_env")
27
+
28
+ if api_key_env:
29
+ api_key = os.getenv(api_key_env)
30
+ # if not api_key:
31
+ # raise EnvironmentError(
32
+ # f"Environment variables '{api_key_env}' is not set"
33
+ # )
34
+
35
+ agent = Agent(name=name, provider=provider, model_id=model_id, role=role, description=description, version=version)
36
+
37
+ return agent
agentify/cli.py ADDED
@@ -0,0 +1,291 @@
1
+ # Copyright 2026 Backplane Software
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ import click
5
+ from pathlib import Path
6
+ import yaml
7
+ import os
8
+
9
+ from .specs import load_agent_specs
10
+ from .agents import create_agent, create_agents
11
+
12
+ from .cli_ui import show_agent_menu
13
+ from .cli_config import set_server, get_server, add_provider, remove_provider, list_providers
14
+ from .runtime_client import list_agents, upload_agent, delete_agent
15
+
16
+ from .web import run_web_ui
17
+
18
+ @click.group()
19
+ def main():
20
+ """Agentify - Declarative AI Agents and Runtime Management"""
21
+ pass
22
+
23
+ # -----------------------------
24
+ # Run local agents (existing logic)
25
+ # -----------------------------
26
+ @main.command()
27
+ @click.argument("path", required=False)
28
+ @click.option("--model", type=str, help="Override the model ID at runtime")
29
+ @click.option("--provider", type=str, help="Override the LLM provider at runtime")
30
+ @click.option("--server", type=str, help="Optional: run on a remote server instead of local")
31
+ @click.option("--web", is_flag=True, help="Run agent with web UI")
32
+ def run(path, provider, model, server, web):
33
+ """
34
+ Run an agent YAML file or a folder containing agent YAMLs.
35
+
36
+ PATH can be:
37
+ - A single YAML file → runs that agent directly
38
+ - A folder containing YAML files → presents a menu to select an agent
39
+ """
40
+ # Determine target path
41
+ agent_path = path or "./agents"
42
+ path = Path(agent_path)
43
+ click.echo(f"Loading agents from: {path}")
44
+
45
+ # If server override is provided, run via runtime API
46
+ if server:
47
+ if not path.is_file():
48
+ raise click.BadParameter("Remote run currently only supports a single YAML file")
49
+ resp = upload_agent(server, str(path))
50
+ click.echo(f"Agent uploaded and executed on server {server}: {resp}")
51
+ return
52
+
53
+ # ----- Local / programmatic agent logic -----
54
+ if path.is_file():
55
+ # Load YAML File
56
+ with open(path, "r") as f:
57
+ spec = yaml.safe_load(f)
58
+
59
+ agent = create_agent(spec, provider=provider, model=model)
60
+
61
+ if web:
62
+ run_web_ui(agent)
63
+ else:
64
+ agent.chat()
65
+
66
+ elif path.is_dir():
67
+ # Multi-agent mode
68
+ specs = load_agent_specs(path)
69
+ agents = create_agents(specs)
70
+ agent = show_agent_menu(agents)
71
+ agent.chat()
72
+ else:
73
+ raise click.BadParameter(f"Path does not exist: {path}")
74
+
75
+
76
+ # -----------------------------
77
+ # List local agents (interactive)
78
+ # -----------------------------
79
+ @main.command()
80
+ @click.argument("path", required=False)
81
+ def list(path):
82
+ """
83
+ List agents in a folder and select one to run (interactive TUI)
84
+ """
85
+ agent_path = path or "./agents"
86
+ path = Path(agent_path)
87
+ click.echo(f"Listing agents from: {path}")
88
+
89
+ if not path.is_dir():
90
+ raise click.BadParameter(f"Path is not a directory: {path}")
91
+
92
+ specs = load_agent_specs(path)
93
+ if not specs:
94
+ click.echo("No agent YAML files found.")
95
+ return
96
+
97
+ agents = create_agents(specs)
98
+ agent = show_agent_menu(agents)
99
+ agent.chat()
100
+
101
+
102
+ # -----------------------------
103
+ # Server configuration
104
+ # -----------------------------
105
+ @main.group()
106
+ def server():
107
+ """Manage default runtime server configuration"""
108
+ pass
109
+
110
+ @server.command("set")
111
+ @click.argument("url")
112
+ def server_set(url):
113
+ """Set the default runtime server"""
114
+ set_server(url)
115
+
116
+ @server.command("show")
117
+ def server_show():
118
+ """Show the current default runtime server"""
119
+ url = get_server()
120
+ if url:
121
+ click.echo(f"Default server: {url}")
122
+ else:
123
+ click.echo("No server configured.")
124
+
125
+ @main.group()
126
+ def config():
127
+ """View or manage Agentify configuration"""
128
+ pass
129
+
130
+ @config.command("show")
131
+ def config_show():
132
+ """Show current Agentify configuration"""
133
+ import json
134
+ from agentify.cli_config import get_server
135
+
136
+ config_data = {
137
+ "server": get_server()
138
+ }
139
+
140
+ click.echo(json.dumps(config_data, indent=4))
141
+
142
+
143
+ # -----------------------------
144
+ # Runtime server commands
145
+ # -----------------------------
146
+ @main.group()
147
+ def runtime():
148
+ """Manage agents on a remote runtime server"""
149
+ pass
150
+
151
+ @runtime.command("list")
152
+ @click.option("--server", default=None, help="Override default server URL")
153
+ def show_server_agents(server):
154
+ """List agents running on the runtime server"""
155
+ url = server or get_server()
156
+ if not url:
157
+ click.echo("No server configured. Use 'agentify server set <url>'")
158
+ return
159
+
160
+ agents = list_agents(url)
161
+ click.echo(f"{'NAME':20} {'STATUS':10} {'PROVIDER':10} {'MODEL'}")
162
+ for a in agents:
163
+ click.echo(f"{a['name']:20} {a['status']:10} {a['provider']:10} {a['model']}")
164
+
165
+ @runtime.command("show")
166
+ @click.argument("agent_name")
167
+ @click.option("--server", default=None, help="Override default server URL")
168
+ def runtime_show(agent_name, server):
169
+ """Show details of a specific agent on the runtime server"""
170
+ url = server or get_server()
171
+ if not url:
172
+ click.echo("No server configured. Use 'agentify server set <url>'")
173
+ return
174
+
175
+ agent = list_agents(url, filter_name=agent_name) # or implement get_agent_details(agent_name)
176
+ if not agent:
177
+ click.echo(f"Agent not found: {agent_name}")
178
+ return
179
+
180
+ # Print detailed info
181
+ for a in agent:
182
+ click.echo(f"Name: {a['name']}")
183
+ click.echo(f"Status: {a['status']}")
184
+ click.echo(f"Provider: {a['provider']}")
185
+ click.echo(f"Model: {a['model']}")
186
+ click.echo(f"Version: {a.get('version', 'N/A')}")
187
+ click.echo("-" * 40)
188
+
189
+ @runtime.command("load")
190
+ @click.argument("path")
191
+ @click.option("--server", default=None, help="Override default server URL")
192
+ def upload(path, server):
193
+ """Upload an agent YAML file to the runtime server"""
194
+ url = server or get_server()
195
+ if not url:
196
+ click.echo("No server configured. Use 'agentify server set <url>'")
197
+ return
198
+ resp = upload_agent(url, path)
199
+ click.echo(f"Uploaded {path} -> {url}: {resp}")
200
+
201
+
202
+ @runtime.command("delete")
203
+ @click.argument("agent_name")
204
+ @click.option("--server", default=None, help="Override default server URL")
205
+ def delete(agent_name, server):
206
+ """Delete an agent from the runtime server"""
207
+ url = server or get_server()
208
+ if not url:
209
+ click.echo("No server configured. Use 'agentify server set <url>'")
210
+ return
211
+ resp = delete_agent(url, agent_name)
212
+ click.echo(f"Deleted {agent_name} from {url}: {resp}")
213
+
214
+ @main.group()
215
+ def providers():
216
+ """Add/Remote AI Provider API Keys"""
217
+ pass
218
+
219
+ @providers.command("add")
220
+ @click.argument("provider")
221
+ @click.option("--env", "env_var", help="Environment variable name")
222
+ @click.option("--key", help="API key (optional, will prompt if omitted)")
223
+ def providers_add(provider, env_var, key):
224
+ """Add an AI Provider and API KEY"""
225
+
226
+ provider = provider.lower()
227
+ env_var = env_var or f"{provider.upper()}_API_KEY"
228
+
229
+ if not key:
230
+ key = click.prompt(
231
+ f"Enter API key for {provider}",
232
+ hide_input=True,
233
+ )
234
+
235
+ add_provider(provider, env_var)
236
+
237
+ click.echo(f"✓ Provider '{provider}' added to local config\n")
238
+ click.echo("To apply in your current shell, run:\n")
239
+ click.echo(f"export {env_var}={key}")
240
+
241
+ @providers.command("list")
242
+ def providers_list():
243
+ """List configured providers with their current status"""
244
+
245
+ providers = list_providers()
246
+
247
+ if not providers:
248
+ click.echo("No providers configured.")
249
+ return
250
+
251
+ # click.echo("Configured providers:\n")
252
+ # for name, cfg in providers.items():
253
+ # click.echo(f"• {name}")
254
+ # click.echo(f" env: {cfg['env']}\n")
255
+
256
+ click.echo("Configured providers:\n")
257
+ for name, cfg in providers.items():
258
+ env_var = cfg.get("env")
259
+
260
+ # Check if the env var is present in the current shell
261
+ if env_var in os.environ and os.environ[env_var]:
262
+ loaded = click.style("READY", fg="green") # green for ready
263
+ else:
264
+ loaded = click.style(
265
+ f"MISSING - run command: agentify providers add {name}", fg="yellow"
266
+ ) # yellow for not set
267
+
268
+ click.echo(f"• {name}")
269
+ click.echo(f" env: {env_var}")
270
+ click.echo(f" status: {loaded}\n")
271
+
272
+
273
+
274
+ @providers.command("remove")
275
+ @click.argument("provider")
276
+ def providers_remove(provider):
277
+ """Remove a configured providers"""
278
+
279
+ env_var = remove_provider(provider)
280
+
281
+ if not env_var:
282
+ click.echo(f"Provider not found: {provider}")
283
+ return
284
+
285
+ click.echo(f"✓ Provider '{provider}' removed from config\n")
286
+ click.echo("To remove from your current shell, run:\n")
287
+ click.echo(f"unset {env_var}")
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()
agentify/cli_config.py ADDED
@@ -0,0 +1,100 @@
1
+ import json
2
+ import yaml
3
+ from pathlib import Path
4
+
5
+ AGENTIFY_DIR = Path.home() / ".agentify"
6
+ CONFIG_PATH = Path.home() / ".agentify" / "config.json"
7
+ PROVIDERS_FILE = AGENTIFY_DIR / "providers.yaml"
8
+
9
+ def load_config():
10
+ if CONFIG_PATH.exists():
11
+ return json.loads(CONFIG_PATH.read_text())
12
+ return {}
13
+
14
+ def save_config(config: dict):
15
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
16
+ CONFIG_PATH.write_text(json.dumps(config, indent=2))
17
+
18
+ def set_server(url: str):
19
+ config = load_config()
20
+ config["server_url"] = url
21
+ save_config(config)
22
+ print(f"Default server set to {url}")
23
+
24
+ def get_server(default=None):
25
+ config = load_config()
26
+ return config.get("server_url", default)
27
+
28
+
29
+
30
+
31
+ # -----------------------------
32
+ # Provider config
33
+ # -----------------------------
34
+ def load_providers():
35
+ """
36
+ Load provider configuration from disk.
37
+
38
+ Returns:
39
+ dict: provider configuration
40
+ """
41
+ if not PROVIDERS_FILE.exists():
42
+ return {}
43
+
44
+ with open(PROVIDERS_FILE, "r") as f:
45
+ return yaml.safe_load(f) or {}
46
+
47
+
48
+ def save_providers(data: dict):
49
+ """
50
+ Persist provider configuration to disk.
51
+ """
52
+ AGENTIFY_DIR.mkdir(parents=True, exist_ok=True)
53
+ with open(PROVIDERS_FILE, "w") as f:
54
+ yaml.safe_dump(data, f)
55
+
56
+
57
+ def add_provider(provider: str, env_var: str):
58
+ """
59
+ Register a provider and its environment variable.
60
+ """
61
+ provider = provider.lower()
62
+
63
+ data = load_providers()
64
+ data.setdefault("providers", {})
65
+ data["providers"][provider] = {
66
+ "env": env_var
67
+ }
68
+
69
+ save_providers(data)
70
+
71
+
72
+ def remove_provider(provider: str):
73
+ """
74
+ Remove a provider from configuration.
75
+
76
+ Returns:
77
+ str | None: env var name if provider existed
78
+ """
79
+ provider = provider.lower()
80
+ data = load_providers()
81
+ providers = data.get("providers", {})
82
+
83
+ if provider not in providers:
84
+ return None
85
+
86
+ env_var = providers[provider]["env"]
87
+ del providers[provider]
88
+ save_providers(data)
89
+ return env_var
90
+
91
+
92
+ def list_providers():
93
+ """
94
+ Return configured providers.
95
+
96
+ Returns:
97
+ dict
98
+ """
99
+ data = load_providers()
100
+ return data.get("providers", {})
agentify/cli_ui.py ADDED
@@ -0,0 +1,44 @@
1
+ # Copyright 2026 Backplane Software
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ from rich.prompt import Prompt
7
+ from rich.table import Table
8
+
9
+ def show_agent_menu(agents: dict) -> "Agent":
10
+ console = Console()
11
+
12
+ table = Table(title="Available Agents", header_style="bold cyan")
13
+ table.add_column("#", style="yellow", justify="right")
14
+ table.add_column("AgentName", style="green")
15
+ table.add_column("Agent Version", style="dim")
16
+ table.add_column("Agent Role", style="dim")
17
+ table.add_column("AI Provider", style="dim")
18
+ table.add_column("LLM Model", style="dim")
19
+
20
+ agent_list = list(agents.values())
21
+
22
+ for i, agent in enumerate(agent_list, start=1):
23
+
24
+
25
+ table.add_row(
26
+ str(i),
27
+ agent.name,
28
+ agent.version,
29
+ agent.description,
30
+ agent.provider,
31
+ agent.model_id,
32
+ )
33
+
34
+ console.print(table)
35
+
36
+ while True:
37
+ choice = input("Select an agent: ").strip()
38
+ if choice.isdigit() and 1 <= int(choice) <= len(agent_list):
39
+ selected_agent = agent_list[int(choice) - 1]
40
+ return selected_agent
41
+ elif int(choice) == (len(agent_list) + 1):
42
+ console.print("Create custom Agent")
43
+ console.print("[red]Invalid selection[/red]")
44
+
@@ -0,0 +1,5 @@
1
+ from .openai import run_openai
2
+ from .anthropic import run_anthropic
3
+ from .google import run_google
4
+ from .bedrock import run_bedrock
5
+ from .x import run_x
@@ -0,0 +1,15 @@
1
+ from anthropic import Anthropic
2
+
3
+ def run_anthropic(model_id: str, user_prompt: str) -> str:
4
+ client = Anthropic()
5
+ message = client.messages.create(
6
+ model= model_id,
7
+ max_tokens=1024,
8
+ messages=[
9
+ {
10
+ "role": "user",
11
+ "content": user_prompt
12
+ }
13
+ ]
14
+ )
15
+ return message.content[0].text
@@ -0,0 +1,30 @@
1
+ import boto3
2
+ import json
3
+
4
+ def run_bedrock(model_id: str, user_prompt: str) -> str:
5
+ client = boto3.client(
6
+ service_name="bedrock-runtime",
7
+ region_name="eu-west-1"
8
+ )
9
+
10
+ body = {
11
+ "anthropic_version": "bedrock-2023-05-31",
12
+ "max_tokens": 1024,
13
+ "temperature": 0.2,
14
+ "messages": [
15
+ {
16
+ "role": "user",
17
+ "content": user_prompt
18
+ }
19
+ ]
20
+ }
21
+
22
+ response = client.invoke_model(
23
+ modelId=model_id,
24
+ body=json.dumps(body),
25
+ contentType="application/json",
26
+ accept="application/json"
27
+ )
28
+
29
+ response_body = json.loads(response["body"].read())
30
+ return response_body["content"][0]["text"]
@@ -0,0 +1,10 @@
1
+ from google import genai
2
+
3
+ def run_google(model_id: str, user_prompt: str) -> str:
4
+ client = genai.Client()
5
+ response = client.models.generate_content(
6
+ model=model_id,
7
+ contents=user_prompt
8
+ )
9
+
10
+ return response.text
@@ -0,0 +1,11 @@
1
+
2
+ from openai import OpenAI
3
+
4
+ def run_openai(model_id: str, user_prompt: str) -> str:
5
+ client = OpenAI()
6
+
7
+ response = client.responses.create(
8
+ model=model_id,
9
+ input=user_prompt
10
+ )
11
+ return response.output_text
@@ -0,0 +1,12 @@
1
+ import os
2
+ from xai_sdk import Client
3
+ from xai_sdk.chat import user, system
4
+
5
+ def run_x(model_id: str, user_prompt: str) -> str:
6
+ client = Client()
7
+ chat = client.chat.create(model=model_id)
8
+ chat.append(system("You are Grok, a highly intelligent, helpful AI assistant."))
9
+ chat.append(user(user_prompt))
10
+ response = chat.sample()
11
+
12
+ return response.content
@@ -0,0 +1,23 @@
1
+ import requests
2
+
3
+ def list_agents(server_url):
4
+ resp = requests.get(f"{server_url}/agents")
5
+ resp.raise_for_status()
6
+ return resp.json()
7
+
8
+ def upload_agent(server_url, yaml_file):
9
+ files = {'file': open(yaml_file, 'rb')}
10
+ resp = requests.post(f"{server_url}/agents", files=files)
11
+ resp.raise_for_status()
12
+ return resp.json()
13
+
14
+ def run_agent(server_url, yaml_file):
15
+ files = {'file': open(yaml_file, 'rb')}
16
+ resp = requests.post(f"{server_url}/agents/run", files=files)
17
+ resp.raise_for_status()
18
+ return resp.json()
19
+
20
+ def delete_agent(server_url, agent_name):
21
+ resp = requests.delete(f"{server_url}/agents/{agent_name}")
22
+ resp.raise_for_status()
23
+ return resp.json()
agentify/specs.py ADDED
@@ -0,0 +1,19 @@
1
+ # Copyright 2026 Backplane Software
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ from pathlib import Path
5
+ import yaml
6
+
7
+ def load_agent_specs(agent_dir: Path | str = "agents") -> list[dict]:
8
+ agent_dir = Path(agent_dir)
9
+ specs = []
10
+ for path in agent_dir.glob("*.yaml"):
11
+ with open(path, "r") as f:
12
+ spec = yaml.safe_load(f)
13
+ spec["_file"] = path.name # optional metadata
14
+ specs.append(spec)
15
+ return specs
16
+
17
+
18
+
19
+