agentomatic 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.
- agentomatic/__init__.py +59 -0
- agentomatic/_version.py +5 -0
- agentomatic/cli/__init__.py +7 -0
- agentomatic/cli/commands.py +715 -0
- agentomatic/cli/templates.py +188 -0
- agentomatic/config/__init__.py +3 -0
- agentomatic/config/defaults.py +10 -0
- agentomatic/config/settings.py +117 -0
- agentomatic/core/__init__.py +31 -0
- agentomatic/core/lifespan.py +102 -0
- agentomatic/core/manifest.py +100 -0
- agentomatic/core/platform.py +571 -0
- agentomatic/core/registry.py +198 -0
- agentomatic/core/router_factory.py +541 -0
- agentomatic/core/state.py +63 -0
- agentomatic/middleware/__init__.py +18 -0
- agentomatic/middleware/auth.py +53 -0
- agentomatic/middleware/feedback.py +207 -0
- agentomatic/middleware/logging.py +40 -0
- agentomatic/middleware/metrics.py +93 -0
- agentomatic/middleware/rate_limit.py +70 -0
- agentomatic/observability/__init__.py +11 -0
- agentomatic/observability/concurrency.py +109 -0
- agentomatic/observability/metrics.py +101 -0
- agentomatic/observability/telemetry.py +316 -0
- agentomatic/optimize/__init__.py +112 -0
- agentomatic/optimize/dataset.py +142 -0
- agentomatic/optimize/loop.py +870 -0
- agentomatic/optimize/metrics.py +781 -0
- agentomatic/optimize/optimizer.py +891 -0
- agentomatic/optimize/report.py +774 -0
- agentomatic/optimize/runner.py +261 -0
- agentomatic/optimize/strategies.py +592 -0
- agentomatic/optimize/synthesizer.py +729 -0
- agentomatic/prompts/__init__.py +7 -0
- agentomatic/prompts/manager.py +59 -0
- agentomatic/protocols/__init__.py +3 -0
- agentomatic/protocols/decorators.py +75 -0
- agentomatic/providers/__init__.py +3 -0
- agentomatic/providers/embeddings.py +44 -0
- agentomatic/providers/llm.py +116 -0
- agentomatic/py.typed +1 -0
- agentomatic/storage/__init__.py +40 -0
- agentomatic/storage/base.py +192 -0
- agentomatic/storage/memory.py +167 -0
- agentomatic/storage/models.py +129 -0
- agentomatic/storage/sqlalchemy.py +317 -0
- agentomatic/ui/.chainlit/config.toml +14 -0
- agentomatic/ui/__init__.py +50 -0
- agentomatic/ui/chat.py +198 -0
- agentomatic-0.1.0.dist-info/METADATA +363 -0
- agentomatic-0.1.0.dist-info/RECORD +55 -0
- agentomatic-0.1.0.dist-info/WHEEL +4 -0
- agentomatic-0.1.0.dist-info/entry_points.txt +2 -0
- agentomatic-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
"""Agentomatic CLI — beautiful terminal experience for agent lifecycle.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
agentomatic init <name> Interactive agent scaffolding
|
|
5
|
+
agentomatic run Start the platform
|
|
6
|
+
agentomatic list List discovered agents
|
|
7
|
+
agentomatic test <name> Test an agent interactively
|
|
8
|
+
agentomatic inspect <name> Show agent details
|
|
9
|
+
agentomatic doctor Check environment health
|
|
10
|
+
agentomatic ui Launch debug UI standalone
|
|
11
|
+
agentomatic optimize <name> Run prompt optimization
|
|
12
|
+
|
|
13
|
+
Requires: pip install agentomatic[cli]
|
|
14
|
+
Fallback: works with basic output if Rich is not installed.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import importlib.util
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
from loguru import logger
|
|
28
|
+
|
|
29
|
+
# Graceful Rich fallback
|
|
30
|
+
try:
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
from rich.panel import Panel
|
|
33
|
+
from rich.table import Table
|
|
34
|
+
from rich.tree import Tree
|
|
35
|
+
|
|
36
|
+
HAS_RICH = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
HAS_RICH = False
|
|
39
|
+
|
|
40
|
+
console: Console = Console() if HAS_RICH else None # type: ignore[assignment]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =====================================================================
|
|
44
|
+
# Utilities
|
|
45
|
+
# =====================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _echo(msg: str) -> None:
|
|
49
|
+
"""Print with Rich if available, plain click.echo otherwise."""
|
|
50
|
+
if console:
|
|
51
|
+
console.print(msg)
|
|
52
|
+
else:
|
|
53
|
+
click.echo(msg)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _print_banner() -> None:
|
|
57
|
+
"""Show the agentomatic banner."""
|
|
58
|
+
if HAS_RICH:
|
|
59
|
+
console.print(
|
|
60
|
+
Panel.fit(
|
|
61
|
+
"[bold magenta]⚡ agentomatic[/bold magenta]\n[dim]Drop agents, not code[/dim]",
|
|
62
|
+
border_style="magenta",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
click.echo("⚡ agentomatic — Drop agents, not code")
|
|
67
|
+
click.echo()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _print_success(msg: str) -> None:
|
|
71
|
+
if HAS_RICH:
|
|
72
|
+
console.print(f"[bold green]✅ {msg}[/bold green]")
|
|
73
|
+
else:
|
|
74
|
+
logger.success(msg)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _print_error(msg: str) -> None:
|
|
78
|
+
if HAS_RICH:
|
|
79
|
+
console.print(f"[bold red]❌ {msg}[/bold red]")
|
|
80
|
+
else:
|
|
81
|
+
logger.error(msg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _print_warning(msg: str) -> None:
|
|
85
|
+
if HAS_RICH:
|
|
86
|
+
console.print(f"[bold yellow]⚠️ {msg}[/bold yellow]")
|
|
87
|
+
else:
|
|
88
|
+
logger.warning(msg)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# =====================================================================
|
|
92
|
+
# CLI Group
|
|
93
|
+
# =====================================================================
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@click.group()
|
|
97
|
+
def cli():
|
|
98
|
+
"""⚡ Agentomatic — Drop agents, not code."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =====================================================================
|
|
103
|
+
# INIT — Scaffold a new agent
|
|
104
|
+
# =====================================================================
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@cli.command()
|
|
108
|
+
@click.argument("name")
|
|
109
|
+
@click.option(
|
|
110
|
+
"--dir", "-d", "agents_dir", default="agents", help="Agents directory (default: agents)"
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--template",
|
|
114
|
+
"-t",
|
|
115
|
+
type=click.Choice(["basic", "full", "rag", "chatbot", "custom"]),
|
|
116
|
+
default=None,
|
|
117
|
+
help="Template to use (default: interactive selection)",
|
|
118
|
+
)
|
|
119
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing files")
|
|
120
|
+
def init(name: str, agents_dir: str, template: str | None, force: bool) -> None:
|
|
121
|
+
"""Scaffold a new agent with template selection."""
|
|
122
|
+
from .templates import TEMPLATES, get_template_files
|
|
123
|
+
|
|
124
|
+
target = Path(agents_dir) / name
|
|
125
|
+
|
|
126
|
+
_print_banner()
|
|
127
|
+
|
|
128
|
+
# Template selection
|
|
129
|
+
if not template:
|
|
130
|
+
# Interactive selection if questionary is available
|
|
131
|
+
try:
|
|
132
|
+
import questionary
|
|
133
|
+
|
|
134
|
+
choices = [questionary.Choice(f"{k} — {v}", value=k) for k, v in TEMPLATES.items()]
|
|
135
|
+
template = questionary.select(
|
|
136
|
+
"Select a template:",
|
|
137
|
+
choices=choices,
|
|
138
|
+
default="basic",
|
|
139
|
+
).ask()
|
|
140
|
+
if not template:
|
|
141
|
+
_print_error("Cancelled")
|
|
142
|
+
return
|
|
143
|
+
except ImportError:
|
|
144
|
+
template = "basic"
|
|
145
|
+
_print_warning("Install questionary for interactive mode: pip install questionary")
|
|
146
|
+
|
|
147
|
+
# Validate template
|
|
148
|
+
if template not in TEMPLATES:
|
|
149
|
+
_print_error(f"Unknown template: {template}. Choose from: {list(TEMPLATES.keys())}")
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
# Check if target exists
|
|
153
|
+
if target.exists() and any(target.iterdir()):
|
|
154
|
+
_print_warning(f"Directory {target} already exists and is not empty")
|
|
155
|
+
if not force:
|
|
156
|
+
if not click.confirm("Overwrite?", default=False):
|
|
157
|
+
logger.info("Cancelled")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Generate files
|
|
161
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
files = get_template_files(template, name)
|
|
163
|
+
|
|
164
|
+
if HAS_RICH:
|
|
165
|
+
tree = Tree(f"[bold cyan]📁 {target}[/bold cyan]")
|
|
166
|
+
for rel_path in sorted(files.keys()):
|
|
167
|
+
tree.add(f"[green]📄 {rel_path}[/green]")
|
|
168
|
+
console.print(tree)
|
|
169
|
+
else:
|
|
170
|
+
click.echo(f"📁 {target}")
|
|
171
|
+
for rel_path in sorted(files.keys()):
|
|
172
|
+
click.echo(f" 📄 {rel_path}")
|
|
173
|
+
|
|
174
|
+
for rel_path, content in files.items():
|
|
175
|
+
file_path = target / rel_path
|
|
176
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
file_path.write_text(content)
|
|
178
|
+
|
|
179
|
+
click.echo()
|
|
180
|
+
logger.success(f"Created agent '{name}' with template '{template}'")
|
|
181
|
+
click.echo(f" 📍 Location: {target}")
|
|
182
|
+
click.echo(f" 📦 Files: {len(files)}")
|
|
183
|
+
click.echo()
|
|
184
|
+
|
|
185
|
+
if HAS_RICH:
|
|
186
|
+
console.print(
|
|
187
|
+
Panel(
|
|
188
|
+
f"[bold]Next steps:[/bold]\n\n"
|
|
189
|
+
f" 1. [cyan]cd {agents_dir}[/cyan]\n"
|
|
190
|
+
f" 2. Edit [yellow]{name}/nodes.py[/yellow] with your logic\n"
|
|
191
|
+
f" 3. [cyan]agentomatic run[/cyan] to start\n"
|
|
192
|
+
f" 4. [cyan]agentomatic test {name}[/cyan] to test\n"
|
|
193
|
+
f" 5. Open [blue]http://localhost:8000/docs[/blue] for API docs",
|
|
194
|
+
title="🚀 What's next?",
|
|
195
|
+
border_style="green",
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
click.echo("Next steps:")
|
|
200
|
+
click.echo(f" 1. Edit {name}/nodes.py with your logic")
|
|
201
|
+
click.echo(" 2. agentomatic run")
|
|
202
|
+
click.echo(f" 3. agentomatic test {name}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =====================================================================
|
|
206
|
+
# RUN — Start the platform
|
|
207
|
+
# =====================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@cli.command()
|
|
211
|
+
@click.option("--agents-dir", default="agents", help="Agents directory")
|
|
212
|
+
@click.option("--host", default="0.0.0.0", help="Host to bind to")
|
|
213
|
+
@click.option("--port", type=int, default=8000, help="Port to listen on")
|
|
214
|
+
@click.option("--reload", is_flag=True, help="Enable auto-reload")
|
|
215
|
+
@click.option("--title", default=None, help="Platform title")
|
|
216
|
+
@click.option("--log-level", default="INFO", help="Log level")
|
|
217
|
+
@click.option("--with-ui", "--ui", is_flag=True, help="Enable Chainlit debug UI at /chat")
|
|
218
|
+
def run(
|
|
219
|
+
agents_dir: str,
|
|
220
|
+
host: str,
|
|
221
|
+
port: int,
|
|
222
|
+
reload: bool,
|
|
223
|
+
title: str | None,
|
|
224
|
+
log_level: str,
|
|
225
|
+
with_ui: bool,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Run the platform with Rich status output."""
|
|
228
|
+
_print_banner()
|
|
229
|
+
logger.info(f"Starting platform from {agents_dir}...")
|
|
230
|
+
|
|
231
|
+
from agentomatic import AgentPlatform
|
|
232
|
+
|
|
233
|
+
kwargs: dict[str, Any] = {
|
|
234
|
+
"title": title or "Agentomatic Platform",
|
|
235
|
+
"log_level": log_level,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Auto-detect and enable UI
|
|
239
|
+
if with_ui:
|
|
240
|
+
from agentomatic.ui import is_available
|
|
241
|
+
|
|
242
|
+
if is_available():
|
|
243
|
+
logger.success("Debug UI will be available at /chat")
|
|
244
|
+
else:
|
|
245
|
+
logger.warning("Chainlit not installed. Install: pip install agentomatic[ui]")
|
|
246
|
+
|
|
247
|
+
platform = AgentPlatform.from_folder(agents_dir, **kwargs)
|
|
248
|
+
|
|
249
|
+
# Mount UI if requested
|
|
250
|
+
if with_ui:
|
|
251
|
+
|
|
252
|
+
@platform.on_startup
|
|
253
|
+
async def _mount_ui():
|
|
254
|
+
from agentomatic.ui import mount
|
|
255
|
+
|
|
256
|
+
if platform._app:
|
|
257
|
+
mount(platform._app)
|
|
258
|
+
|
|
259
|
+
platform.run(host=host, port=port, reload=reload)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# =====================================================================
|
|
263
|
+
# LIST — Show discovered agents
|
|
264
|
+
# =====================================================================
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@cli.command("list")
|
|
268
|
+
@click.option("--agents-dir", default="agents", help="Agents directory")
|
|
269
|
+
def list_agents(agents_dir: str) -> None:
|
|
270
|
+
"""List agents in a directory with Rich table."""
|
|
271
|
+
agents_path = Path(agents_dir)
|
|
272
|
+
_print_banner()
|
|
273
|
+
|
|
274
|
+
if not agents_path.exists():
|
|
275
|
+
_print_error(f"Directory not found: {agents_path}")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
|
|
278
|
+
agents = []
|
|
279
|
+
for entry in sorted(agents_path.iterdir()):
|
|
280
|
+
if entry.is_dir() and (entry / "__init__.py").exists() and not entry.name.startswith("_"):
|
|
281
|
+
info: dict[str, Any] = {"name": entry.name, "path": str(entry)}
|
|
282
|
+
|
|
283
|
+
# Try to read manifest
|
|
284
|
+
try:
|
|
285
|
+
spec = importlib.util.spec_from_file_location(
|
|
286
|
+
f"_agent_{entry.name}",
|
|
287
|
+
entry / "__init__.py",
|
|
288
|
+
)
|
|
289
|
+
if spec and spec.loader:
|
|
290
|
+
_ = importlib.util.module_from_spec(spec)
|
|
291
|
+
# Don't actually exec — just check for manifest in source
|
|
292
|
+
source = (entry / "__init__.py").read_text()
|
|
293
|
+
if "AgentManifest" in source:
|
|
294
|
+
info["has_manifest"] = True
|
|
295
|
+
if "graph.py" in [f.name for f in entry.iterdir()]:
|
|
296
|
+
info["has_graph"] = True
|
|
297
|
+
info["files"] = len([f for f in entry.iterdir() if f.is_file()])
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
agents.append(info)
|
|
302
|
+
|
|
303
|
+
if not agents:
|
|
304
|
+
_print_warning(f"No agents found in {agents_path}")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
if HAS_RICH:
|
|
308
|
+
table = Table(title=f"🤖 Agents in {agents_path}", show_lines=True)
|
|
309
|
+
table.add_column("Name", style="bold cyan")
|
|
310
|
+
table.add_column("Files", justify="center")
|
|
311
|
+
table.add_column("Manifest", justify="center")
|
|
312
|
+
table.add_column("Graph", justify="center")
|
|
313
|
+
|
|
314
|
+
for a in agents:
|
|
315
|
+
table.add_row(
|
|
316
|
+
a["name"],
|
|
317
|
+
str(a.get("files", "?")),
|
|
318
|
+
"✅" if a.get("has_manifest") else "❌",
|
|
319
|
+
"✅" if a.get("has_graph") else "—",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
console.print(table)
|
|
323
|
+
else:
|
|
324
|
+
click.echo(f"📂 {agents_path}")
|
|
325
|
+
for a in agents:
|
|
326
|
+
click.echo(f" 🤖 {a['name']} ({a.get('files', '?')} files)")
|
|
327
|
+
|
|
328
|
+
_echo(f"\n Total: {len(agents)} agent(s)")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# =====================================================================
|
|
332
|
+
# TEST — Interactive agent testing
|
|
333
|
+
# =====================================================================
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@cli.command()
|
|
337
|
+
@click.argument("name")
|
|
338
|
+
@click.option("--host", default="localhost", help="Platform API host")
|
|
339
|
+
@click.option("--port", type=int, default=8000, help="Platform API port")
|
|
340
|
+
@click.option("--agents-dir", default="agents", help="Agents directory")
|
|
341
|
+
def test(name: str, host: str, port: int, agents_dir: str) -> None:
|
|
342
|
+
"""Test an agent interactively in the terminal."""
|
|
343
|
+
import asyncio
|
|
344
|
+
|
|
345
|
+
_print_banner()
|
|
346
|
+
base_url = f"http://{host}:{port}"
|
|
347
|
+
|
|
348
|
+
if HAS_RICH:
|
|
349
|
+
_echo(f"🧪 Testing agent: [bold cyan]{name}[/bold cyan]")
|
|
350
|
+
else:
|
|
351
|
+
logger.info(f"Testing agent: {name}")
|
|
352
|
+
|
|
353
|
+
click.echo(f" API: {base_url}/api/v1/{name}/invoke")
|
|
354
|
+
click.echo(" Type 'quit' or 'exit' to stop\n")
|
|
355
|
+
|
|
356
|
+
async def _test_loop():
|
|
357
|
+
import httpx
|
|
358
|
+
|
|
359
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=60) as client:
|
|
360
|
+
# Health check
|
|
361
|
+
try:
|
|
362
|
+
resp = await client.get(f"/api/v1/{name}/health")
|
|
363
|
+
if resp.status_code == 200:
|
|
364
|
+
logger.success(f"Agent '{name}' is healthy")
|
|
365
|
+
else:
|
|
366
|
+
logger.error(f"Agent '{name}' health check failed: {resp.status_code}")
|
|
367
|
+
return
|
|
368
|
+
except httpx.ConnectError:
|
|
369
|
+
logger.error(f"Cannot connect to {base_url}. Is the platform running?")
|
|
370
|
+
click.echo(" Start with: agentomatic run")
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
# Interactive loop
|
|
374
|
+
thread_id = None
|
|
375
|
+
while True:
|
|
376
|
+
try:
|
|
377
|
+
query = input("\n🗣️ You: ").strip()
|
|
378
|
+
except (KeyboardInterrupt, EOFError):
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
if query.lower() in ("quit", "exit", "q"):
|
|
382
|
+
break
|
|
383
|
+
if not query:
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
payload = {"query": query, "user_id": "cli-tester"}
|
|
388
|
+
if thread_id:
|
|
389
|
+
payload["thread_id"] = thread_id
|
|
390
|
+
|
|
391
|
+
resp = await client.post(
|
|
392
|
+
f"/api/v1/{name}/invoke",
|
|
393
|
+
json=payload,
|
|
394
|
+
)
|
|
395
|
+
resp.raise_for_status()
|
|
396
|
+
data = resp.json()
|
|
397
|
+
|
|
398
|
+
thread_id = data.get("thread_id", thread_id)
|
|
399
|
+
|
|
400
|
+
if HAS_RICH:
|
|
401
|
+
console.print(
|
|
402
|
+
f"\n🤖 [bold green]{name}[/bold green]: {data.get('response', '')}"
|
|
403
|
+
)
|
|
404
|
+
if data.get("steps_taken"):
|
|
405
|
+
console.print(
|
|
406
|
+
f" [dim]Steps: {' → '.join(data['steps_taken'])}[/dim]"
|
|
407
|
+
)
|
|
408
|
+
if data.get("suggestions"):
|
|
409
|
+
console.print(
|
|
410
|
+
f" [dim]Suggestions: {', '.join(data['suggestions'])}[/dim]"
|
|
411
|
+
)
|
|
412
|
+
console.print(f" [dim]⏱ {data.get('duration_ms', 0):.0f}ms[/dim]")
|
|
413
|
+
else:
|
|
414
|
+
click.echo(f"\n🤖 {name}: {data.get('response', '')}")
|
|
415
|
+
if data.get("duration_ms"):
|
|
416
|
+
click.echo(f" ⏱ {data['duration_ms']:.0f}ms")
|
|
417
|
+
|
|
418
|
+
except httpx.HTTPStatusError as exc:
|
|
419
|
+
logger.error(f"API Error: {exc.response.status_code}")
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
logger.error(f"Error: {exc}")
|
|
422
|
+
|
|
423
|
+
click.echo("\n👋 Test session ended")
|
|
424
|
+
|
|
425
|
+
asyncio.run(_test_loop())
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# =====================================================================
|
|
429
|
+
# INSPECT — Show agent details
|
|
430
|
+
# =====================================================================
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@cli.command()
|
|
434
|
+
@click.argument("name")
|
|
435
|
+
@click.option("--agents-dir", default="agents", help="Agents directory")
|
|
436
|
+
def inspect(name: str, agents_dir: str) -> None:
|
|
437
|
+
"""Inspect an agent's structure and configuration."""
|
|
438
|
+
_print_banner()
|
|
439
|
+
|
|
440
|
+
target = Path(agents_dir) / name
|
|
441
|
+
if not target.exists():
|
|
442
|
+
_print_error(f"Agent not found: {target}")
|
|
443
|
+
sys.exit(1)
|
|
444
|
+
|
|
445
|
+
files = sorted([f for f in target.rglob("*") if f.is_file() and not f.name.startswith(".")])
|
|
446
|
+
|
|
447
|
+
if HAS_RICH:
|
|
448
|
+
# Header
|
|
449
|
+
console.print(
|
|
450
|
+
Panel(
|
|
451
|
+
f"[bold cyan]{name}[/bold cyan]\n[dim]{target}[/dim]",
|
|
452
|
+
title="🔍 Agent Inspector",
|
|
453
|
+
border_style="cyan",
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# File tree
|
|
458
|
+
tree = Tree(f"[bold]📁 {name}/[/bold]")
|
|
459
|
+
for f in files:
|
|
460
|
+
rel = f.relative_to(target)
|
|
461
|
+
size = f.stat().st_size
|
|
462
|
+
tree.add(f"📄 {rel} [dim]({size:,} bytes)[/dim]")
|
|
463
|
+
console.print(tree)
|
|
464
|
+
|
|
465
|
+
# Read manifest info
|
|
466
|
+
init_file = target / "__init__.py"
|
|
467
|
+
if init_file.exists():
|
|
468
|
+
source = init_file.read_text()
|
|
469
|
+
console.print(Panel(source, title="__init__.py", border_style="green"))
|
|
470
|
+
|
|
471
|
+
# Check for config
|
|
472
|
+
config_file = target / "config.py"
|
|
473
|
+
if config_file.exists():
|
|
474
|
+
console.print(Panel(config_file.read_text(), title="config.py", border_style="yellow"))
|
|
475
|
+
|
|
476
|
+
# Check for prompts
|
|
477
|
+
prompts_file = target / "prompts.json"
|
|
478
|
+
if prompts_file.exists():
|
|
479
|
+
data = json.loads(prompts_file.read_text())
|
|
480
|
+
console.print(
|
|
481
|
+
Panel(
|
|
482
|
+
json.dumps(data, indent=2),
|
|
483
|
+
title=f"prompts.json ({len(data)} versions)",
|
|
484
|
+
border_style="blue",
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
click.echo(f"🔍 Agent: {name}")
|
|
489
|
+
click.echo(f" Path: {target}")
|
|
490
|
+
click.echo(f" Files: {len(files)}")
|
|
491
|
+
for f in files:
|
|
492
|
+
click.echo(f" 📄 {f.relative_to(target)}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# =====================================================================
|
|
496
|
+
# DOCTOR — Environment health check
|
|
497
|
+
# =====================================================================
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@cli.command()
|
|
501
|
+
@click.option("--agents-dir", default="agents", help="Agents directory")
|
|
502
|
+
def doctor(agents_dir: str) -> None:
|
|
503
|
+
"""Check environment health and dependencies."""
|
|
504
|
+
_print_banner()
|
|
505
|
+
|
|
506
|
+
checks: list[tuple[str, bool, str]] = []
|
|
507
|
+
|
|
508
|
+
# Python version
|
|
509
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
510
|
+
ok = sys.version_info >= (3, 11)
|
|
511
|
+
checks.append(("Python", ok, f"{py_ver} {'✅' if ok else '(need ≥3.11)'}"))
|
|
512
|
+
|
|
513
|
+
# Core deps
|
|
514
|
+
for pkg in ["fastapi", "uvicorn", "pydantic", "loguru", "httpx"]:
|
|
515
|
+
try:
|
|
516
|
+
mod = importlib.import_module(pkg)
|
|
517
|
+
ver = getattr(mod, "__version__", "installed")
|
|
518
|
+
checks.append((pkg, True, ver))
|
|
519
|
+
except ImportError:
|
|
520
|
+
checks.append((pkg, False, "not installed"))
|
|
521
|
+
|
|
522
|
+
# Optional deps
|
|
523
|
+
for pkg, extra in [
|
|
524
|
+
("langgraph", "langgraph"),
|
|
525
|
+
("langchain_core", "langchain"),
|
|
526
|
+
("rich", "cli"),
|
|
527
|
+
("questionary", "cli"),
|
|
528
|
+
("chainlit", "ui"),
|
|
529
|
+
("sqlalchemy", "db"),
|
|
530
|
+
("prometheus_client", "metrics"),
|
|
531
|
+
]:
|
|
532
|
+
try:
|
|
533
|
+
mod = importlib.import_module(pkg)
|
|
534
|
+
ver = getattr(mod, "__version__", "installed")
|
|
535
|
+
checks.append((f"{pkg} [{extra}]", True, ver))
|
|
536
|
+
except ImportError:
|
|
537
|
+
checks.append((f"{pkg} [{extra}]", False, f"pip install agentomatic[{extra}]"))
|
|
538
|
+
|
|
539
|
+
# Agents directory
|
|
540
|
+
agents_path = Path(agents_dir)
|
|
541
|
+
if agents_path.exists():
|
|
542
|
+
count = len(
|
|
543
|
+
[d for d in agents_path.iterdir() if d.is_dir() and (d / "__init__.py").exists()]
|
|
544
|
+
)
|
|
545
|
+
checks.append(("Agents directory", True, f"{count} agent(s) in {agents_path}"))
|
|
546
|
+
else:
|
|
547
|
+
checks.append(("Agents directory", False, f"Not found: {agents_path}"))
|
|
548
|
+
|
|
549
|
+
if HAS_RICH:
|
|
550
|
+
table = Table(title="🩺 Environment Health Check", show_lines=True)
|
|
551
|
+
table.add_column("Component", style="bold")
|
|
552
|
+
table.add_column("Status", justify="center")
|
|
553
|
+
table.add_column("Details")
|
|
554
|
+
|
|
555
|
+
for check_name, ok, detail in checks:
|
|
556
|
+
status = "[green]✅[/green]" if ok else "[red]❌[/red]"
|
|
557
|
+
style = "" if ok else "dim"
|
|
558
|
+
table.add_row(check_name, status, f"[{style}]{detail}[/{style}]" if style else detail)
|
|
559
|
+
|
|
560
|
+
console.print(table)
|
|
561
|
+
|
|
562
|
+
all_ok = all(ok for _, ok, _ in checks[:6]) # Core deps only
|
|
563
|
+
if all_ok:
|
|
564
|
+
logger.success("All core dependencies satisfied!")
|
|
565
|
+
else:
|
|
566
|
+
logger.error("Some core dependencies are missing")
|
|
567
|
+
else:
|
|
568
|
+
click.echo("🩺 Environment Health Check")
|
|
569
|
+
for check_name, ok, detail in checks:
|
|
570
|
+
status = "✅" if ok else "❌"
|
|
571
|
+
click.echo(f" {status} {check_name}: {detail}")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# =====================================================================
|
|
575
|
+
# UI — Launch debug UI
|
|
576
|
+
# =====================================================================
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@cli.command()
|
|
580
|
+
@click.option("--host", default="localhost", help="Platform API host")
|
|
581
|
+
@click.option("--port", type=int, default=8000, help="Platform API port")
|
|
582
|
+
@click.option("--ui-port", type=int, default=8001, help="UI port")
|
|
583
|
+
def ui(host: str, port: int, ui_port: int) -> None:
|
|
584
|
+
"""Launch the Chainlit debug UI."""
|
|
585
|
+
_print_banner()
|
|
586
|
+
|
|
587
|
+
from agentomatic.ui import is_available
|
|
588
|
+
|
|
589
|
+
if not is_available():
|
|
590
|
+
logger.error("Chainlit not installed")
|
|
591
|
+
click.echo(" Install: pip install agentomatic[ui]")
|
|
592
|
+
sys.exit(1)
|
|
593
|
+
|
|
594
|
+
logger.success("Launching debug UI...")
|
|
595
|
+
click.echo(f" Platform API: http://{host}:{port}")
|
|
596
|
+
|
|
597
|
+
import subprocess
|
|
598
|
+
|
|
599
|
+
chat_path = Path(__file__).parent.parent / "ui" / "chat.py"
|
|
600
|
+
subprocess.run(
|
|
601
|
+
[sys.executable, "-m", "chainlit", "run", str(chat_path), "--port", str(ui_port)],
|
|
602
|
+
env={
|
|
603
|
+
**__import__("os").environ,
|
|
604
|
+
"AGENTOMATIC_API_URL": f"http://{host}:{port}",
|
|
605
|
+
},
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# =====================================================================
|
|
610
|
+
# OPTIMIZE — Prompt optimization
|
|
611
|
+
# =====================================================================
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@cli.command()
|
|
615
|
+
@click.argument("agent")
|
|
616
|
+
@click.option("--dataset", "-d", required=True, help="Path to JSONL/CSV dataset")
|
|
617
|
+
@click.option("--metrics", "-m", default="exact_match", help="Comma-separated metrics")
|
|
618
|
+
@click.option(
|
|
619
|
+
"--strategy",
|
|
620
|
+
"-s",
|
|
621
|
+
default="iterative_rewrite",
|
|
622
|
+
type=click.Choice(["iterative_rewrite", "few_shot", "chain_of_thought"]),
|
|
623
|
+
help="Optimization strategy",
|
|
624
|
+
)
|
|
625
|
+
@click.option("--max-iterations", type=int, default=10, help="Max iterations")
|
|
626
|
+
@click.option("--target-score", type=float, default=0.9, help="Target score")
|
|
627
|
+
@click.option("--rewrite-llm", default=None, help="LLM for prompt rewriting")
|
|
628
|
+
@click.option("--eval-llm", default=None, help="LLM for evaluation")
|
|
629
|
+
@click.option("--llm", default="ollama/mistral:7b", help="Default LLM")
|
|
630
|
+
@click.option("--patience", type=int, default=3, help="Early stopping patience")
|
|
631
|
+
@click.option("--prompt", default=None, help="Initial prompt (overrides prompts.json)")
|
|
632
|
+
@click.option("--no-report", is_flag=True, help="Skip HTML report generation")
|
|
633
|
+
@click.option("--apply", "auto_apply", is_flag=True, help="Auto-apply best prompt")
|
|
634
|
+
@click.option("--host", default="http://localhost:8000", help="Platform API base")
|
|
635
|
+
def optimize(
|
|
636
|
+
agent: str,
|
|
637
|
+
dataset: str,
|
|
638
|
+
metrics: str,
|
|
639
|
+
strategy: str,
|
|
640
|
+
max_iterations: int,
|
|
641
|
+
target_score: float,
|
|
642
|
+
rewrite_llm: str | None,
|
|
643
|
+
eval_llm: str | None,
|
|
644
|
+
llm: str,
|
|
645
|
+
patience: int,
|
|
646
|
+
prompt: str | None,
|
|
647
|
+
no_report: bool,
|
|
648
|
+
auto_apply: bool,
|
|
649
|
+
host: str,
|
|
650
|
+
) -> None:
|
|
651
|
+
"""Run prompt optimization for an agent."""
|
|
652
|
+
import asyncio
|
|
653
|
+
|
|
654
|
+
try:
|
|
655
|
+
from agentomatic.optimize import Dataset, PromptOptimizer
|
|
656
|
+
except ImportError:
|
|
657
|
+
logger.error("Optimization module not available.")
|
|
658
|
+
click.echo("Install: pip install agentomatic[optimize]")
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
# Load dataset
|
|
662
|
+
if dataset.endswith(".csv"):
|
|
663
|
+
ds = Dataset.from_csv(dataset)
|
|
664
|
+
else:
|
|
665
|
+
ds = Dataset.from_jsonl(dataset)
|
|
666
|
+
|
|
667
|
+
logger.info(f"Dataset: {len(ds)} points from {dataset}")
|
|
668
|
+
|
|
669
|
+
# Parse metrics
|
|
670
|
+
metric_list = [m.strip() for m in metrics.split(",")]
|
|
671
|
+
|
|
672
|
+
# Create optimizer
|
|
673
|
+
optimizer = PromptOptimizer(
|
|
674
|
+
agent=agent,
|
|
675
|
+
metrics=metric_list, # type: ignore[arg-type]
|
|
676
|
+
llm=llm,
|
|
677
|
+
rewrite_llm=rewrite_llm,
|
|
678
|
+
eval_llm=eval_llm,
|
|
679
|
+
strategy=strategy,
|
|
680
|
+
api_base=host,
|
|
681
|
+
auto_report=not no_report,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# Run optimization
|
|
685
|
+
result = asyncio.run(
|
|
686
|
+
optimizer.optimize(
|
|
687
|
+
dataset=ds,
|
|
688
|
+
initial_prompt=prompt,
|
|
689
|
+
max_iterations=max_iterations,
|
|
690
|
+
target_score=target_score,
|
|
691
|
+
patience=patience,
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Print report
|
|
696
|
+
click.echo(result.report())
|
|
697
|
+
|
|
698
|
+
# Auto-apply if requested
|
|
699
|
+
if auto_apply:
|
|
700
|
+
version = result.apply()
|
|
701
|
+
logger.success(f"Applied as '{version}'")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# =====================================================================
|
|
705
|
+
# Backward-compatible main() entry point
|
|
706
|
+
# =====================================================================
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def main() -> None:
|
|
710
|
+
"""CLI entry point (backward compatible)."""
|
|
711
|
+
cli()
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
if __name__ == "__main__":
|
|
715
|
+
cli()
|