Files
nextcraft/apps/ai-service/README.md
T
CIAgent 88a1dab810 docs(milestone): complete v0.2-ai-tutor-architecture
---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).
2026-09-11 17:34:50 +00:00

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/.