AMRYB's picture
Upload 91 files
287f3d3 verified
Raw
History Blame Contribute Delete
8.61 kB
"""Interactive CLI demo of the agentic system.
Run with ``python -m agentic_core.cli``. Walks the exact demo flow:
vague idea -> discovery questions -> summary -> confirm -> autonomous
engineering with live progress -> rendered artifacts.
"""
from __future__ import annotations
import asyncio
from .artifacts import ArtifactStore, render_all
from .config import get_settings
from .llm import LLMService, create_llm_provider
from .orchestrator import DiscoveryError, EventBus, Orchestrator
from .project_store import ProjectStore
def parse_user_answer(raw: str, options: list[str]) -> str:
"""Turn a CLI answer into text: option numbers become their option text,
anything else is used verbatim (the user's own answer)."""
text = raw.strip()
if not options or not text:
return text
parts = [p.strip() for p in text.replace(",", " ").split() if p.strip()]
if parts and all(p.isdigit() for p in parts):
picked = [options[int(p) - 1] for p in parts if 1 <= int(p) <= len(options)]
if picked:
return "; ".join(picked)
return text
async def _wait_with_progress(coro):
"""Await *coro* while printing a heartbeat so long agent runs don't feel stuck."""
task = asyncio.create_task(coro)
while not task.done():
await asyncio.sleep(5)
print(".", end="", flush=True)
print()
return task.result()
def _print_event(event) -> None:
"""Render a live progress event for the user-facing demo.
Kept human: symbol + agent + short reason + elapsed seconds. Raw telemetry
(token counts, schema sizes) belongs in the benchmark/debug output, not the
demo CLI, so the run feels like an autonomous engineering system.
"""
symbols = {
"workflow_started": "▶",
"agent_started": "→",
"agent_completed": "✓",
"agent_retrying": "↻",
"agent_failed": "✗",
"review_started": "◈",
"review_completed": "✓",
"review_failed": "⚠",
"workflow_completed": "✔",
"workflow_failed": "✗",
}
symbol = symbols.get(event.event, "•")
label = event.agent or event.event
detail = f" — {event.reason}" if event.reason else ""
if event.invocation is not None and event.invocation > 1:
detail += f" [invocation #{event.invocation}]"
if event.duration_ms is not None:
detail += f" ({event.duration_ms / 1000:.0f}s)"
print(f" {symbol} {label}{detail}")
def _print_call_summary(results: dict) -> None:
counts = results.get("call_counts", {})
revisions = results.get("revisions", {})
if not counts:
return
order = ["requirements", "architecture", "database", "api", "devops", "reviewer"]
print("\n" + "=" * 60)
print("TOTAL LLM CALLS")
print("=" * 60)
total = 0
for agent in order:
n = counts.get(agent, 0)
total += n
revision = f" (revised x{revisions.get(agent, 0)})" if revisions.get(agent, 0) else ""
print(f" {agent:<14} {n}{revision}")
print(f" {'TOTAL':<14} {total}")
async def run() -> None:
settings = get_settings()
provider = create_llm_provider(settings)
llm_service = LLMService(provider, settings)
event_bus = EventBus()
orchestrator = Orchestrator(llm_service, event_bus, None, settings)
project_store = ProjectStore(settings.db_path, legacy_dir=settings.projects_dir)
print("=" * 60)
print("Agentic AI Core — Business Idea to Engineering Blueprint")
print("=" * 60)
try:
idea = input("\nDescribe your business idea: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
return
if not idea:
print("No idea provided. Exiting.")
return
context = project_store.create(idea)
print(f"\n[project {context.project_id}] Starting discovery…\n")
try:
print("Analyzing your idea (can take a minute)…", end="", flush=True)
output = await _wait_with_progress(orchestrator.discovery_turn(context, idea))
except DiscoveryError as exc:
print(f"Discovery failed: {exc}")
return
while output.status != "ready":
if not output.questions:
# Agent says more info is needed but asked nothing: nudge it once
# instead of looping forever.
print(" (agent needs a bit more detail — nudging it to proceed)")
context.add_turn("user", "Please continue.")
print("Updating understanding…", end="", flush=True)
try:
output = await _wait_with_progress(orchestrator.discovery_turn(context))
except DiscoveryError as exc:
print(f"Discovery failed: {exc}")
return
continue
for idx, question in enumerate(output.questions, 1):
print(f"\n{idx}. {question.question} ({question.reason})")
if question.options:
for j, option in enumerate(question.options, 1):
print(f" {j}) {option}")
answers = []
for question in output.questions:
hint = " (pick a number, several like 1,3, or type your own)" if question.options else ""
print(hint)
try:
raw = input("\n> ")
except (EOFError, KeyboardInterrupt):
print("\nBye.")
return
answer = parse_user_answer(raw, question.options)
if not answer:
print(" (empty answer ignored — type something or pick an option so discovery can continue)")
answers.append(None)
else:
answers.append(answer)
real_answers = [a for a in answers if a]
if not real_answers:
print(" (no answers provided — nothing sent to discovery)")
continue
# Batch every answer into a single discovery run: one turn instead of
# one Cursor run per question, cutting discovery cost dramatically.
for answer in real_answers:
context.add_turn("user", answer)
print("Updating understanding…", end="", flush=True)
try:
output = await _wait_with_progress(orchestrator.discovery_turn(context))
except DiscoveryError as exc:
print(f"Discovery failed: {exc}")
return
print("\n" + "=" * 60)
print("YOUR PROJECT UNDERSTANDING")
print("=" * 60)
print(output.summary)
print("\n--- Context ---")
print(f"Problem: {context.problem or '-'}")
print(f"Users: {', '.join(context.target_users) or '-'}")
print(f"Roles: {', '.join(context.user_roles) or '-'}")
print(f"Goals: {', '.join(context.business_goals) or '-'}")
print(f"Features: {', '.join(context.core_features) or '-'}")
print(f"Constraints: {', '.join(context.constraints) or '-'}")
print(f"Integrations: {', '.join(context.integrations) or '-'}")
print(f"Tech pref: {', '.join(context.technology_preferences) or '-'}")
try:
confirm = input("\n[Confirm & Generate] (y/n): ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
return
if confirm not in ("y", "yes"):
print("Generation cancelled.")
return
orchestrator.confirm(context)
event_bus.subscribe(_print_event)
print("\n" + "=" * 60)
print("AUTONOMOUS ENGINEERING WORKFLOW")
print("=" * 60)
try:
results = await orchestrator.generate(context)
finally:
event_bus.unsubscribe(_print_event)
_print_call_summary(results)
project_store.save(context)
if context.status in ("approved", "revised"):
print("\n" + "=" * 60)
print("FINAL PROJECT BLUEPRINT")
print("=" * 60)
files = render_all(context)
artifact_store = ArtifactStore(settings.artifacts_dir)
for name, content in files.items():
artifact_store.write(context.project_id, name, content)
for name in sorted(files):
print(f" • {name}")
print(f"\nArtifacts saved under: {settings.artifacts_dir / context.project_id}")
else:
print(f"\nWorkflow finished with status: {context.status}")
if __name__ == "__main__":
try:
asyncio.run(run())
except KeyboardInterrupt:
print("\nBye.")