M2: agent logic in text mode

- LLMProvider interface: AnthropicLLM + ScriptedLLM test fake
- System prompt composition from super-admin template + structured tenant
  data only (§6.3); auto-generated greetings with recording disclosure,
  out-of-hours variant, chat variant
- Agent tools (§6.5): get_salon_info, check_availability (≤8 slots to model,
  agent offers ≤3), submit_booking_request (caller-ID fallback, hallucinated
  service-id guard), take_message
- Conversation loop with tool dispatch, round-limit guard, transcript capture,
  outcome classification (request_created/message_taken/info_only/abandoned)
- Terminal playground (python -m gogo.agent.cli) + demo seed (python -m gogo.seed)
- Deterministic loop tests (ScriptedLLM) + Appendix A golden-scenario
  acceptance tests against a real LLM (skipped without an API key)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:53:08 +02:00
parent e855650f09
commit 0d0838e6e2
9 changed files with 1445 additions and 0 deletions

58
gogo/agent/cli.py Normal file
View File

@@ -0,0 +1,58 @@
"""Terminal playground: text conversation with a tenant's agent (M2).
Usage: python -m gogo.agent.cli salon-merima [--voice]
Requires GOGO_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY via the SDK default).
"""
from __future__ import annotations
import asyncio
import sys
from sqlalchemy import select
from gogo.agent.loop import AgentConversation
from gogo.db import get_sessionmaker
from gogo.models import Tenant
async def main() -> None:
args = [a for a in sys.argv[1:] if not a.startswith("--")]
slug = args[0] if args else "salon-merima"
channel = "voice" if "--voice" in sys.argv else "chat"
async with get_sessionmaker()() as session:
tenant = (
await session.execute(select(Tenant).where(Tenant.slug == slug))
).scalar_one_or_none()
if tenant is None:
print(f"tenant '{slug}' not found — run: python -m gogo.seed")
sys.exit(1)
convo = AgentConversation(
session,
tenant,
channel=channel,
caller_phone="+38765123456" if channel == "voice" else "",
)
print(f"[{tenant.name}{channel} mode, ctrl-d to exit]\n")
print(f"Gogo: {await convo.greeting()}\n")
while True:
try:
text = input("Vi: ").strip()
except (EOFError, KeyboardInterrupt):
break
if not text:
continue
reply = await convo.user_turn(text)
await session.commit()
print(f"\nGogo: {reply}\n")
print(f"\n[outcome: {convo.outcome}]")
for name, tool_input in convo.executor.tool_calls:
print(f"[tool] {name}({tool_input})")
if __name__ == "__main__":
asyncio.run(main())