Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/captiveau-playground_mulai-plus.json
T

1 line
14 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "captiveau-playground/mulai-plus", "files": {"apps/ai/README.md": {"ff4136d15af2f80cdb7414465c3b959813c25f56": {"t": 1785846947.0563972, "n": 11143, "h": "724512bc54f3f651", "c": "# 🤖 MULAI+ AI Service — Chatbot Engine\n\nFastAPI service untuk chatbot MULAI+. Menangani LLM chat, tool calling (universitas, prodi, passing grade), answer caching, session management, dan feedback.\n\n## Arsitektur\n\n```\n┌──────────────────────────────────────────────────────────────────────────┐\n│ FRONTEND (Next.js) │\n│ chatbot-widget.tsx │\n│ ├── fetch(API_ENDPOINT) → POST /ai/chat (SSE streaming) │\n│ ├── fetch(/ai/quota) → GET /ai/quota (sisa chat) │\n│ ├── fetch(/ai/history) → GET /ai/history (riwayat chat) │\n│ ├── fetch(/ai/feedback) → POST feedback (thumbs up/down) │\n│ └── fetch(/ai/track/...) → POST tracking │\n└──────────────────────────┬───────────────────────────────────────────────┘\n │ POST /ai/chat (x-session-id)\n ▼\n┌──────────────────────────────────────────────────────────────────────────┐\n│ SERVER PROXY (Hono) │\n│ apps/server/src/index.ts │\n│ ├── CORS middleware │\n│ ├── requireAdmin middleware (/ai/admin/*) │\n│ ├── Public proxy (/ai/*) → forward ke AI service │\n│ └── Forward: x-user-id, x-session-id, Authorization: Bearer │\n└──────────────────────────┬───────────────────────────────────────────────┘\n │ http://ai:8000/api/chat\n ▼\n┌──────────────────────────────────────────────────────────────────────────┐\n│ AI SERVICE (FastAPI / Python) │\n│ │\n│ routes.py cache.py │\n│ ┌──────────────────────┐ ┌──────────────────┐ │\n│ │ POST /api/chat │ │ Exact match │ │\n│ │ ├─ Check cache ────┼─ HIT ──▶│ (SHA256 hash) │ │\n│ │ ├─ (if miss) quota │ │ ├──────────────────┤ │\n│ │ ├─ Call LLM │ │ │ Fuzzy match │ │\n│ │ ├─ Save messages │ │ │ (token overlap + │ │\n│ │ └─ SSE response │ │ │ SequenceMatcher,│ │\n│ │ │ │ │ threshold 0.82) │ │\n│ │ GET /api/quota │ │ ├──────────────────┤ │\n│ │ GET /api/history │ │ │ LFU/LRU eviction │ │\n│ │ POST /api/feedback │ │ │ (max 1000 entry) │ │\n│ │ POST /api/lead │ │ └──────────────────┘ │\n│ └──────────────────────┘ │\n│ │\n│ engine/responder.py │\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ 1. Build messages (system prompt + history + user) │ │\n│ │ 2. Call LLM (AsyncOpenAI with tool calling) │ │\n│ │ 3. Process tool calls (search_universities, etc.) │ │\n│ │ 4. Call LLM again with tool results │ │\n│ │ 5. Extract follow-up topics from response │ │\n│ │ 6. Return (reply, follow_ups, token_usage) │ │\n│ └─────────────────────────────────────────────────────┘ │\n└──────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Struktur File\n\n```\napps/ai/\n├── src/\n│ ├── main.py # FastAPI app, CORS, auth middleware, startup\n│ ├── routes.py # API endpoints: chat, quota, history, admin sessions\n│ ├── config.py # Settings (env vars)\n│ ├── schemas.py # Pydantic models (request/response validation)\n│ ├── db.py # DB pool + read-only query helper (tools)\n│ ├── chatbot_db.py # Session, message, feedback, admin DB operations\n│ ├── cache.py # Answer cache: exact + fuzzy match, LFU/LRU\n│ ├── tools.py # LLM tool definitions & handlers (search DB)\n│ └── engine/\n│ └── responder.py # LLM wrapper (AsyncOpenAI, tool calling, cost)\n├── tests/\n│ ├── test_cache.py # 13 tests (normalize, similarity)\n│ ├── test_responder.py # 13 tests (topics, cost, fallback)\n│ └── test_schemas.py # 17 tests (Pydantic validation)\n├── requirements.txt\n└── README.md # ← ini\n```\n\n---\n\n## Flow Chat — Skenario\n\n### 🟢 Skenario 1: Chat Pertama (Guest, cache miss)\n\n```\nUser: \"Cari universitas negeri di Jakarta\"\n ↓\nWidget: generate session_id → localStorage\n ↓\nWidget: POST /ai/chat (header: x-session-id)\n ↓\nServer: forward ke AI Service (Authorization: Bearer + x-session-id)\n ↓\nAI Service:\n 1. get_or_create_session → session baru (message_count=0)\n 2. cache.get_cached_answer → MISS (pertanyaan pertama)\n 3. _check_session_allowed → GUEST_LIMIT=1, 0 < 1 → OK\n 4. get_history → []\n 5. get_response() → LLM call + tool calling\n ├─ SYSTEM_PROMPT + user message\n ├─ LLM decides: perlu data real-time?\n │ YES → call search_universities(\"Jakarta\")\n │ │ → DB query → format hasil\n │ │ → LLM response with results\n │ NO → langsung jawab dari pengetahuan\n ├─ content = \"Berikut universitas negeri di Jakarta: UI, UNJ...\"\n └─ follow_ups = [\"Info passing grade UI\", \"Jurusan favorit\", ...]\n 6. cache.set_cached_answer() → simpan\n 7. save_message (user + assistant)\n 8. increment_message_count → 1/1\n 9. return SSE stream\n ↓\nWidget: terima SSE → streaming typewriter → tampilkan\n sisa quota = 0\n```\n\n**Hasil:** ✅ Chat sukses, jawaban tampil. Cache terisi, quota guest habis.\n\n---\n\n### 🟢 Skenario 2: Chat Kedua (Guest, cache HIT — pertanyaan mirip)\n\n```\nUser: \"Universitas negeri Jakarta\"\n ↓\nAI Service:\n 1. cache.get_cached_answer → HIT (fuzzy match 0.85 ≥ 0.82)\n └─ langsung return SSE tanpa LLM, tanpa quota check\n (jawaban dari chat pertama)\n ↓\nWidget: tampilkan jawaban instan (< 50ms)\n```\n\n**Hasil:** ✅ Langsung balas dari cache. Gak kena quota, gak kena LLM cost.\n\n---\n\n### 🟡 Skenario 3: Quota Guest Habis (pertanyaan beda)\n\n```\nUser: \"Info beasiswa LPDP\"\n ↓\nAI Service:\n 1. cache.get_cached_answer → MISS\n 2. quota check: message_count(1) >= GUEST_LIMIT(1) → HABIS\n 3. return JSON {\"reply\": \"Kamu sudah menggunakan chat gratis!...\", \"requires_auth\": true}\n ↓\nWidget: lihat contentType = application/json\n tampilkan AUTH_GATE → button \"Login / Daftar Gratis\"\n klik → redirect ke /login?callbackUrl=...\n```\n\n**Hasil:** ✅ User diarahkan login. Data click tercatat (`/ai/track/login-click`).\n\n---\n\n### 🟡 Skenario 4: Auth user dengan quota override\n\n```\nUser: \"Program mentoring\" (login, quota 5 habis)\n ↓\nAI Service:\n 1. x-user-id terdeteksi → is_auth = True\n 2. get_active_limit → cek credit_limit di session\n ├─ admin set credit_limit = 10 → pakai 10\n └─ admin set -1 → unlimited\n```\n\n**Hasil:** ✅ Admin override limit via `PUT /api/admin/sessions/{id}/credit`.\n\n---\n\n### 🔴 Skenario 5: User kena ban\n\n```\nUser: \"Halo\"\n ↓\nAI Service:\n 1. is_banned → TRUE\n 2. return {\"reply\": \"Akun kamu telah dibatasi...\"}\n```\n\n**Hasil:** ✅ User diblokir. Admin unban via Portainer.\n\n---\n\n### 🔴 Skenario 6: LLM error / timeout\n\n```\nUser: \"Cari prodi teknik\"\n ↓\nget_response → AsyncOpenAI timeout (30s) → Exception\n ↓\ncatch → random.choice(FALLBACK_REPLIES)\n └─ \"Maaf, layanan sedang sibuk...\" + 3 suggestions\n```\n\n**Hasil:** ✅ Graceful fallback.\n\n---\n\n### 🔴 Skenario 7: Invalid input\n\n```\nUser: \"\" (empty)\n ↓\nFastAPI: Pydantic ValidationError → 422\n ↓\nWidget: catch → tampilkan \"Maaf, terjadi kesalahan\"\n```\n\n**Hasil:** ❌ Request ditolak. Widget tampilkan error.\n\n---\n\n### 🔴 Skenario 8: Cache / DB down\n\n```\nUser: \"Info UI\"\n ↓\ncache.get_cached_answer error (DB timeout)\n ↓\nException → skip cache, langsung call LLM\n```\n\n**Hasil:** ✅ Graceful degradation. Cache offline → fallback ke LLM.\n\n---\n\n## State Machine Widget\n\n```\n ┌──────────────────────────┐\n │ IDLE │\n │ (menunggu input) │\n └─────┬────────────────────┘\n │ user mengirim chat\n ▼\n ┌──────────────────────────┐\n │ LOADING │\n │ (spinner + \"Mengetik...\")│\n └─────┬────────────────────┘\n │\n ┌───────────┼───────────┐\n │ │ │\n ▼ ▼ ▼\n ┌──────────────┐ ┌──────────┐ ┌──────────┐\n │ SSE STREAM │ │ JSON 200 │ │ ERROR │\n │ (text/event- │ │(quota │ │(fetch │\n │ stream) │ │ habis) │ │ gagal) │\n └──────┬───────┘ └────┬─────┘ └────┬─────┘\n │ │ │\n ▼ ▼ ▼\n ┌──────────────┐ ┌──────────┐ ┌──────────┐\n │ TAMPILKAN │ │ AUTH │ │ TAMPILKAN│\n │ + typewriter │ │ GATE │ │ ERROR │\n │ + follow-up │ │ (login) │ │ MESSAGE │\n └──────┬───────┘ └──────────┘ └──────────┘\n │\n ▼\n ┌──────────────┐\n │ IDLE │\n └──────────────┘\n```\n\n---\n\n## Kunci Performa\n\n| Aspek | Strategi |\n|-------|----------|\n| **Cache** | Exact match (SHA256) + fuzzy (token overlap + SequenceMatcher, threshold 0.82) |\n| **Eviction** | LFU (hit_count ASC) + LRU (last_accessed_at ASC). Max 1000 entries |\n| **Quota** | Cache HIT tidak kena quota. Guest limit 1, auth limit 5 (admin override) |\n| **LLM** | `AsyncOpenAI` timeout 30s, max_retries 1. Follow-up dari response, bukan LLM call baru |\n| **Error** | 3 fallback random. Graceful degradation tiap layer |\n\n---\n\n## Setup & Test\n\n```bash\n# Setup\ncd apps/ai\npython3 -m venv .venv\n.venv/bin/pip install -r requirements.txt\n\n# Run (development)\nAI_API_KEY=test-key .venv/bin/uvicorn src.main:app --host 0.0.0.0 --port 8000\n\n# Test\n.venv/bin/python3 -m pytest tests/ -v\n# 43 tests: cache (13), responder (13), schemas (17)\n```\n\n### Environment Variables\n\n| Variable | Default | Keterangan |\n|----------|---------|------------|\n| `AI_API_KEY` | `\"\"` | Shared secret. Wajib diset, kalo kosong semua request 503 |\n| `AI_HOST` | `0.0.0.0` | Bind address |\n| `AI_PORT` | `8000` | Port |\n| `DATABASE_URL` | – | PostgreSQL connection string |\n| `OPENAI_API_KEY` | – | API key LLM provider |\n| `OPENAI_BASE_URL` | `https://opencode.ai/zen/go/v1` | LLM endpoint |\n| `OPENAI_MODEL` | `deepseek-v4-flash` | Model name |\n| `CORS_ORIGIN` | `http://localhost:3001` | Frontend URL untuk CORS |\n\n### Docker\n\n```bash\n# Build\ndocker build -t mulai-ai:local .\n\n# Run\ndocker run -p 8000:8000 --env-file .env mulai-ai:local\n```\n"}}}}