---ci--- phase: 7 milestone: v0.2 status: complete requirements: covered: [REQ-2-001, REQ-2-002, REQ-2-003, REQ-2-004, REQ-2-005, REQ-2-006, REQ-2-007, REQ-2-008, REQ-2-009, REQ-2-010, REQ-2-011, REQ-2-012] partial: [] ---/ci--- Milestone v0.2 (ai-tutor-architecture) merged to main. Escalation record (audit remediation, durable): P1 executor delegation failed twice (empty subagent results, zero files created); auto-resolved at full autonomy to inline execution with identical plan fidelity (commit 3271373, reflog-only after phase branch squash-delete).
3.6 KiB
Nextcraft AI Service (apps/ai-service)
Python FastAPI service hosting the six AI tutor agents (Coach, Tutor, Lab, Assessor, Proctor, Mentor) behind a provider-agnostic LLM layer. Port 8420.
Quickstart
# 1. Bootstrap (idempotent): venv + deps
bash scripts/bootstrap.sh
# 2. Run tests (mock provider only — zero network calls)
bash scripts/test.sh
# 3. Lint
bash scripts/lint.sh
# 4. Dev server (exports keys from .ciagent/.env.secrets if present)
bash scripts/dev.sh
Or via the monorepo root (corepack pnpm install first):
pnpm ai:bootstrap
pnpm ai:test
pnpm ai:lint
pnpm ai:dev
Configuration
All settings use the AI_ env prefix (pydantic-settings; see .env.example).
| Var | Default | Purpose |
|---|---|---|
AI_PORT |
8420 | Listen port |
AI_PROVIDER |
mock | ollama-cloud | local | mock |
AI_MODEL |
gemma4:31b | Model for all agents |
AI_OLLAMA_CLOUD_BASE_URL |
https://ollama.com/v1 | Cloud base URL |
AI_OLLAMA_CLOUD_API_KEY |
(empty) | Bearer key — never commit |
AI_JSON_MODE |
auto | auto sends response_format, degrades on 400; off never sends |
Tests run with AI_PROVIDER=mock (enforced in tests/conftest.py by an instance assertion) — the suite never calls the cloud.
Endpoints
GET /health— status, configured provider, model (no cloud call)POST /v1/chat/stream— SSE chat stream. Body:{"agent": "coach"|"tutor", "session_id": "...", "messages": [{"role":"user","content":"..."}]}. Unknown agents are rejected with 422.
SSE envelope (D-016): meta event first (agent/session/model), then delta events (incremental content), then done; on mid-stream failure an error event precedes the terminal [DONE] sentinel. sse-starlette emits : ping keep-alive comment lines on idle connections — clients must ignore frames without data:.
Manual ollama-cloud persona probe (Phase 3, documented — not automated)
With the real provider, Coach and Tutor must produce distinct on-persona responses to the same prompt:
# start with the cloud provider (keys exported from .ciagent/.env.secrets)
AI_PROVIDER=ollama-cloud .venv/bin/uvicorn ai_service.main:app --port 8420
# Coach: expect pacing + one concrete next action + a retrieval-practice question
curl -sN -X POST localhost:8420/v1/chat/stream -H 'Content-Type: application/json' \
-d '{"agent":"coach","session_id":"probe-coach","messages":[{"role":"user","content":"I am stuck on multi-agent communication patterns"}]}' \
| grep '^data:'
# Tutor: expect ONE concept + a worked example + a Socratic check question
curl -sN -X POST localhost:8420/v1/chat/stream -H 'Content-Type: application/json' \
-d '{"agent":"tutor","session_id":"probe-tutor","messages":[{"role":"user","content":"I am stuck on multi-agent communication patterns"}]}' \
| grep '^data:'
Verify: the two responses have visibly different voice/structure (Coach: action + accountability; Tutor: concept + example + question). The automated suite never calls the cloud — distinctness is enforced against the deterministic mock (distinct system prompts → distinct hash-seeded outputs).
Layout
ai_service/
main.py app factory, lifespan (httpx pool), CORS, /health
config.py pydantic-settings
api/ endpoints (SSE envelope lives here, D-016)
llm/ provider layer — dumb pipe, no envelope logic
scripts/ bootstrap.sh dev.sh test.sh lint.sh
tests/ pytest — mock provider only
Boundary rules: llm/ imports nothing from agents/ or api/; agents/ imports nothing from api/.