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 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
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+ class HiveMode(Enum):
4
+ ISOLATED = 'isolated'
5
+ SHARED = 'shared'
6
+ SOVEREIGN = 'sovereign'
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ antz = antz.cli:app
@@ -0,0 +1 @@
1
+ antz