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.
Files changed (55) hide show
  1. agentomatic/__init__.py +59 -0
  2. agentomatic/_version.py +5 -0
  3. agentomatic/cli/__init__.py +7 -0
  4. agentomatic/cli/commands.py +715 -0
  5. agentomatic/cli/templates.py +188 -0
  6. agentomatic/config/__init__.py +3 -0
  7. agentomatic/config/defaults.py +10 -0
  8. agentomatic/config/settings.py +117 -0
  9. agentomatic/core/__init__.py +31 -0
  10. agentomatic/core/lifespan.py +102 -0
  11. agentomatic/core/manifest.py +100 -0
  12. agentomatic/core/platform.py +571 -0
  13. agentomatic/core/registry.py +198 -0
  14. agentomatic/core/router_factory.py +541 -0
  15. agentomatic/core/state.py +63 -0
  16. agentomatic/middleware/__init__.py +18 -0
  17. agentomatic/middleware/auth.py +53 -0
  18. agentomatic/middleware/feedback.py +207 -0
  19. agentomatic/middleware/logging.py +40 -0
  20. agentomatic/middleware/metrics.py +93 -0
  21. agentomatic/middleware/rate_limit.py +70 -0
  22. agentomatic/observability/__init__.py +11 -0
  23. agentomatic/observability/concurrency.py +109 -0
  24. agentomatic/observability/metrics.py +101 -0
  25. agentomatic/observability/telemetry.py +316 -0
  26. agentomatic/optimize/__init__.py +112 -0
  27. agentomatic/optimize/dataset.py +142 -0
  28. agentomatic/optimize/loop.py +870 -0
  29. agentomatic/optimize/metrics.py +781 -0
  30. agentomatic/optimize/optimizer.py +891 -0
  31. agentomatic/optimize/report.py +774 -0
  32. agentomatic/optimize/runner.py +261 -0
  33. agentomatic/optimize/strategies.py +592 -0
  34. agentomatic/optimize/synthesizer.py +729 -0
  35. agentomatic/prompts/__init__.py +7 -0
  36. agentomatic/prompts/manager.py +59 -0
  37. agentomatic/protocols/__init__.py +3 -0
  38. agentomatic/protocols/decorators.py +75 -0
  39. agentomatic/providers/__init__.py +3 -0
  40. agentomatic/providers/embeddings.py +44 -0
  41. agentomatic/providers/llm.py +116 -0
  42. agentomatic/py.typed +1 -0
  43. agentomatic/storage/__init__.py +40 -0
  44. agentomatic/storage/base.py +192 -0
  45. agentomatic/storage/memory.py +167 -0
  46. agentomatic/storage/models.py +129 -0
  47. agentomatic/storage/sqlalchemy.py +317 -0
  48. agentomatic/ui/.chainlit/config.toml +14 -0
  49. agentomatic/ui/__init__.py +50 -0
  50. agentomatic/ui/chat.py +198 -0
  51. agentomatic-0.1.0.dist-info/METADATA +363 -0
  52. agentomatic-0.1.0.dist-info/RECORD +55 -0
  53. agentomatic-0.1.0.dist-info/WHEEL +4 -0
  54. agentomatic-0.1.0.dist-info/entry_points.txt +2 -0
  55. 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()