antz-studio 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.
- antz/__init__.py +29 -0
- antz/cli.py +545 -0
- antz/client.py +91 -0
- antz/config.py +97 -0
- antz/db_connector.py +35 -0
- antz/decorators.py +250 -0
- antz/exceptions.py +35 -0
- antz/hive_mode.py +6 -0
- antz/privacy.py +24 -0
- antz_studio-0.1.0.dist-info/METADATA +34 -0
- antz_studio-0.1.0.dist-info/RECORD +14 -0
- antz_studio-0.1.0.dist-info/WHEEL +5 -0
- antz_studio-0.1.0.dist-info/entry_points.txt +2 -0
- antz_studio-0.1.0.dist-info/top_level.txt +1 -0
antz/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Antz Studio -- Colony SDK
|
|
2
|
+
# Sovereign Agentic OS - antz.studio
|
|
3
|
+
|
|
4
|
+
from antz.decorators import agent, tool, memory
|
|
5
|
+
from antz.client import AntzClient
|
|
6
|
+
from antz.config import AntzConfig
|
|
7
|
+
from antz.hive_mode import HiveMode
|
|
8
|
+
from antz.db_connector import db_connector
|
|
9
|
+
from antz.privacy import add_gaussian_noise
|
|
10
|
+
from antz.exceptions import (
|
|
11
|
+
AntzConnectionError,
|
|
12
|
+
ConstitutionViolationError,
|
|
13
|
+
AntzAuthError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"agent",
|
|
19
|
+
"tool",
|
|
20
|
+
"memory",
|
|
21
|
+
"HiveMode",
|
|
22
|
+
"db_connector",
|
|
23
|
+
"add_gaussian_noise",
|
|
24
|
+
"AntzClient",
|
|
25
|
+
"AntzConfig",
|
|
26
|
+
"AntzConnectionError",
|
|
27
|
+
"ConstitutionViolationError",
|
|
28
|
+
"AntzAuthError",
|
|
29
|
+
]
|
antz/cli.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Ant'z Studio — Sovereign Agentic OS CLI", add_completion=False)
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── antz login ────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def login(
|
|
22
|
+
nest: str = typer.Option(..., "--nest", help="Nest URL (e.g. http://nest.local:8001)"),
|
|
23
|
+
api_key: str = typer.Option(..., "--api-key", help="Nest API key"),
|
|
24
|
+
mode: str = typer.Option("isolated", "--mode", help="isolated | shared | sovereign"),
|
|
25
|
+
):
|
|
26
|
+
"""Authenticate with a Nest and save credentials locally."""
|
|
27
|
+
from antz.config import AntzConfig
|
|
28
|
+
from antz.client import AntzClient
|
|
29
|
+
|
|
30
|
+
console.print(f"[dim]Connecting to Nest at {nest}...[/dim]")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
cfg = AntzConfig(nest_url=nest, api_key=api_key, mode=mode)
|
|
34
|
+
client = AntzClient(cfg)
|
|
35
|
+
health = client.health()
|
|
36
|
+
|
|
37
|
+
AntzConfig.save_global(nest_url=nest, api_key=api_key, mode=mode)
|
|
38
|
+
|
|
39
|
+
console.print(Panel.fit(
|
|
40
|
+
f"[green]✓ Connected[/green]\n\n"
|
|
41
|
+
f" Nest: [cyan]{nest}[/cyan]\n"
|
|
42
|
+
f" Mode: [yellow]{mode}[/yellow]\n"
|
|
43
|
+
f" Version: {health.get('version', 'unknown')}\n\n"
|
|
44
|
+
f"Credentials saved to [dim]~/.antz/config.yaml[/dim]",
|
|
45
|
+
title="Ant'z Studio",
|
|
46
|
+
border_style="green",
|
|
47
|
+
))
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
console.print(f"[red]✗ Connection failed:[/red] {e}")
|
|
51
|
+
console.print("[dim]Hint: make sure the Nest is running and the API key is correct.[/dim]")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── antz status ───────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def status():
|
|
59
|
+
"""Check connection to the configured Nest."""
|
|
60
|
+
from antz.config import AntzConfig
|
|
61
|
+
from antz.client import AntzClient
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
cfg = AntzConfig.load()
|
|
65
|
+
client = AntzClient(cfg)
|
|
66
|
+
health = client.health()
|
|
67
|
+
|
|
68
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
69
|
+
table.add_row("[dim]Nest URL[/dim]", f"[cyan]{cfg.nest_url}[/cyan]")
|
|
70
|
+
table.add_row("[dim]Mode[/dim]", f"[yellow]{cfg.mode}[/yellow]")
|
|
71
|
+
table.add_row("[dim]Status[/dim]", "[green]● online[/green]")
|
|
72
|
+
table.add_row("[dim]Version[/dim]", health.get("version", "unknown"))
|
|
73
|
+
|
|
74
|
+
console.print(Panel(table, title="Ant'z Nest Status", border_style="green"))
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
console.print(f"[red]✗ Cannot reach Nest:[/red] {e}")
|
|
78
|
+
console.print("[dim]Run: antz login --nest <url> --api-key <key>[/dim]")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── antz init colony ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def init(
|
|
86
|
+
resource: str = typer.Argument(..., help="Resource type: colony"),
|
|
87
|
+
name: str = typer.Argument(..., help="Colony name (e.g. financial-credit-analysis)"),
|
|
88
|
+
framework: str = typer.Option("langgraph", "--framework", "-f",
|
|
89
|
+
help="langgraph | crewai | mixed"),
|
|
90
|
+
):
|
|
91
|
+
"""Scaffold a new Colony with the chosen framework template."""
|
|
92
|
+
if resource != "colony":
|
|
93
|
+
console.print(f"[red]Unknown resource '{resource}'. Currently supported: colony[/red]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
target = Path.cwd() / f"colony-{name}"
|
|
97
|
+
|
|
98
|
+
if target.exists():
|
|
99
|
+
console.print(f"[red]✗ Directory '{target.name}' already exists.[/red]")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
|
|
102
|
+
_scaffold_colony(name=name, framework=framework, target=target)
|
|
103
|
+
|
|
104
|
+
console.print(Panel.fit(
|
|
105
|
+
f"[green]✓ Colony scaffolded[/green]\n\n"
|
|
106
|
+
f" Name: [cyan]{name}[/cyan]\n"
|
|
107
|
+
f" Framework: [yellow]{framework}[/yellow]\n"
|
|
108
|
+
f" Directory: [dim]{target}[/dim]\n\n"
|
|
109
|
+
f"Next steps:\n"
|
|
110
|
+
f" [dim]cd colony-{name}[/dim]\n"
|
|
111
|
+
f" [dim]code .[/dim] # open in VSCode\n"
|
|
112
|
+
f" [dim]antz run --demo[/dim] # run the example agent",
|
|
113
|
+
title="Colony created",
|
|
114
|
+
border_style="cyan",
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── antz run ──────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def run(
|
|
122
|
+
input_file: Optional[Path] = typer.Option(None, "--input", "-i",
|
|
123
|
+
help="JSON input file for the agent"),
|
|
124
|
+
demo: bool = typer.Option(False, "--demo", help="Run the built-in demo agent"),
|
|
125
|
+
colony_dir: Optional[Path] = typer.Option(None, "--colony", "-c",
|
|
126
|
+
help="Colony directory (default: cwd)"),
|
|
127
|
+
):
|
|
128
|
+
"""Execute the Colony agent."""
|
|
129
|
+
cwd = colony_dir or Path.cwd()
|
|
130
|
+
cfg_file = cwd / "antz.config.yaml"
|
|
131
|
+
|
|
132
|
+
if not cfg_file.exists():
|
|
133
|
+
console.print(
|
|
134
|
+
"[red]✗ No antz.config.yaml found.[/red]\n"
|
|
135
|
+
"[dim]Run from inside a Colony directory, or use: antz init colony <name>[/dim]"
|
|
136
|
+
)
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
|
|
139
|
+
with open(cfg_file) as f:
|
|
140
|
+
colony_cfg = yaml.safe_load(f) or {}
|
|
141
|
+
|
|
142
|
+
colony_id = colony_cfg.get("colony_id", "unknown")
|
|
143
|
+
entrypoint = colony_cfg.get("entrypoint", "agents/main.py")
|
|
144
|
+
|
|
145
|
+
console.print(f"[dim]Running Colony [cyan]{colony_id}[/cyan]...[/dim]")
|
|
146
|
+
|
|
147
|
+
if demo:
|
|
148
|
+
_run_demo(cwd, colony_cfg)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
entry_path = cwd / entrypoint
|
|
152
|
+
if not entry_path.exists():
|
|
153
|
+
console.print(f"[red]✗ Entrypoint not found: {entry_path}[/red]")
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
cmd = [sys.executable, str(entry_path)]
|
|
157
|
+
if input_file:
|
|
158
|
+
cmd += ["--input", str(input_file)]
|
|
159
|
+
|
|
160
|
+
result = subprocess.run(cmd, cwd=str(cwd))
|
|
161
|
+
raise typer.Exit(result.returncode)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── antz version ─────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
@app.command()
|
|
167
|
+
def version():
|
|
168
|
+
"""Show SDK version."""
|
|
169
|
+
from antz import __version__
|
|
170
|
+
console.print(f"antz-sdk [cyan]{__version__}[/cyan]")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── Internal helpers ──────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
def _scaffold_colony(name: str, framework: str, target: Path) -> None:
|
|
176
|
+
"""Create Colony directory structure from template."""
|
|
177
|
+
target.mkdir(parents=True)
|
|
178
|
+
|
|
179
|
+
(target / "agents").mkdir()
|
|
180
|
+
(target / "tools").mkdir()
|
|
181
|
+
(target / "workflows").mkdir()
|
|
182
|
+
(target / "tests").mkdir()
|
|
183
|
+
|
|
184
|
+
# antz.config.yaml
|
|
185
|
+
config = {
|
|
186
|
+
"colony_id": name,
|
|
187
|
+
"framework": framework,
|
|
188
|
+
"entrypoint": "agents/main.py",
|
|
189
|
+
"nest_url": "http://localhost:8001",
|
|
190
|
+
"mode": "isolated",
|
|
191
|
+
}
|
|
192
|
+
with open(target / "antz.config.yaml", "w", encoding="utf-8") as f:
|
|
193
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
194
|
+
|
|
195
|
+
# constitution.yaml
|
|
196
|
+
constitution = {
|
|
197
|
+
"agent_id": f"{name}-v1",
|
|
198
|
+
"version": "1.0",
|
|
199
|
+
"allowed_tools": [],
|
|
200
|
+
"prohibited_actions": ["delete_record", "export_data", "modify_audit_log"],
|
|
201
|
+
"spend_limits": {"max_per_action_usd": 5.0, "max_per_day_usd": 500.0},
|
|
202
|
+
"data_policy": {
|
|
203
|
+
"can_send_outside_nest": False,
|
|
204
|
+
"can_log_pii": False,
|
|
205
|
+
"audit_all_tool_calls": True,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
with open(target / "constitution.yaml", "w", encoding="utf-8") as f:
|
|
209
|
+
yaml.dump(constitution, f, default_flow_style=False)
|
|
210
|
+
|
|
211
|
+
# agents/main.py
|
|
212
|
+
agent_code = _agent_template(name=name, framework=framework)
|
|
213
|
+
with open(target / "agents" / "main.py", "w", encoding="utf-8") as f:
|
|
214
|
+
f.write(agent_code)
|
|
215
|
+
|
|
216
|
+
# README
|
|
217
|
+
with open(target / "README.md", "w", encoding="utf-8") as f:
|
|
218
|
+
f.write(_readme_template(name=name, framework=framework))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _agent_template(name: str, framework: str) -> str:
|
|
222
|
+
safe_name = name.replace("-", "_")
|
|
223
|
+
|
|
224
|
+
if framework == "langgraph":
|
|
225
|
+
return f'''# Colony: {name}
|
|
226
|
+
# Framework: LangGraph
|
|
227
|
+
# Ant'z SDK injects: observability, audit, constitution validation, memory
|
|
228
|
+
|
|
229
|
+
from antz import agent, tool, memory
|
|
230
|
+
from typing import TypedDict
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── State ─────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
class AgentState(TypedDict):
|
|
236
|
+
input: dict
|
|
237
|
+
result: dict
|
|
238
|
+
status: str
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ── Tools ─────────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
@tool("{safe_name}-lookup")
|
|
244
|
+
def lookup_data(query: str) -> dict:
|
|
245
|
+
"""
|
|
246
|
+
Replace with your actual tool logic.
|
|
247
|
+
Ant'z automatically logs this call and validates against constitution.
|
|
248
|
+
"""
|
|
249
|
+
return {{"data": f"mock result for: {{query}}", "confidence": 0.9}}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ── Main agent ────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
@agent("{safe_name}-v1")
|
|
255
|
+
@memory(namespace="{safe_name}")
|
|
256
|
+
def run_agent(input_data: dict, memory_context: list = None) -> dict:
|
|
257
|
+
"""
|
|
258
|
+
Main agent entrypoint.
|
|
259
|
+
memory_context is auto-populated with relevant past runs from Hive.
|
|
260
|
+
Replace the logic below with your LangGraph workflow.
|
|
261
|
+
"""
|
|
262
|
+
past = memory_context or []
|
|
263
|
+
context = past[0]["content"] if past else "No prior context."
|
|
264
|
+
|
|
265
|
+
result = lookup_data(str(input_data))
|
|
266
|
+
|
|
267
|
+
return {{
|
|
268
|
+
"status": "success",
|
|
269
|
+
"colony": "{name}",
|
|
270
|
+
"result": result,
|
|
271
|
+
"memory_hits": len(past),
|
|
272
|
+
"context_used": context,
|
|
273
|
+
}}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ── Demo entrypoint ───────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
import json, sys
|
|
280
|
+
|
|
281
|
+
sample_input = {{"query": "demo run", "source": "antz-cli"}}
|
|
282
|
+
|
|
283
|
+
if "--input" in sys.argv:
|
|
284
|
+
idx = sys.argv.index("--input")
|
|
285
|
+
with open(sys.argv[idx + 1]) as f:
|
|
286
|
+
sample_input = json.load(f)
|
|
287
|
+
|
|
288
|
+
output = run_agent(sample_input)
|
|
289
|
+
print(json.dumps(output, indent=2))
|
|
290
|
+
'''
|
|
291
|
+
|
|
292
|
+
elif framework == "crewai":
|
|
293
|
+
return f'''# Colony: {name}
|
|
294
|
+
# Framework: CrewAI
|
|
295
|
+
# Ant'z SDK injects: observability, audit, constitution validation, memory
|
|
296
|
+
|
|
297
|
+
from antz import agent, tool, memory
|
|
298
|
+
from crewai import Agent, Task, Crew
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ── Tools ─────────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
@tool("{safe_name}-lookup")
|
|
304
|
+
def lookup_data(query: str) -> str:
|
|
305
|
+
"""Replace with your actual tool logic."""
|
|
306
|
+
return f"mock result for: {{query}}"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ── Crew definition ───────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
def build_crew():
|
|
312
|
+
analyst = Agent(
|
|
313
|
+
role="Data Analyst",
|
|
314
|
+
goal="Analyze input data and produce a structured report",
|
|
315
|
+
backstory="Expert analyst with deep domain knowledge",
|
|
316
|
+
verbose=False,
|
|
317
|
+
)
|
|
318
|
+
task = Task(
|
|
319
|
+
description="Analyze the provided data and return a structured summary.",
|
|
320
|
+
expected_output="A JSON-formatted analysis report",
|
|
321
|
+
agent=analyst,
|
|
322
|
+
)
|
|
323
|
+
return Crew(agents=[analyst], tasks=[task], verbose=False)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ── Main agent ────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
@agent("{safe_name}-v1")
|
|
329
|
+
@memory(namespace="{safe_name}")
|
|
330
|
+
def run_agent(input_data: dict, memory_context: list = None) -> dict:
|
|
331
|
+
"""
|
|
332
|
+
Main agent entrypoint.
|
|
333
|
+
memory_context is auto-populated with relevant past runs from Hive.
|
|
334
|
+
"""
|
|
335
|
+
past = memory_context or []
|
|
336
|
+
crew = build_crew()
|
|
337
|
+
result = crew.kickoff(inputs=input_data)
|
|
338
|
+
return {{
|
|
339
|
+
"status": "success",
|
|
340
|
+
"colony": "{name}",
|
|
341
|
+
"result": str(result),
|
|
342
|
+
"memory_hits": len(past),
|
|
343
|
+
}}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ── Demo entrypoint ───────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
import json, sys
|
|
350
|
+
|
|
351
|
+
sample_input = {{"query": "demo run", "source": "antz-cli"}}
|
|
352
|
+
|
|
353
|
+
if "--input" in sys.argv:
|
|
354
|
+
idx = sys.argv.index("--input")
|
|
355
|
+
with open(sys.argv[idx + 1]) as f:
|
|
356
|
+
sample_input = json.load(f)
|
|
357
|
+
|
|
358
|
+
output = run_agent(sample_input)
|
|
359
|
+
print(json.dumps(output, indent=2))
|
|
360
|
+
'''
|
|
361
|
+
|
|
362
|
+
else: # mixed
|
|
363
|
+
return f'''# Colony: {name}
|
|
364
|
+
# Framework: Mixed (LangGraph + raw LiteLLM)
|
|
365
|
+
# Ant'z SDK injects: observability, audit, constitution validation, memory
|
|
366
|
+
|
|
367
|
+
from antz import agent, tool, memory
|
|
368
|
+
import httpx
|
|
369
|
+
import os
|
|
370
|
+
|
|
371
|
+
LITELLM_URL = os.getenv("ANTZ_LITELLM_URL", "http://localhost:4000")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ── Tools ─────────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
@tool("{safe_name}-llm-call")
|
|
377
|
+
def call_llm(prompt: str) -> str:
|
|
378
|
+
"""Direct LiteLLM call through the Nest — stays sovereign."""
|
|
379
|
+
r = httpx.post(
|
|
380
|
+
f"{{LITELLM_URL}}/chat/completions",
|
|
381
|
+
json={{
|
|
382
|
+
"model": "antz-default",
|
|
383
|
+
"messages": [{{"role": "user", "content": prompt}}],
|
|
384
|
+
"max_tokens": 512,
|
|
385
|
+
}},
|
|
386
|
+
timeout=30,
|
|
387
|
+
)
|
|
388
|
+
r.raise_for_status()
|
|
389
|
+
return r.json()["choices"][0]["message"]["content"]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ── Main agent ────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
@agent("{safe_name}-v1")
|
|
395
|
+
@memory(namespace="{safe_name}")
|
|
396
|
+
def run_agent(input_data: dict, memory_context: list = None) -> dict:
|
|
397
|
+
"""
|
|
398
|
+
Main agent with memory context from Hive.
|
|
399
|
+
memory_context is auto-populated with relevant past runs.
|
|
400
|
+
"""
|
|
401
|
+
past = memory_context or []
|
|
402
|
+
context_str = "\\n".join(str(m) for m in past[:3]) if past else "No prior context."
|
|
403
|
+
|
|
404
|
+
prompt = f"""
|
|
405
|
+
Context from previous runs:
|
|
406
|
+
{{context_str}}
|
|
407
|
+
|
|
408
|
+
Current input: {{input_data}}
|
|
409
|
+
|
|
410
|
+
Analyze and respond with a structured result.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
response = call_llm(prompt)
|
|
414
|
+
|
|
415
|
+
return {{
|
|
416
|
+
"status": "success",
|
|
417
|
+
"colony": "{name}",
|
|
418
|
+
"result": response,
|
|
419
|
+
"memory_hits": len(past),
|
|
420
|
+
}}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ── Demo entrypoint ───────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
import json, sys
|
|
427
|
+
|
|
428
|
+
sample_input = {{"query": "demo run", "source": "antz-cli"}}
|
|
429
|
+
|
|
430
|
+
if "--input" in sys.argv:
|
|
431
|
+
idx = sys.argv.index("--input")
|
|
432
|
+
with open(sys.argv[idx + 1]) as f:
|
|
433
|
+
sample_input = json.load(f)
|
|
434
|
+
|
|
435
|
+
output = run_agent(sample_input)
|
|
436
|
+
print(json.dumps(output, indent=2))
|
|
437
|
+
'''
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _readme_template(name: str, framework: str) -> str:
|
|
441
|
+
return f"""# Colony: {name}
|
|
442
|
+
|
|
443
|
+
Framework: `{framework}`
|
|
444
|
+
|
|
445
|
+
## Run
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# Demo (no input required)
|
|
449
|
+
antz run --demo
|
|
450
|
+
|
|
451
|
+
# With input file
|
|
452
|
+
antz run --input data/sample.json
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Structure
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
agents/main.py # Main agent logic — edit this
|
|
459
|
+
tools/ # Add your tools here
|
|
460
|
+
workflows/ # Workflow definitions
|
|
461
|
+
constitution.yaml # Agent behavioral constraints
|
|
462
|
+
antz.config.yaml # Colony configuration
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Constitution
|
|
466
|
+
|
|
467
|
+
Edit `constitution.yaml` to define what this agent can and cannot do.
|
|
468
|
+
The Ant'z Hive validates every tool call against these rules at runtime.
|
|
469
|
+
|
|
470
|
+
## Observability
|
|
471
|
+
|
|
472
|
+
All executions are automatically traced. View them in the Nest Grafana dashboard.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _run_demo(cwd: Path, colony_cfg: dict) -> None:
|
|
477
|
+
"""Run the demo mode — executes agents/main.py with sample data."""
|
|
478
|
+
entry = cwd / "agents" / "main.py"
|
|
479
|
+
if not entry.exists():
|
|
480
|
+
console.print("[red]✗ agents/main.py not found.[/red]")
|
|
481
|
+
raise typer.Exit(1)
|
|
482
|
+
|
|
483
|
+
console.print("[dim]Running demo agent...[/dim]\n")
|
|
484
|
+
result = subprocess.run([sys.executable, str(entry)], cwd=str(cwd))
|
|
485
|
+
raise typer.Exit(result.returncode)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# -- antz nest ----------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
@app.command("nest")
|
|
493
|
+
def nest_cmd(
|
|
494
|
+
action: str = typer.Argument(..., help="status"),
|
|
495
|
+
tenant: str = typer.Option(..., "--tenant", "-t", help="Tenant ID"),
|
|
496
|
+
watch: bool = typer.Option(False, "--watch", "-w"),
|
|
497
|
+
api_key: str = typer.Option(None, "--api-key"),
|
|
498
|
+
):
|
|
499
|
+
"""Manage Nest provisioning and status."""
|
|
500
|
+
import httpx, os, time
|
|
501
|
+
cp_url = os.getenv("ANTZ_CONTROL_PLANE_URL", "https://control.antz.studio")
|
|
502
|
+
if not api_key:
|
|
503
|
+
api_key = typer.prompt("Control Plane API Key")
|
|
504
|
+
STATUS = {
|
|
505
|
+
"pending": "Aguardando...", "provisioning": "Iniciando...",
|
|
506
|
+
"creating_vm": "Criando VM...", "vm_created": "VM criada...",
|
|
507
|
+
"wireguard_ready": "WireGuard pronto...", "provisioning_nest": "Instalando Nest...",
|
|
508
|
+
"sending_email": "Enviando email...", "active": "Nest ativo!", "failed": "Falhou",
|
|
509
|
+
}
|
|
510
|
+
def get():
|
|
511
|
+
try:
|
|
512
|
+
r = httpx.get(f"{cp_url}/tenants/{tenant}", headers={"x-api-key": api_key}, timeout=10)
|
|
513
|
+
return r.json() if r.status_code == 200 else None
|
|
514
|
+
except Exception:
|
|
515
|
+
return None
|
|
516
|
+
data = get()
|
|
517
|
+
if not data:
|
|
518
|
+
console.print(f"[red]Tenant {tenant!r} nao encontrado.[/red]")
|
|
519
|
+
raise typer.Exit(1)
|
|
520
|
+
if not watch:
|
|
521
|
+
console.print(f"Tenant: [cyan]{tenant}[/cyan]")
|
|
522
|
+
console.print(f"Status: {STATUS.get(data.get('status','?'), data.get('status','?'))}")
|
|
523
|
+
if data.get("nest_url"):
|
|
524
|
+
console.print(f"URL: [green]{data['nest_url']}[/green]")
|
|
525
|
+
return
|
|
526
|
+
last = None
|
|
527
|
+
while True:
|
|
528
|
+
data = get()
|
|
529
|
+
if not data:
|
|
530
|
+
time.sleep(10); continue
|
|
531
|
+
s = data.get("status", "")
|
|
532
|
+
if s != last:
|
|
533
|
+
console.print(STATUS.get(s, s)); last = s
|
|
534
|
+
if s == "active":
|
|
535
|
+
url = data.get("nest_url", "")
|
|
536
|
+
console.print(f"[green]Nest ativo em {url}[/green]")
|
|
537
|
+
console.print(f"[dim]antz login --nest {url} --api-key SUA_CHAVE[/dim]")
|
|
538
|
+
raise typer.Exit(0)
|
|
539
|
+
if s == "failed":
|
|
540
|
+
console.print("[red]Falhou.[/red]"); raise typer.Exit(1)
|
|
541
|
+
time.sleep(15)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
app()
|
antz/client.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from antz.config import AntzConfig
|
|
7
|
+
from antz.exceptions import AntzConnectionError, AntzAuthError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AntzClient:
|
|
11
|
+
"""
|
|
12
|
+
Low-level HTTP client for Nest and Hive APIs.
|
|
13
|
+
Used internally by decorators — developers rarely call this directly.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Optional[AntzConfig] = None):
|
|
17
|
+
self.config = config or AntzConfig.load()
|
|
18
|
+
self._headers = {
|
|
19
|
+
"Authorization": f"Bearer {self.config.api_key}",
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"X-Antz-Mode": self.config.mode,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# ── Nest endpoints ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
def health(self) -> dict:
|
|
27
|
+
"""Ping the Nest health endpoint."""
|
|
28
|
+
return self._get(f"{self.config.nest_url}/health")
|
|
29
|
+
|
|
30
|
+
def validate_constitution(self, agent_id: str, action: str, payload: dict) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
Ask the Nest constitution engine whether an action is permitted.
|
|
33
|
+
Returns: {"allowed": bool, "rule_id": str | None, "message": str}
|
|
34
|
+
"""
|
|
35
|
+
return self._post(
|
|
36
|
+
f"{self.config.nest_url}/constitution/validate",
|
|
37
|
+
{"agent_id": agent_id, "action": action, "payload": payload},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def log_audit(self, event: dict) -> dict:
|
|
41
|
+
"""Write an immutable audit event to the Nest append-only DB."""
|
|
42
|
+
return self._post(f"{self.config.nest_url}/audit/log", event)
|
|
43
|
+
|
|
44
|
+
# ── Hive endpoints ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def memory_store(self, namespace: str, agent_id: str, content: str, metadata: dict) -> dict:
|
|
47
|
+
base = self.config.hive_url or self.config.nest_url
|
|
48
|
+
return self._post(
|
|
49
|
+
f"{base}/memory/store",
|
|
50
|
+
{"namespace": namespace, "agent_id": agent_id,
|
|
51
|
+
"content": content, "metadata": metadata},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def memory_search(self, namespace: str, query: str, top_k: int = 5) -> list[dict]:
|
|
55
|
+
base = self.config.hive_url or self.config.nest_url
|
|
56
|
+
result = self._get(
|
|
57
|
+
f"{base}/memory/search",
|
|
58
|
+
params={"namespace": namespace, "query": query, "top_k": top_k},
|
|
59
|
+
)
|
|
60
|
+
return result.get("results", [])
|
|
61
|
+
|
|
62
|
+
# ── Internal helpers ──────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
def _get(self, url: str, params: Optional[dict] = None) -> Any:
|
|
65
|
+
try:
|
|
66
|
+
r = httpx.get(url, headers=self._headers, params=params, timeout=10)
|
|
67
|
+
self._raise_for_status(r)
|
|
68
|
+
return r.json()
|
|
69
|
+
except httpx.ConnectError as e:
|
|
70
|
+
raise AntzConnectionError(f"Cannot reach Nest at {self.config.nest_url}: {e}") from e
|
|
71
|
+
|
|
72
|
+
def _post(self, url: str, body: dict) -> Any:
|
|
73
|
+
try:
|
|
74
|
+
r = httpx.post(url, headers=self._headers, json=body, timeout=15)
|
|
75
|
+
self._raise_for_status(r)
|
|
76
|
+
return r.json()
|
|
77
|
+
except httpx.ConnectError as e:
|
|
78
|
+
raise AntzConnectionError(f"Cannot reach Nest at {self.config.nest_url}: {e}") from e
|
|
79
|
+
|
|
80
|
+
def _raise_for_status(self, r: httpx.Response) -> None:
|
|
81
|
+
if r.status_code == 401:
|
|
82
|
+
raise AntzAuthError("Invalid API key. Run: antz login --nest <url> --api-key <key>")
|
|
83
|
+
if r.status_code >= 400:
|
|
84
|
+
raise AntzConnectionError(f"Nest returned {r.status_code}: {r.text}")
|
|
85
|
+
|
|
86
|
+
def sync_to_hive_central(self, namespace: str) -> dict:
|
|
87
|
+
"""Trigger sync of anonymized vectors to Hive Central."""
|
|
88
|
+
return self._post(
|
|
89
|
+
f"{self.config.nest_url}/sync/push",
|
|
90
|
+
{"namespace": namespace, "top_k": 100},
|
|
91
|
+
)
|
antz/config.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ── Config file locations ─────────────────────────────────────────────────────
|
|
12
|
+
GLOBAL_CONFIG_PATH = Path.home() / ".antz" / "config.yaml"
|
|
13
|
+
LOCAL_CONFIG_NAME = "antz.config.yaml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AntzConfig(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Resolved configuration for the SDK.
|
|
19
|
+
Priority: env vars > local antz.config.yaml > ~/.antz/config.yaml
|
|
20
|
+
"""
|
|
21
|
+
nest_url: str
|
|
22
|
+
api_key: str
|
|
23
|
+
colony_id: Optional[str] = None
|
|
24
|
+
mode: str = "isolated" # isolated | shared | sovereign
|
|
25
|
+
hive_url: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@field_validator("mode")
|
|
28
|
+
@classmethod
|
|
29
|
+
def validate_mode(cls, v: str) -> str:
|
|
30
|
+
allowed = {"isolated", "shared", "sovereign"}
|
|
31
|
+
if v not in allowed:
|
|
32
|
+
raise ValueError(f"mode must be one of {allowed}, got '{v}'")
|
|
33
|
+
return v
|
|
34
|
+
|
|
35
|
+
@field_validator("nest_url")
|
|
36
|
+
@classmethod
|
|
37
|
+
def strip_trailing_slash(cls, v: str) -> str:
|
|
38
|
+
return v.rstrip("/")
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def load(cls, colony_dir: Optional[Path] = None) -> "AntzConfig":
|
|
42
|
+
"""
|
|
43
|
+
Load config in priority order:
|
|
44
|
+
1. Environment variables
|
|
45
|
+
2. Local antz.config.yaml (in colony_dir or cwd)
|
|
46
|
+
3. Global ~/.antz/config.yaml
|
|
47
|
+
"""
|
|
48
|
+
data: dict = {}
|
|
49
|
+
|
|
50
|
+
# 3. Global config (lowest priority)
|
|
51
|
+
if GLOBAL_CONFIG_PATH.exists():
|
|
52
|
+
data.update(_read_yaml(GLOBAL_CONFIG_PATH))
|
|
53
|
+
|
|
54
|
+
# 2. Local colony config
|
|
55
|
+
search_dir = colony_dir or Path.cwd()
|
|
56
|
+
local_cfg = search_dir / LOCAL_CONFIG_NAME
|
|
57
|
+
if local_cfg.exists():
|
|
58
|
+
data.update(_read_yaml(local_cfg))
|
|
59
|
+
|
|
60
|
+
# 1. Environment variables (highest priority)
|
|
61
|
+
if nest_url := os.getenv("ANTZ_NEST_URL"):
|
|
62
|
+
data["nest_url"] = nest_url
|
|
63
|
+
if api_key := os.getenv("ANTZ_API_KEY"):
|
|
64
|
+
data["api_key"] = api_key
|
|
65
|
+
if mode := os.getenv("ANTZ_MODE"):
|
|
66
|
+
data["mode"] = mode
|
|
67
|
+
if hive_url := os.getenv("ANTZ_HIVE_URL"):
|
|
68
|
+
data["hive_url"] = hive_url
|
|
69
|
+
|
|
70
|
+
if not data.get("nest_url") or not data.get("api_key"):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"Missing Ant'z credentials.\n"
|
|
73
|
+
"Run: antz login --nest <url> --api-key <key>\n"
|
|
74
|
+
"Or set ANTZ_NEST_URL and ANTZ_API_KEY environment variables."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return cls(**data)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def save_global(cls, nest_url: str, api_key: str, mode: str = "isolated") -> None:
|
|
81
|
+
"""Persist credentials to ~/.antz/config.yaml (used by `antz login`)."""
|
|
82
|
+
GLOBAL_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
payload = {"nest_url": nest_url, "api_key": api_key, "mode": mode}
|
|
84
|
+
with open(GLOBAL_CONFIG_PATH, "w") as f:
|
|
85
|
+
yaml.dump(payload, f, default_flow_style=False)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_yaml(path: Path) -> dict:
|
|
89
|
+
with open(path) as f:
|
|
90
|
+
content = f.read()
|
|
91
|
+
# Resolve ${VAR} patterns
|
|
92
|
+
import re
|
|
93
|
+
def replace_env(match):
|
|
94
|
+
var = match.group(1)
|
|
95
|
+
return os.getenv(var, match.group(0))
|
|
96
|
+
content = re.sub(r'\$\{([^}]+)\}', replace_env, content)
|
|
97
|
+
return yaml.safe_load(content) or {}
|
antz/db_connector.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import httpx
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DBConnector:
|
|
7
|
+
def __init__(self, name: str, nest_url: str, api_key: str):
|
|
8
|
+
self.name = name
|
|
9
|
+
self.nest_url = nest_url
|
|
10
|
+
self.api_key = api_key
|
|
11
|
+
self._engine = None
|
|
12
|
+
|
|
13
|
+
def _get_connection_url(self) -> str:
|
|
14
|
+
r = httpx.get(
|
|
15
|
+
f"{self.nest_url}/vault/db/{self.name}",
|
|
16
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
17
|
+
timeout=10,
|
|
18
|
+
)
|
|
19
|
+
r.raise_for_status()
|
|
20
|
+
return r.json()["url"]
|
|
21
|
+
|
|
22
|
+
def execute(self, sql: str, params: list = None) -> list:
|
|
23
|
+
import sqlalchemy
|
|
24
|
+
if not self._engine:
|
|
25
|
+
url = self._get_connection_url()
|
|
26
|
+
self._engine = sqlalchemy.create_engine(url)
|
|
27
|
+
with self._engine.connect() as conn:
|
|
28
|
+
result = conn.execute(sqlalchemy.text(sql), params or {})
|
|
29
|
+
return [dict(row) for row in result]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def db_connector(name: str) -> DBConnector:
|
|
33
|
+
from antz.config import AntzConfig
|
|
34
|
+
cfg = AntzConfig.load()
|
|
35
|
+
return DBConnector(name=name, nest_url=cfg.nest_url, api_key=cfg.api_key)
|
antz/decorators.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
import traceback
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Callable, Optional
|
|
8
|
+
|
|
9
|
+
from opentelemetry import trace
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
|
|
14
|
+
from antz.exceptions import ConstitutionViolationError
|
|
15
|
+
from antz.hive_mode import HiveMode
|
|
16
|
+
from antz.privacy import add_gaussian_noise
|
|
17
|
+
|
|
18
|
+
# -- Lazy OTel tracer ----------------------------------------------------------
|
|
19
|
+
_tracer: Optional[trace.Tracer] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_tracer() -> trace.Tracer:
|
|
23
|
+
global _tracer
|
|
24
|
+
if _tracer is None:
|
|
25
|
+
_tracer = trace.get_tracer("antz.sdk", "0.1.0")
|
|
26
|
+
return _tracer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _setup_otel_if_needed(nest_url: str, api_key: str = None) -> None:
|
|
30
|
+
provider = TracerProvider()
|
|
31
|
+
headers = {}
|
|
32
|
+
if api_key:
|
|
33
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
34
|
+
exporter = OTLPSpanExporter(
|
|
35
|
+
endpoint=f"{nest_url}/v1/traces",
|
|
36
|
+
headers=headers,
|
|
37
|
+
)
|
|
38
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
39
|
+
trace.set_tracer_provider(provider)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# -- @agent --------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def agent(agent_id: str, constitution: Optional[str] = None):
|
|
45
|
+
def decorator(func: Callable) -> Callable:
|
|
46
|
+
@functools.wraps(func)
|
|
47
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
48
|
+
from antz.config import AntzConfig
|
|
49
|
+
from antz.client import AntzClient
|
|
50
|
+
|
|
51
|
+
run_id = str(uuid.uuid4())
|
|
52
|
+
start_ts = time.time()
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
cfg = AntzConfig.load()
|
|
56
|
+
client = AntzClient(cfg)
|
|
57
|
+
_setup_otel_if_needed(cfg.nest_url, cfg.api_key)
|
|
58
|
+
except Exception:
|
|
59
|
+
return func(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
tracer = _get_tracer()
|
|
62
|
+
|
|
63
|
+
with tracer.start_as_current_span(f"agent.{agent_id}") as span:
|
|
64
|
+
span.set_attribute("antz.agent_id", agent_id)
|
|
65
|
+
span.set_attribute("antz.run_id", run_id)
|
|
66
|
+
span.set_attribute("antz.mode", cfg.mode)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
validation = client.validate_constitution(
|
|
70
|
+
agent_id=agent_id,
|
|
71
|
+
action=func.__name__,
|
|
72
|
+
payload={"args_count": len(args), "kwargs": list(kwargs.keys())},
|
|
73
|
+
)
|
|
74
|
+
if not validation.get("allowed", True):
|
|
75
|
+
raise ConstitutionViolationError(
|
|
76
|
+
rule_id=validation.get("rule_id", "unknown"),
|
|
77
|
+
action=func.__name__,
|
|
78
|
+
message=validation.get("message", "Action blocked by constitution"),
|
|
79
|
+
)
|
|
80
|
+
except ConstitutionViolationError:
|
|
81
|
+
raise
|
|
82
|
+
except Exception:
|
|
83
|
+
span.set_attribute("antz.constitution_check", "skipped")
|
|
84
|
+
|
|
85
|
+
status = "success"
|
|
86
|
+
result = None
|
|
87
|
+
try:
|
|
88
|
+
result = func(*args, **kwargs)
|
|
89
|
+
span.set_attribute("antz.status", "success")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
status = "failure"
|
|
92
|
+
span.set_attribute("antz.status", "failure")
|
|
93
|
+
span.set_attribute("antz.error", str(e))
|
|
94
|
+
span.record_exception(e)
|
|
95
|
+
raise
|
|
96
|
+
finally:
|
|
97
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
98
|
+
span.set_attribute("antz.latency_ms", latency_ms)
|
|
99
|
+
try:
|
|
100
|
+
client.log_audit({
|
|
101
|
+
"event_id": run_id,
|
|
102
|
+
"agent_id": agent_id,
|
|
103
|
+
"action_type": "agent_execution",
|
|
104
|
+
"tool_called": func.__name__,
|
|
105
|
+
"status": status,
|
|
106
|
+
"latency_ms": latency_ms,
|
|
107
|
+
})
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
wrapper._antz_agent_id = agent_id
|
|
114
|
+
return wrapper
|
|
115
|
+
return decorator
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# -- @tool ---------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def tool(tool_id: str):
|
|
121
|
+
def decorator(func: Callable) -> Callable:
|
|
122
|
+
@functools.wraps(func)
|
|
123
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
124
|
+
from antz.config import AntzConfig
|
|
125
|
+
from antz.client import AntzClient
|
|
126
|
+
|
|
127
|
+
start_ts = time.time()
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
cfg = AntzConfig.load()
|
|
131
|
+
client = AntzClient(cfg)
|
|
132
|
+
except Exception:
|
|
133
|
+
return func(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
tracer = _get_tracer()
|
|
136
|
+
|
|
137
|
+
with tracer.start_as_current_span(f"tool.{tool_id}") as span:
|
|
138
|
+
span.set_attribute("antz.tool_id", tool_id)
|
|
139
|
+
span.set_attribute("antz.pii_check", "pending_redaction")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
validation = client.validate_constitution(
|
|
143
|
+
agent_id="__tool__",
|
|
144
|
+
action=tool_id,
|
|
145
|
+
payload={},
|
|
146
|
+
)
|
|
147
|
+
if not validation.get("allowed", True):
|
|
148
|
+
raise ConstitutionViolationError(
|
|
149
|
+
rule_id=validation.get("rule_id", "unknown"),
|
|
150
|
+
action=tool_id,
|
|
151
|
+
message=validation.get("message", f"Tool '{tool_id}' blocked"),
|
|
152
|
+
)
|
|
153
|
+
except ConstitutionViolationError:
|
|
154
|
+
raise
|
|
155
|
+
except Exception:
|
|
156
|
+
span.set_attribute("antz.constitution_check", "skipped")
|
|
157
|
+
|
|
158
|
+
status = "success"
|
|
159
|
+
try:
|
|
160
|
+
result = func(*args, **kwargs)
|
|
161
|
+
span.set_attribute("antz.status", "success")
|
|
162
|
+
return result
|
|
163
|
+
except Exception as e:
|
|
164
|
+
status = "failure"
|
|
165
|
+
span.set_attribute("antz.status", "failure")
|
|
166
|
+
span.record_exception(e)
|
|
167
|
+
raise
|
|
168
|
+
finally:
|
|
169
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
170
|
+
span.set_attribute("antz.latency_ms", latency_ms)
|
|
171
|
+
try:
|
|
172
|
+
client.log_audit({
|
|
173
|
+
"agent_id": "__tool__",
|
|
174
|
+
"action_type": "tool_call",
|
|
175
|
+
"tool_called": tool_id,
|
|
176
|
+
"status": status,
|
|
177
|
+
"latency_ms": latency_ms,
|
|
178
|
+
})
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
wrapper._antz_tool_id = tool_id
|
|
183
|
+
return wrapper
|
|
184
|
+
return decorator
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# -- @memory -------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def memory(namespace: str, hive: HiveMode = HiveMode.ISOLATED):
|
|
190
|
+
"""
|
|
191
|
+
Decorator that wraps a function with Hive semantic memory.
|
|
192
|
+
|
|
193
|
+
hive=HiveMode.ISOLATED ? default, zero leaves Nest
|
|
194
|
+
hive=HiveMode.SHARED ? anon vectors sent to Hive Central
|
|
195
|
+
hive=HiveMode.SOVEREIGN ? air-gapped, OTel disabled
|
|
196
|
+
"""
|
|
197
|
+
def decorator(func: Callable) -> Callable:
|
|
198
|
+
@functools.wraps(func)
|
|
199
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
200
|
+
from antz.config import AntzConfig
|
|
201
|
+
from antz.client import AntzClient
|
|
202
|
+
|
|
203
|
+
# SOVEREIGN: skip all telemetry
|
|
204
|
+
if hive == HiveMode.SOVEREIGN:
|
|
205
|
+
kwargs.setdefault("memory_context", [])
|
|
206
|
+
return func(*args, **kwargs)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
cfg = AntzConfig.load()
|
|
210
|
+
client = AntzClient(cfg)
|
|
211
|
+
|
|
212
|
+
query = " ".join(str(a) for a in args) + " ".join(
|
|
213
|
+
f"{k}={v}" for k, v in kwargs.items()
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
memories = client.memory_search(
|
|
217
|
+
namespace=namespace,
|
|
218
|
+
query=query,
|
|
219
|
+
top_k=5,
|
|
220
|
+
)
|
|
221
|
+
kwargs["memory_context"] = memories
|
|
222
|
+
except Exception:
|
|
223
|
+
kwargs.setdefault("memory_context", [])
|
|
224
|
+
|
|
225
|
+
result = func(*args, **kwargs)
|
|
226
|
+
|
|
227
|
+
# Store result in Hive memory
|
|
228
|
+
try:
|
|
229
|
+
client.memory_store(
|
|
230
|
+
namespace=namespace,
|
|
231
|
+
agent_id=func.__name__,
|
|
232
|
+
content=str(result),
|
|
233
|
+
metadata={"function": func.__name__, "hive_mode": hive.value},
|
|
234
|
+
)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print(f"[memory] store failed: {e}")
|
|
237
|
+
|
|
238
|
+
# SHARED: sync anonymized vectors to Hive Central
|
|
239
|
+
if hive == HiveMode.SHARED:
|
|
240
|
+
try:
|
|
241
|
+
client.sync_to_hive_central(namespace=namespace)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
print(f"[memory] sync to Hive Central failed: {e}")
|
|
244
|
+
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
wrapper._antz_memory_namespace = namespace
|
|
248
|
+
wrapper._antz_hive_mode = hive
|
|
249
|
+
return wrapper
|
|
250
|
+
return decorator
|
antz/exceptions.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class AntzError(Exception):
|
|
2
|
+
"""Base exception for all Ant'z SDK errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AntzConnectionError(AntzError):
|
|
7
|
+
"""Cannot reach the Nest endpoint."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AntzAuthError(AntzError):
|
|
12
|
+
"""Invalid or missing API key."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConstitutionViolationError(AntzError):
|
|
17
|
+
"""
|
|
18
|
+
Raised when an agent action violates the Agentic Constitution.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
rule_id: The constitution constraint that was violated.
|
|
22
|
+
action: The action that triggered the violation.
|
|
23
|
+
message: Human-readable explanation.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, rule_id: str, action: str, message: str):
|
|
26
|
+
self.rule_id = rule_id
|
|
27
|
+
self.action = action
|
|
28
|
+
super().__init__(
|
|
29
|
+
f"[Constitution violation] rule='{rule_id}' action='{action}' — {message}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ColonyNotFoundError(AntzError):
|
|
34
|
+
"""Colony directory or config not found."""
|
|
35
|
+
pass
|
antz/hive_mode.py
ADDED
antz/privacy.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def add_gaussian_noise(vector: list, epsilon: float = 1.0, sensitivity: float = 1.0) -> list:
|
|
6
|
+
"""
|
|
7
|
+
Aplica differential privacy com ruido gaussiano.
|
|
8
|
+
Epsilon menor = mais privacidade, mais ruido.
|
|
9
|
+
Epsilon maior = menos privacidade, menos ruido.
|
|
10
|
+
"""
|
|
11
|
+
sigma = sensitivity / epsilon
|
|
12
|
+
return [v + random.gauss(0, sigma) for v in vector]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def privacy_budget(epsilon: float) -> str:
|
|
16
|
+
"""Retorna descricao do nivel de privacidade."""
|
|
17
|
+
if epsilon <= 0.1:
|
|
18
|
+
return "very_high"
|
|
19
|
+
elif epsilon <= 1.0:
|
|
20
|
+
return "high"
|
|
21
|
+
elif epsilon <= 5.0:
|
|
22
|
+
return "medium"
|
|
23
|
+
else:
|
|
24
|
+
return "low"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: antz-studio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Antz Studio -- Sovereign Agentic OS
|
|
5
|
+
Author-email: Antz Studio <gabriel@antz.studio>
|
|
6
|
+
License: AGPL-3.0
|
|
7
|
+
Project-URL: Homepage, https://antz.studio
|
|
8
|
+
Project-URL: Repository, https://github.com/Gabbsx7/antz-studio-aos
|
|
9
|
+
Keywords: agents,sovereign-ai,agentic-os,llm,observability,nest,colony,hive
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: typer>=0.12
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: opentelemetry-api>=1.24
|
|
21
|
+
Requires-Dist: opentelemetry-sdk>=1.24
|
|
22
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.24
|
|
23
|
+
Requires-Dist: rich>=13.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
30
|
+
Provides-Extra: cloud
|
|
31
|
+
Requires-Dist: azure-mgmt-compute>=30.0.0; extra == "cloud"
|
|
32
|
+
Requires-Dist: azure-mgmt-network>=25.0.0; extra == "cloud"
|
|
33
|
+
Requires-Dist: azure-mgmt-resource>=23.0.0; extra == "cloud"
|
|
34
|
+
Requires-Dist: azure-identity>=1.15.0; extra == "cloud"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
antz/__init__.py,sha256=w8gFukkSXtdymQDoZnTcjuUkzUnCCMVmqoMwHGZ1Kjg,700
|
|
2
|
+
antz/cli.py,sha256=t52PEQgW8l8GrN9UbvDfx0UuOV133dKuyg9cAjOI6UI,19972
|
|
3
|
+
antz/client.py,sha256=eQlEjN96a0t1dWVM-r0MPfLX9t55NCvuaLzh7HvPEEo,4055
|
|
4
|
+
antz/config.py,sha256=O0hT0GAlNI65DLSglj4Xo7aKpXvHx1MLgxPrgLl8xHI,3343
|
|
5
|
+
antz/db_connector.py,sha256=c_NDoO_sKGHdNHEl1s89sYMQ-fOdoCvNtAhFpFTGZXo,1136
|
|
6
|
+
antz/decorators.py,sha256=Ido52tjLKsfM6ZMH5Tksbn3CMptxBo5Qf2Ltps503Zk,9448
|
|
7
|
+
antz/exceptions.py,sha256=6yUeNySc3rb8vUPkuiaqJvBrctaJSis1Ev0tZdTycfA,921
|
|
8
|
+
antz/hive_mode.py,sha256=g97M7ZUnAyqgXYtTRwCNu5Ib-HKtQfUQ6folDThAkO8,127
|
|
9
|
+
antz/privacy.py,sha256=hrea2Nd0A1cYKITASBh2P_lPrH9FydTsIHC9BterA6g,682
|
|
10
|
+
antz_studio-0.1.0.dist-info/METADATA,sha256=TUuBk7nYJOKlLX6XUivxx7e3SlIA0AGSG-i3VAdDT5c,1420
|
|
11
|
+
antz_studio-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
antz_studio-0.1.0.dist-info/entry_points.txt,sha256=v_JAzkfl2RdkZRZGpoHubSBPCtOWpTKXrpCUq3yIH2k,38
|
|
13
|
+
antz_studio-0.1.0.dist-info/top_level.txt,sha256=JFEP9Nyk3w07ieuche5had6-CjcBbKJQx0HacgMTiV8,5
|
|
14
|
+
antz_studio-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
antz
|