Spaces:
Paused
Paused
File size: 8,610 Bytes
287f3d3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """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.") |