diff --git a/.agents/skills/_LCBP3-CONTEXT.md b/.agents/skills/_LCBP3-CONTEXT.md index 5394ef8a..36b03caa 100644 --- a/.agents/skills/_LCBP3-CONTEXT.md +++ b/.agents/skills/_LCBP3-CONTEXT.md @@ -5,14 +5,14 @@ **Project:** NAP-DMS (LCBP3) — Laem Chabang Port Phase 3 Document Management System **Stack:** NestJS 11 + Next.js 16 + TypeScript + MariaDB 11.8 + Redis + BullMQ + Elasticsearch + Ollama (on-prem AI) -**Version:** 1.8.9 (2026-04-18) +**Version:** 1.9.7 (2026-05-25) --- ## 📌 Canonical Rule Sources (read in this order) 1. **`AGENTS.md`** (repo root) — primary rule file for AI agents; supersedes legacy `GEMINI.md`. -2. **`specs/06-Decision-Records/`** — architectural decisions (22 ADRs); ADR priority > Engineering Guidelines. +2. **`specs/06-Decision-Records/`** — architectural decisions (29 ADRs); ADR priority > Engineering Guidelines. 3. **`specs/05-Engineering-Guidelines/`** — backend/frontend/testing/i18n/git patterns. 4. **`specs/00-Overview/00-02-glossary.md`** — domain terminology (Correspondence / RFA / Transmittal / Circulation). 5. **`specs/00-Overview/00-03-product-vision.md`** — project constitution (Vision, Strategic Pillars, Guardrails). @@ -29,6 +29,7 @@ - **ADR-002 Document Numbering:** Redis Redlock + TypeORM `@VersionColumn` (double-lock). Never use application-side counter alone. - **ADR-008 Notifications:** BullMQ queue — never inline email/notification in a request thread. - **ADR-023/023A AI Boundary:** Ollama on Admin Desktop only; AI → DMS API → DB (never direct DB/storage). 2-model stack: `gemma4:e4b Q8_0` + `nomic-embed-text`. BullMQ `ai-realtime` / `ai-batch` queues. Human-in-the-loop validation required. (ADR-018 superseded by ADR-023) +- **ADR-029 Dynamic Prompt Management:** Prompt templates in DB (`ai_prompts`), never hardcoded in processor; Redis cache `ai:prompt:active:{type}` TTL 60s; `activate()` runs in DB transaction + Redis DEL after commit; `system.manage_all` guard on all mutations. - **ADR-007 Error Handling:** Layered (Validation / Business / System); `BusinessException` hierarchy; user-friendly `userMessage` + `recoveryAction`; technical stack only in logs. - **TypeScript Strict:** Zero `any`, zero `console.log` (use NestJS `Logger`). - **i18n:** No hardcoded Thai/English strings in components — use i18n keys (see `05-08-i18n-guidelines.md`). @@ -53,15 +54,21 @@ ## 📁 Key Files for Generating / Validating Artifacts -| When you need... | Read | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| A new feature spec | `.agents/skills/speckit-specify/templates/spec-template.md` + `specs/01-Requirements/01-06-edge-cases-and-rules.md` | -| A plan | `.agents/skills/speckit-plan/templates/plan-template.md` + relevant ADRs | -| Task breakdown | `.agents/skills/speckit-tasks/templates/tasks-template.md` + existing patterns in `specs/08-Tasks/` | -| Acceptance criteria / UAT | `specs/01-Requirements/01-05-acceptance-criteria.md` | -| Schema / table definition | `specs/03-Data-and-Storage/lcbp3-v1.9.0-schema-02-tables.sql` + `03-01-data-dictionary.md` | -| RBAC / permissions | `specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` + `01-02-01-rbac-matrix.md` | -| Release / hotfix | `specs/04-Infrastructure-OPS/04-08-release-management-policy.md` | +| When you need... | Read | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| A new feature spec | `.agents/skills/speckit-specify/templates/spec-template.md` + `specs/01-Requirements/01-06-edge-cases-and-rules.md` | +| A plan | `.agents/skills/speckit-plan/templates/plan-template.md` + relevant ADRs | +| Task breakdown | `.agents/skills/speckit-tasks/templates/tasks-template.md` + existing patterns in `specs/08-Tasks/` | +| Acceptance criteria / UAT | `specs/01-Requirements/01-05-acceptance-criteria.md` | +| Schema / table definition | `specs/03-Data-and-Storage/lcbp3-v1.9.0-schema-02-tables.sql` + `03-01-data-dictionary.md` | +| RBAC / permissions | `specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` + `01-02-01-rbac-matrix.md` | +| Release / hotfix | `specs/04-Infrastructure-OPS/04-08-release-management-policy.md` | +| ADR-024 Intent Class. | `specs/06-Decision-Records/ADR-024-intent-classification-strategy.md` | +| ADR-025 AI Tool Layer | `specs/06-Decision-Records/ADR-025-ai-tool-layer-architecture.md` | +| ADR-026 Chat UI | `specs/06-Decision-Records/ADR-026-document-chat-ui-pattern.md` | +| ADR-027 AI Admin Console | `specs/06-Decision-Records/ADR-027-ai-admin-console-and-dynamic-control.md` | +| ADR-028 Migration Refactor | `specs/06-Decision-Records/ADR-028-migration-architecture-refactor.md` | +| ADR-029 Dynamic Prompts | `specs/06-Decision-Records/ADR-029-dynamic-prompt-management.md` | --- @@ -83,7 +90,7 @@ - [ ] Business comments in Thai, code identifiers in English - [ ] Schema changes via SQL directly (not migration) - [ ] Test coverage meets targets (Backend 70%+, Business Logic 80%+) -- [ ] Relevant ADRs referenced (007/008/009/016/019/021/023/023A for AI work) +- [ ] Relevant ADRs referenced (007/008/009/016/019/021/023/023A/024-029 for AI work) - [ ] Domain glossary terms used correctly - [ ] Error handling: `Logger` + `HttpException` / `BusinessException` - [ ] i18n keys used (no hardcode text) diff --git a/.windsurf/skills/_LCBP3-CONTEXT.md b/.windsurf/skills/_LCBP3-CONTEXT.md index 5394ef8a..36b03caa 100644 --- a/.windsurf/skills/_LCBP3-CONTEXT.md +++ b/.windsurf/skills/_LCBP3-CONTEXT.md @@ -5,14 +5,14 @@ **Project:** NAP-DMS (LCBP3) — Laem Chabang Port Phase 3 Document Management System **Stack:** NestJS 11 + Next.js 16 + TypeScript + MariaDB 11.8 + Redis + BullMQ + Elasticsearch + Ollama (on-prem AI) -**Version:** 1.8.9 (2026-04-18) +**Version:** 1.9.7 (2026-05-25) --- ## 📌 Canonical Rule Sources (read in this order) 1. **`AGENTS.md`** (repo root) — primary rule file for AI agents; supersedes legacy `GEMINI.md`. -2. **`specs/06-Decision-Records/`** — architectural decisions (22 ADRs); ADR priority > Engineering Guidelines. +2. **`specs/06-Decision-Records/`** — architectural decisions (29 ADRs); ADR priority > Engineering Guidelines. 3. **`specs/05-Engineering-Guidelines/`** — backend/frontend/testing/i18n/git patterns. 4. **`specs/00-Overview/00-02-glossary.md`** — domain terminology (Correspondence / RFA / Transmittal / Circulation). 5. **`specs/00-Overview/00-03-product-vision.md`** — project constitution (Vision, Strategic Pillars, Guardrails). @@ -29,6 +29,7 @@ - **ADR-002 Document Numbering:** Redis Redlock + TypeORM `@VersionColumn` (double-lock). Never use application-side counter alone. - **ADR-008 Notifications:** BullMQ queue — never inline email/notification in a request thread. - **ADR-023/023A AI Boundary:** Ollama on Admin Desktop only; AI → DMS API → DB (never direct DB/storage). 2-model stack: `gemma4:e4b Q8_0` + `nomic-embed-text`. BullMQ `ai-realtime` / `ai-batch` queues. Human-in-the-loop validation required. (ADR-018 superseded by ADR-023) +- **ADR-029 Dynamic Prompt Management:** Prompt templates in DB (`ai_prompts`), never hardcoded in processor; Redis cache `ai:prompt:active:{type}` TTL 60s; `activate()` runs in DB transaction + Redis DEL after commit; `system.manage_all` guard on all mutations. - **ADR-007 Error Handling:** Layered (Validation / Business / System); `BusinessException` hierarchy; user-friendly `userMessage` + `recoveryAction`; technical stack only in logs. - **TypeScript Strict:** Zero `any`, zero `console.log` (use NestJS `Logger`). - **i18n:** No hardcoded Thai/English strings in components — use i18n keys (see `05-08-i18n-guidelines.md`). @@ -53,15 +54,21 @@ ## 📁 Key Files for Generating / Validating Artifacts -| When you need... | Read | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| A new feature spec | `.agents/skills/speckit-specify/templates/spec-template.md` + `specs/01-Requirements/01-06-edge-cases-and-rules.md` | -| A plan | `.agents/skills/speckit-plan/templates/plan-template.md` + relevant ADRs | -| Task breakdown | `.agents/skills/speckit-tasks/templates/tasks-template.md` + existing patterns in `specs/08-Tasks/` | -| Acceptance criteria / UAT | `specs/01-Requirements/01-05-acceptance-criteria.md` | -| Schema / table definition | `specs/03-Data-and-Storage/lcbp3-v1.9.0-schema-02-tables.sql` + `03-01-data-dictionary.md` | -| RBAC / permissions | `specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` + `01-02-01-rbac-matrix.md` | -| Release / hotfix | `specs/04-Infrastructure-OPS/04-08-release-management-policy.md` | +| When you need... | Read | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| A new feature spec | `.agents/skills/speckit-specify/templates/spec-template.md` + `specs/01-Requirements/01-06-edge-cases-and-rules.md` | +| A plan | `.agents/skills/speckit-plan/templates/plan-template.md` + relevant ADRs | +| Task breakdown | `.agents/skills/speckit-tasks/templates/tasks-template.md` + existing patterns in `specs/08-Tasks/` | +| Acceptance criteria / UAT | `specs/01-Requirements/01-05-acceptance-criteria.md` | +| Schema / table definition | `specs/03-Data-and-Storage/lcbp3-v1.9.0-schema-02-tables.sql` + `03-01-data-dictionary.md` | +| RBAC / permissions | `specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` + `01-02-01-rbac-matrix.md` | +| Release / hotfix | `specs/04-Infrastructure-OPS/04-08-release-management-policy.md` | +| ADR-024 Intent Class. | `specs/06-Decision-Records/ADR-024-intent-classification-strategy.md` | +| ADR-025 AI Tool Layer | `specs/06-Decision-Records/ADR-025-ai-tool-layer-architecture.md` | +| ADR-026 Chat UI | `specs/06-Decision-Records/ADR-026-document-chat-ui-pattern.md` | +| ADR-027 AI Admin Console | `specs/06-Decision-Records/ADR-027-ai-admin-console-and-dynamic-control.md` | +| ADR-028 Migration Refactor | `specs/06-Decision-Records/ADR-028-migration-architecture-refactor.md` | +| ADR-029 Dynamic Prompts | `specs/06-Decision-Records/ADR-029-dynamic-prompt-management.md` | --- @@ -83,7 +90,7 @@ - [ ] Business comments in Thai, code identifiers in English - [ ] Schema changes via SQL directly (not migration) - [ ] Test coverage meets targets (Backend 70%+, Business Logic 80%+) -- [ ] Relevant ADRs referenced (007/008/009/016/019/021/023/023A for AI work) +- [ ] Relevant ADRs referenced (007/008/009/016/019/021/023/023A/024-029 for AI work) - [ ] Domain glossary terms used correctly - [ ] Error handling: `Logger` + `HttpException` / `BusinessException` - [ ] i18n keys used (no hardcode text) diff --git a/CONTEXT.md b/CONTEXT.md index 852373c7..aeae44da 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -88,6 +88,18 @@ _Avoid_: Semantic search (overloaded), Vector search (incomplete) Container สำเร็จรูป (opaque black box) เปิด HTTP API ให้ NestJS เรียก — ไม่มีโค้ด Python ใน repo, ทีมไม่ maintain runtime ภายใน _Avoid_: Python sidecar, OCR microservice (ที่เรา maintain เอง) +**Prompt Version**: +Immutable snapshot ของ prompt template ใน `ai_prompts` table — ทุกครั้งที่ admin กด "บันทึก" จะสร้าง version ใหม่ (version*number เพิ่มทีละ 1) version เก่ายังอยู่ใน history ลบได้ยกเว้น active version (ADR-029) +\_Avoid*: Prompt config, Prompt setting, Editable prompt + +**Active Prompt**: +Prompt Version ที่มี `is_active = 1` ต่อ `prompt_type` — ใช้โดยทั้ง OCR Sandbox และ `processMigrateDocument` พร้อมกัน, cached ใน Redis TTL 60s; invalidated เมื่อ admin activate version อื่น (ADR-029) +_Avoid_: Production prompt (ทั้ง sandbox และ migrate-document ใช้อันเดียวกัน) + +**Prompt Template**: +String ที่มี `{{ocr_text}}` placeholder บังคับ — backend validate ก่อน save; processor แทนที่ด้วย OCR output ก่อนส่งเข้า Ollama (ADR-029) +_Avoid_: Prompt string, Prompt text (ambiguous — อาจหมายถึง resolved prompt ที่มี OCR text แล้ว) + **Human-in-the-loop**: ทุก AI suggestion ต้องผ่านการ accept/reject โดย user ก่อนกลายเป็น state change — บันทึกใน `ai_audit_logs` _Avoid_: Auto-apply, AI auto-execute @@ -113,6 +125,7 @@ _Avoid_: Throw exception from tool, Untyped error - A **Correspondence** has a 1:1 specialization to **RFA** / **Transmittal** / etc. (table inheritance) - A **RFA** has 1:N **RFA Revisions**, each linking to one or more **Shop Drawing Revisions** via `rfa_items` - A **Workflow Instance** governs exactly one **Correspondence**; its current state is projected into entity columns (e.g. `rfa_revisions.rfa_status_code_id`) but **`workflow_instances` is the source of truth** +- A **Prompt Version** lives in `ai_prompts`; exactly one per `prompt_type` has `is_active = 1` — this is the **Active Prompt** consumed by both OCR Sandbox and `processMigrateDocument`; cached in Redis TTL 60s - A **Document Chunk** (MariaDB) has a 1:1 **Vector Point** in Qdrant via shared `chunk_public_id` (UUIDv7) - An **AI Document Assistant** suggestion produces an `ai_audit_logs` row; if user accepts, it triggers a normal **Workflow Transition** (AI never writes the transition itself) - **Qdrant queries MUST be filtered by `project_public_id`** — enforced at compile time by `QdrantService` signature @@ -198,16 +211,17 @@ User Chat → Intent Router (ยังไม่มี) → Server-side Intent ## System readiness summary (resolved) -| Component | สถานะ | หมายเหตุ | -| -------------------- | --------------- | ----------------------------------------------------------------------------- | -| **Infrastructure** | ✅ พร้อม | NestJS + Next.js + MariaDB + Redis + Elasticsearch | -| **Workflow Engine** | ✅ พร้อม | DSL-based, ADR-001/021 | -| **AI Boundary** | ✅ พร้อม | ADR-023A — Ollama isolation, no direct DB access | -| **RAG Pipeline** | 🟡 บางส่วน | Qdrant service มีใน code, ต้องตรวจสอบ deployment | -| **Intent Router** | 🟡 ADR Accepted | ADR-024 Accepted — Intent Classifier (Pattern→LLM Fallback) รอ implementation | -| **AI Tool Layer** | 🟡 ADR Accepted | ADR-025 Accepted — Tool Layer Architecture รอ implementation | -| **Document Chat UI** | 🟡 ADR Accepted | ADR-026 Accepted — Side-panel Chat UI รอ implementation | -| **AI Admin Console** | 🟡 ADR Accepted | ADR-027 Accepted — Dynamic Control Panel รอ implementation | +| Component | สถานะ | หมายเหตุ | +| ----------------------- | --------------- | ----------------------------------------------------------------------------------------- | +| **Infrastructure** | ✅ พร้อม | NestJS + Next.js + MariaDB + Redis + Elasticsearch | +| **Workflow Engine** | ✅ พร้อม | DSL-based, ADR-001/021 | +| **AI Boundary** | ✅ พร้อม | ADR-023A — Ollama isolation, no direct DB access | +| **RAG Pipeline** | 🟡 บางส่วน | Qdrant service มีใน code, ต้องตรวจสอบ deployment | +| **Intent Router** | 🟡 ADR Accepted | ADR-024 Accepted — Intent Classifier (Pattern→LLM Fallback) รอ implementation | +| **AI Tool Layer** | 🟡 ADR Accepted | ADR-025 Accepted — Tool Layer Architecture รอ implementation | +| **Document Chat UI** | 🟡 ADR Accepted | ADR-026 Accepted — Side-panel Chat UI รอ implementation | +| **AI Admin Console** | 🟡 ADR Accepted | ADR-027 Accepted — Dynamic Control Panel รอ implementation | +| **Dynamic Prompt Mgmt** | 🟡 Spec Ready | ADR-029 Active — speckit.prepare เสร็จแล้ว (spec/plan/tasks/contracts); รอ implementation | ## Flagged ambiguities @@ -289,12 +303,13 @@ User Chat → Intent Router (ยังไม่มี) → Server-side Intent ## ADRs ที่เกี่ยวข้องกับ AI Runtime Layer -| ADR | หัวข้อ | ตัดสินใจอะไร | สถานะ | -| ------- | ---------------------------------- | ------------------------------------------------- | ----------- | -| ADR-024 | Intent Classification Strategy | Hybrid: Pattern First → LLM Fallback | ✅ Accepted | -| ADR-025 | AI Tool Layer Architecture | Bridge pattern, CASL enforcement, response shape | ✅ Accepted | -| ADR-026 | Document Chat UI Pattern | Side-panel vs modal vs separate page | ✅ Accepted | -| ADR-027 | AI Admin Console & Dynamic Control | Admin Panel + dynamic model/prompt/intent control | ✅ Accepted | -| ADR-028 | Migration Architecture Refactor | Staging Queue & post-migration cleanup | ✅ Active | +| ADR | หัวข้อ | ตัดสินใจอะไร | สถานะ | +| ------- | ---------------------------------- | ---------------------------------------------------------------------------------------- | ----------- | +| ADR-024 | Intent Classification Strategy | Hybrid: Pattern First → LLM Fallback | ✅ Accepted | +| ADR-025 | AI Tool Layer Architecture | Bridge pattern, CASL enforcement, response shape | ✅ Accepted | +| ADR-026 | Document Chat UI Pattern | Side-panel vs modal vs separate page | ✅ Accepted | +| ADR-027 | AI Admin Console & Dynamic Control | Admin Panel + dynamic model/prompt/intent control | ✅ Accepted | +| ADR-028 | Migration Architecture Refactor | Staging Queue & post-migration cleanup | ✅ Active | +| ADR-029 | Dynamic Prompt Management | `ai_prompts` table, versioned OCR extraction prompt shared by sandbox + migrate-document | ✅ Active | **หมายเหตุ**: ADR-023A ยังคงเป็น canonical สำหรับ infrastructure — ADR-024/025/026/027 เพิ่ม runtime layer; ADR-028 ปรับ Migration Pipeline diff --git a/memory/agent-memory.md b/memory/agent-memory.md index 2c591828..1a8a883f 100644 --- a/memory/agent-memory.md +++ b/memory/agent-memory.md @@ -7,6 +7,7 @@ - 2026-05-25 (Session 4): Normalize migration error logging ตาม AGENTS.md — แก้ n8n `Log Error to CSV`/`Log Error to DB`, harden backend `logError()`, เพิ่ม `job_id` ใน migration_errors SQL/delta, และเพิ่ม regression test. - 2026-05-25 (Session 5): N8N Workflow Debug — แก้ Submit AI Job (jsonBody serialization + RBAC permission gap) และเพิ่ม checksum-based dedup ใน FileStorageService.upload(). - 2026-05-25 (Session 6): AI Model Management (ADR-027) — เพิ่มระบบเลือกโมเดล AI แบบไดนามิกผ่าน AI Admin Console: สร้าง `ai_available_models` table + entity, extend `AiSettingsService` ด้วย methods CRUD โมเดล, add REST endpoints, update frontend UI ด้วย Select dropdown และ model list management, update `OllamaService` ใช้ DB-configured model แทน ENV เท่านั้น. +- 2026-05-25 (Session 7): PaddleOCR Sidecar setup บน Desk-5439 — สร้าง FastAPI sidecar (port 8765) รองรับ `/ocr` + `/normalize`, แก้ AggregateError ใน ocr.service.ts, เพิ่ม path remapping (`OCR_SIDECAR_UPLOAD_BASE`), CIFS volume mount จาก QNAP. --> # 🧠 Agent Long-term Project Memory @@ -181,17 +182,18 @@ docker compose ps # Check status ## 🌐 7. Environment & Services -| Service | Local URL / Port | Production | Notes | -| ---------------- | --------------------------- | ------------------------- | ------------------------------------ | -| **Backend API** | `http://localhost:3001` | QNAP `192.168.10.8` | NestJS — `/api` prefix | -| **Frontend** | `http://localhost:3000` | QNAP `192.168.10.8` | Next.js | -| **MariaDB** | `localhost:3307` | QNAP internal | DB: `lcbp3`, root via docker | -| **Redis** | `localhost:6379` | QNAP internal | BullMQ + session store | -| **n8n** | `http://localhost:5678` | QNAP `192.168.10.8:5678` | Migration orchestrator only | -| **Ollama** | `http://192.168.10.X:11434` | Admin Desktop (Desk-5439) | gemma4:e4b Q8_0 + nomic-embed-text | -| **Qdrant** | `http://localhost:6333` | Admin Desktop (Desk-5439) | Vector DB — requires projectPublicId | -| **Gitea** | `https://git.np-dms.work` | QNAP `192.168.10.8` | Source + CI/CD | -| **Gitea Runner** | ASUSTOR `192.168.10.9` | — | CI runner | +| Service | Local URL / Port | Production | Notes | +| ---------------- | ----------------------------- | ------------------------- | ------------------------------------ | +| **Backend API** | `http://localhost:3001` | QNAP `192.168.10.8` | NestJS — `/api` prefix | +| **Frontend** | `http://localhost:3000` | QNAP `192.168.10.8` | Next.js | +| **MariaDB** | `localhost:3307` | QNAP internal | DB: `lcbp3`, root via docker | +| **Redis** | `localhost:6379` | QNAP internal | BullMQ + session store | +| **n8n** | `http://localhost:5678` | QNAP `192.168.10.8:5678` | Migration orchestrator only | +| **Ollama** | `http://192.168.10.100:11434` | Admin Desktop (Desk-5439) | gemma4:e2b + nomic-embed-text | +| **Qdrant** | `http://localhost:6333` | Admin Desktop (Desk-5439) | Vector DB — requires projectPublicId | +| **PaddleOCR** | `http://192.168.10.100:8765` | Admin Desktop (Desk-5439) | `/ocr` + `/normalize` (FastAPI) | +| **Gitea** | `https://git.np-dms.work` | QNAP `192.168.10.8` | Source + CI/CD | +| **Gitea Runner** | ASUSTOR `192.168.10.9` | — | CI runner | ### Key Environment Variables (ตรวจสอบใน `docker-compose.yml`) @@ -207,13 +209,14 @@ QDRANT_URL ## 🚀 8. Recent Rollouts -| วันที่ | Version | รายการ | สถานะ | -| ---------- | ------- | --------------------------------------------------------------------------------------------- | --------------------------- | -| 2026-05-23 | v1.9.6 | Specs reorganization (`100/200/300-*` folders), AGENTS.md v1.9.6 update | ✅ Complete | -| 2026-05-23 | v1.9.6 | N8N Workflow v2 (`n8n.workflow.v2.json`) — ADR-023A compliant, ลบ Ollama direct | ⏳ Pending import to n8n UI | -| 2026-05-24 | v1.9.6 | AGENTS.md Project Memory Override rule (Windsurf / Antigravity / Codex) | ✅ Complete | -| 2026-05-25 | v1.9.6 | Migration Queue attachment UUID fix — DTO + Service + n8n.workflow.v2.json (Session 3) | ✅ Complete (tsc verified) | -| 2026-05-25 | v1.9.6 | Migration error normalization + `job_id` logging — workflow + backend + SQL/delta (Session 4) | ✅ Complete | +| วันที่ | Version | รายการ | สถานะ | +| ---------- | ------- | ---------------------------------------------------------------------------------------------------- | --------------------------- | +| 2026-05-23 | v1.9.6 | Specs reorganization (`100/200/300-*` folders), AGENTS.md v1.9.6 update | ✅ Complete | +| 2026-05-23 | v1.9.6 | N8N Workflow v2 (`n8n.workflow.v2.json`) — ADR-023A compliant, ลบ Ollama direct | ⏳ Pending import to n8n UI | +| 2026-05-24 | v1.9.6 | AGENTS.md Project Memory Override rule (Windsurf / Antigravity / Codex) | ✅ Complete | +| 2026-05-25 | v1.9.6 | Migration Queue attachment UUID fix — DTO + Service + n8n.workflow.v2.json (Session 3) | ✅ Complete (tsc verified) | +| 2026-05-25 | v1.9.6 | Migration error normalization + `job_id` logging — workflow + backend + SQL/delta (Session 4) | ✅ Complete | +| 2026-05-25 | v1.9.6 | PaddleOCR Sidecar บน Desk-5439 — FastAPI `/ocr`+`/normalize`, CIFS mount, path remapping (Session 7) | ✅ Running | --- @@ -347,7 +350,7 @@ unsupported value -> UNKNOWN --- -### Session 5 — 2026-05-25 (N8N Submit AI Job Debug + Upload Dedup) ← **ล่าสุด** +### Session 5 — 2026-05-25 (N8N Submit AI Job Debug + Upload Dedup) #### ปัญหาที่พบ (Root Cause) @@ -390,6 +393,60 @@ unsupported value -> UNKNOWN --- +### Session 7 — 2026-05-25 (PaddleOCR Sidecar Setup) ← **ล่าสุด** + +#### สิ่งที่ทำ + +- แก้ `AggregateError` (empty message) ใน `ocr.service.ts` — wrap เป็น Error พร้อม context ที่ชัดเจน +- สร้าง PaddleOCR + PyThaiNLP FastAPI sidecar รันบน Desk-5439 (Windows 10/11, Docker Desktop WSL2) +- เพิ่ม path remapping `remapPath()` ใน `OcrService` — แปลง `/app/uploads/...` → `/mnt/uploads/...` + +#### ไฟล์ที่สร้าง/แก้ไข + +| ไฟล์ | การเปลี่ยนแปลง | +| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/Dockerfile` | ✅ สร้างใหม่ — Python 3.10 slim, ลบ pre-download step (segfault) | +| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` | ✅ สร้างใหม่ — FastAPI: `/health`, `/ocr` (PaddleOCR), `/normalize` (PyThaiNLP) | +| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/requirements.txt` | ✅ สร้างใหม่ — `numpy<2.0` ต้องอยู่ก่อน paddlepaddle (ABI fix) | +| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/docker-compose.yml` | ✅ สร้างใหม่ — CIFS volume mount + named volume สำหรับ model cache | +| `specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml` | เพิ่ม `OCR_API_URL`, `OCR_CHAR_THRESHOLD`, `OCR_SIDECAR_UPLOAD_BASE` | +| `specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/.env.example` | เพิ่ม `OCR_API_URL`, `OCR_CHAR_THRESHOLD`, `OCR_SIDECAR_UPLOAD_BASE` | +| `backend/src/modules/ai/services/ocr.service.ts` | เพิ่ม `remapPath()`, AggregateError fix | + +#### Known Issues / Fixes + +- `numpy<2.0` ต้องอยู่ก่อน `paddlepaddle` — ABI mismatch กับ cv2 (numpy 2.x) +- Docker Desktop WSL2 ไม่รองรับ bind mount จาก network drive (Z:\) → ใช้ CIFS volume แทน +- Pre-download model ใน Dockerfile ทำให้ segfault (exit 139) → ลบออก download ตอน runtime +- `OLLAMA_RAG_MODEL` → เปลี่ยนเป็น `OLLAMA_MODEL_MAIN=gemma4:e2b` ตาม ADR-023A + +#### CIFS Volume Config + +```yaml +volumes: + qnap_uploads: + driver: local + driver_opts: + type: cifs + o: 'username=${QNAP_USER},password=${QNAP_PASS},vers=3.0,uid=0,gid=0' + device: '//192.168.10.8/np-dms-as/data/uploads' +``` + +#### Path Remapping + +``` +backend: /app/uploads/temp/xxx.pdf → sidecar: /mnt/uploads/temp/xxx.pdf +OCR_SIDECAR_UPLOAD_BASE=/mnt/uploads (env var) +``` + +#### Verification + +- `curl http://localhost:8765/health` → `{"status":"ok","engine":"paddleocr"}` ✅ +- `POST /ocr` ทดสอบกับไฟล์จริงใน `/mnt/uploads/temp/` → ได้ text กลับ ✅ +- 78 test suites, 672 tests ผ่านทั้งหมด ✅ + +--- + ## 🎯 10. แผนงานขั้นต่อไป (Next Session Focus) ### N8N Migration (งานหลักที่เหลือ) diff --git a/specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.rollback.sql b/specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.rollback.sql new file mode 100644 index 00000000..5284b9fc --- /dev/null +++ b/specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.rollback.sql @@ -0,0 +1,4 @@ +-- Rollback: ลบตาราง ai_prompts (ADR-029) +-- Date: 2026-05-25 + +DROP TABLE IF EXISTS ai_prompts; diff --git a/specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql b/specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql new file mode 100644 index 00000000..349ffa6e --- /dev/null +++ b/specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql @@ -0,0 +1,73 @@ +-- Delta: สร้างตาราง ai_prompts สำหรับ Dynamic Prompt Management +-- Date: 2026-05-25 +-- Related ADR: ADR-029 +-- Applied in: v1.9.0 -> v1.9.6 +-- ------------------------------------------------------------ +-- การเปลี่ยนแปลงโครงสร้างฐานข้อมูล (Schema changes) +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS ai_prompts ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ภายใน (ไม่ expose ใน API)', + prompt_type VARCHAR(50) NOT NULL COMMENT 'ประเภท prompt เช่น ocr_extraction', + version_number INT NOT NULL COMMENT 'เลข version ต่อเนื่องต่อ prompt_type (1, 2, 3...)', + template TEXT NOT NULL COMMENT 'prompt template ที่มี {{ocr_text}} placeholder บังคับ', + field_schema JSON NULL COMMENT 'definition ของ fields ที่คาดหวังในผลลัพธ์ JSON', + is_active TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = version นี้ใช้งานจริงทั้ง sandbox และ migrate-document (1 per prompt_type)', + test_result_json JSON NULL COMMENT 'ผลลัพธ์ JSON จาก sandbox run ล่าสุด (auto-save โดย processor)', + manual_note TEXT NULL COMMENT 'หมายเหตุ/annotation จาก admin (manual input)', + last_tested_at TIMESTAMP NULL COMMENT 'เวลาที่ sandbox รันครั้งล่าสุดสำหรับ version นี้', + activated_at TIMESTAMP NULL COMMENT 'เวลาที่ version นี้ถูก activate เป็น active', + created_by INT NOT NULL COMMENT 'user_id ของผู้สร้าง version นี้', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_type_version (prompt_type, version_number), + INDEX idx_prompt_type_active (prompt_type, is_active), + FOREIGN KEY (created_by) REFERENCES users(user_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตาราง versioned prompt templates สำหรับ OCR extraction (ADR-029)'; + +-- ------------------------------------------------------------ +-- Seed: migrate hardcoded prompt เป็น active version 1 +-- (8 fields รวม category, tags, summary — ใช้ร่วมกันทั้ง sandbox และ migrate-document) +-- ------------------------------------------------------------ +INSERT INTO ai_prompts ( + prompt_type, + version_number, + template, + field_schema, + is_active, + manual_note, + activated_at, + created_by + ) +VALUES ( + 'ocr_extraction', + 1, + 'You are a professional document intelligence engine.\nAnalyze the following OCR text extracted from a project document and extract the metadata fields.\n\nOCR TEXT:\n{{ocr_text}}\n\nExtract these fields:\n1. documentNumber: The official document number or code. If not found, return null.\n2. subject: The main subject, title, or topic of the document. If not found, return null.\n3. discipline: Must be exactly one of: "Civil", "Mechanical", "Electrical", "Architectural", or null if not specified.\n4. category: Must be exactly one of: "Correspondence", "Transmittal", "Circulation", "RFA", "Shop Drawing", "Contract Drawing", or null if not specified.\n5. date: The issue/document date in YYYY-MM-DD format. If not found, return null.\n6. confidence: A float between 0.0 and 1.0 indicating your confidence in this extraction.\n7. tags: An array of tags/keywords (strings) that describe the document.\n8. summary: A short 1-2 sentence summary of the document contents.\nReturn ONLY a valid JSON object matching this schema. Do NOT include markdown code blocks, HTML, or any conversational text. Example:\n{\n "documentNumber": "LCBP3-CIV-001",\n "subject": "Foundation Inspection Report",\n "discipline": "Civil",\n "category": "Correspondence",\n "date": "2026-05-20",\n "confidence": 0.95,\n "tags": ["foundation", "inspection", "concrete"],\n "summary": "This document is a foundation inspection report for the LCBP3 project, confirming concrete strength."\n}', + JSON_OBJECT( + 'documentNumber', + 'string|null', + 'subject', + 'string|null', + 'discipline', + 'enum:Civil,Mechanical,Electrical,Architectural|null', + 'category', + 'enum:Correspondence,Transmittal,Circulation,RFA,Shop Drawing,Contract Drawing|null', + 'date', + 'date:YYYY-MM-DD|null', + 'confidence', + 'float:0-1', + 'tags', + 'string[]', + 'summary', + 'string|null' + ), + 1, + 'Migrated from hardcoded prompt in processSandboxExtract / processMigrateDocument (ADR-029)', + CURRENT_TIMESTAMP, + ( + SELECT user_id + FROM users + WHERE username = 'superadmin' + LIMIT 1 + ) -- PREREQUISITE: user seed (user.seed.ts) MUST run before this delta; + -- 'superadmin' is always the first user inserted per standard deployment order + ) ON DUPLICATE KEY +UPDATE prompt_type = prompt_type; diff --git a/specs/06-Decision-Records/ADR-029-dynamic-prompt-management.md b/specs/06-Decision-Records/ADR-029-dynamic-prompt-management.md new file mode 100644 index 00000000..6d1b8d1a --- /dev/null +++ b/specs/06-Decision-Records/ADR-029-dynamic-prompt-management.md @@ -0,0 +1,222 @@ +# ADR-029: Dynamic Prompt Management for OCR Extraction + +**Status:** Accepted +**Date:** 2026-05-25 +**Decision Makers:** Development Team, System Architect +**Related Documents:** +- [ADR-027: AI Admin Console and Dynamic Control](./ADR-027-ai-admin-console-and-dynamic-control.md) +- [ADR-023A: Unified AI Architecture — Model Revision](./ADR-023A-unified-ai-architecture.md) +- [ADR-009: Database Migration Strategy](./ADR-009-database-migration-strategy.md) +- [ADR-019: Hybrid Identifier Strategy](./ADR-019-hybrid-identifier-strategy.md) + +--- + +## บริบทและปัญหา (Context and Problem Statement) + +`processSandboxExtract` และ `processMigrateDocument` ใน `ai-batch.processor.ts` ต่างมี prompt template แบบ **hardcoded** ที่ไม่สามารถแก้ไขได้โดยไม่ต้อง redeploy: + +- `processSandboxExtract` สกัด 5 fields (documentNumber, subject, discipline, date, confidence) +- `processMigrateDocument` สกัด 8 fields (+ category, tags, summary) +- ทั้งสองใช้ prompt ที่ต่างกัน ทำให้ sandbox ไม่ simulate พฤติกรรมจริงได้แม่นยำ + +ปัญหาเพิ่มเติมที่พบ: +1. **Timeout Bug**: `AI_TIMEOUT_MS = 30000ms` (30 วินาที) สั้นเกินไปสำหรับ Ollama ใน OCR Sandbox — การรันครั้งที่สองมักล้มเหลวเพราะ model ต้องโหลดใหม่เข้า VRAM +2. **ไม่มี version history**: ไม่สามารถ rollback กลับไป prompt เดิมได้ + +--- + +## ปัจจัยขับเคลื่อนการตัดสินใจ (Decision Drivers) + +- **Admin Control**: Superadmin ต้องแก้ไข prompt ได้ runtime ผ่าน AI Admin Console +- **Consistency**: Sandbox และ migrate-document ต้องใช้ prompt เดียวกัน ผลลัพธ์ sandbox จึงสะท้อนพฤติกรรมจริง +- **Auditability**: ต้องมี version history เพื่อ compare ผลลัพธ์ระหว่าง prompt versions +- **Safety**: ห้ามลบ active version, ต้อง validate placeholder `{{ocr_text}}` ก่อน save + +--- + +## ทางเลือกที่ถูกพิจารณา (Considered Options) + +### Option 1: เก็บ prompt ใน `system_settings` (Generic Key-Value) +- **ข้อดี:** ไม่ต้องสร้างตารางใหม่ +- **ข้อเสีย:** `system_settings` ออกแบบสำหรับ "current value" เท่านั้น ไม่มี version history, ไม่มี result storage + +### Option 2: ตาราง `ai_prompts` แยกต่างหาก (ตัวเลือกที่ได้รับเลือก) +- **ข้อดี:** Versioned, immutable snapshots, รองรับ test result storage, ออกแบบตรงกับ use case +- **ข้อเสีย:** ต้องสร้าง entity/service/controller ใหม่ + +--- + +## ผลการตัดสินใจ (Decision Outcome) + +**ทางเลือกที่ได้รับเลือก:** Option 2 — ตาราง `ai_prompts` พร้อม versioning + +--- + +## ข้อตกลงหลัก (Core Decisions — Grilling Session 2026-05-25) + +| # | ประเด็น | การตัดสินใจ | +|---|---------|-------------| +| 1 | Prompt type scope | `prompt_type = 'ocr_extraction'` เดียว (8 fields) ใช้ร่วมกันทั้ง sandbox และ migrate-document | +| 2 | Activation model | Single `is_active` flag — "นำไปใช้จริง" = activate ทันทีทั้งระบบ (ทั้ง sandbox และ migrate-document) | +| 3 | Result storage | Auto-save `test_result_json` จาก sandbox run ล่าสุด + `manual_note` สำหรับ admin annotation | +| 4 | Versioning | Immutable version — ทุก "บันทึก" สร้าง version ใหม่เสมอ, สามารถลบได้ (ยกเว้น active version) | +| 5 | Template format | Full template พร้อม `{{ocr_text}}` placeholder — backend validate ก่อน save | +| 6 | Bug fix | เพิ่ม `timeoutMs` เฉพาะ sandbox-extract เป็น 120000ms แทน default 30000ms | + +--- + +## รายละเอียดเชิงสถาปัตยกรรม (Implementation Details) + +### 1. โครงสร้างตาราง `ai_prompts` + +```sql +CREATE TABLE ai_prompts ( + id INT PRIMARY KEY AUTO_INCREMENT, + prompt_type VARCHAR(50) NOT NULL + COMMENT 'ประเภท prompt เช่น ocr_extraction', + version_number INT NOT NULL + COMMENT 'เลข version ต่อเนื่องต่อ prompt_type (1, 2, 3...)', + template TEXT NOT NULL + COMMENT 'prompt template ที่มี {{ocr_text}} placeholder บังคับ', + field_schema JSON NULL + COMMENT 'definition ของ fields ที่คาดหวังในผลลัพธ์ JSON', + is_active TINYINT(1) DEFAULT 0 + COMMENT '1 = version นี้ใช้งานจริงทั้ง sandbox และ migrate-document', + test_result_json JSON NULL + COMMENT 'ผลลัพธ์ JSON จาก sandbox run ล่าสุด (auto-save โดย processor)', + manual_note TEXT NULL + COMMENT 'หมายเหตุ/annotation จาก admin (manual input)', + last_tested_at TIMESTAMP NULL + COMMENT 'เวลาที่ sandbox รันครั้งล่าสุดสำหรับ version นี้', + activated_at TIMESTAMP NULL + COMMENT 'เวลาที่ version นี้ถูก activate เป็น active', + created_by INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_type_version (prompt_type, version_number), + INDEX idx_prompt_type_active (prompt_type, is_active), + FOREIGN KEY (created_by) REFERENCES users(user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='ตาราง versioned prompt templates สำหรับ OCR extraction (ADR-029)'; +``` + +**Seed data** — default active version ที่ migrate มาจาก hardcoded prompt ปัจจุบัน: +```sql +INSERT INTO ai_prompts (prompt_type, version_number, template, is_active, created_by) +VALUES ('ocr_extraction', 1, '', 1, 1); +``` + +### 2. Validation Rule (backend — ก่อน save) + +```typescript +// AiPromptsService.create() +if (!dto.template.includes('{{ocr_text}}')) { + throw new BadRequestException( + 'template ต้องมี {{ocr_text}} placeholder เพื่อระบุตำแหน่งที่จะแทรกข้อความจาก OCR' + ); +} +``` + +### 3. Prompt Resolution ใน Processor + +```typescript +// ใช้ใน processSandboxExtract และ processMigrateDocument +private async resolvePrompt(ocrText: string): Promise { + const activePrompt = await this.aiPromptsService.getActive('ocr_extraction'); + if (!activePrompt) { + throw new Error('ไม่พบ active prompt สำหรับ ocr_extraction'); + } + return activePrompt.template.replace('{{ocr_text}}', ocrText); +} +``` + +- ทั้ง `processSandboxExtract` และ `processMigrateDocument` เรียก `resolvePrompt()` เดียวกัน +- `processSandboxExtract` auto-save ผล JSON ลงใน `active_prompt.test_result_json` + update `last_tested_at` + +### 4. Ollama Timeout Fix + +```typescript +// processSandboxExtract — ส่ง timeoutMs เฉพาะเพื่อแก้ bug timeout ครั้งที่ 2 +const response = await this.ollamaService.generate(prompt, { + timeoutMs: 120000, // 2 นาที แทน default 30 วินาที +}); +``` + +**Root cause ของ bug:** `AI_TIMEOUT_MS = 30000ms` — Ollama unload model จาก VRAM หลังจาก idle ระยะหนึ่ง (default keep_alive = 5 นาที แต่ VRAM pressure อาจเร็วกว่า) การรันครั้งที่สองต้องโหลด model ใหม่ ซึ่งใช้เวลา > 30 วินาที + +### 5. API Endpoints ใน `ai.controller.ts` + +| Method | Path | Action | Guard | +|--------|------|--------|-------| +| `GET` | `/ai/prompts/:type` | ดึง all versions ของ prompt_type (paginated) | `system.manage_all` | +| `POST` | `/ai/prompts/:type` | สร้าง version ใหม่ (validate `{{ocr_text}}`) | `system.manage_all` | +| `DELETE` | `/ai/prompts/:type/:version` | ลบ version (guard: ห้ามลบ active) | `system.manage_all` | +| `POST` | `/ai/prompts/:type/:version/activate` | Activate version ("นำไปใช้จริง") | `system.manage_all` | +| `PATCH` | `/ai/prompts/:type/:version/note` | บันทึก manual_note | `system.manage_all` | + +### 6. UI/UX ใน OCR Sandbox Tab + +Layout ใหม่ของ OCR Sandbox tab: +``` +┌─────────────────────────────────────────────────────┐ +│ OCR Sandbox Playground │ +├──────────────────────┬──────────────────────────────┤ +│ Prompt Editor │ Version History │ +│ ┌────────────────┐ │ ┌────────────────────────┐ │ +│ │ textarea │ │ │ v3 (active) ✅ │ │ +│ │ {{ocr_text}} │ │ │ v2 - 2026-05-24 │ │ +│ │ ... │ │ │ v1 - 2026-05-22 │ │ +│ └────────────────┘ │ └────────────────────────┘ │ +│ [บันทึก Version ใหม่]│ [Load] [Activate] [Delete] │ +├──────────────────────┴──────────────────────────────┤ +│ File Upload: [เลือก PDF] │ +│ [เริ่มทำ OCR Sandbox] │ +├─────────────────────────────────────────────────────┤ +│ ผลลัพธ์ JSON + [บันทึก Manual Note] │ +└─────────────────────────────────────────────────────┘ +``` + +**Flow:** +1. เปิด tab → โหลด active version เข้า textarea อัตโนมัติ +2. Admin แก้ไข prompt → กด **"บันทึก Version ใหม่"** → สร้าง version ใหม่ (inactive) +3. Admin upload PDF → กด **"เริ่มทำ OCR Sandbox"** → รันด้วย active version +4. ผลลัพธ์ auto-save ลง active version's `test_result_json` +5. Admin ตรวจสอบผล → กด **"นำไปใช้จริง"** บน version ที่ต้องการ → activate + +--- + +## ผลกระทบ (Consequences) + +### ผลดี +- Admin ปรับ prompt ได้ real-time ไม่ต้อง redeploy +- Sandbox สะท้อนพฤติกรรม migrate-document ได้แม่นยำ (8 fields เหมือนกัน) +- Version history เปรียบเทียบผลลัพธ์ระหว่าง prompt versions ได้ +- Bug timeout ได้รับการแก้ไข + +### ผลเสีย / ข้อระวัง +- ถ้าไม่มี active prompt → processor throw error → ต้องมี seed data พร้อมก่อน deploy +- `processMigrateDocument` อาจเปลี่ยน prompt กลางชุด batch ถ้า admin activate ระหว่างที่ batch กำลังรัน — **acceptable tradeoff** เนื่องจาก batch มักสั้น และ admin ควร activate เมื่อไม่มี batch running +- เพิ่ม DB query ต่อ job (query active prompt) — **mitigate** ด้วย Redis cache TTL 60s สำหรับ active prompt + +--- + +## Redis Cache Strategy สำหรับ Active Prompt + +``` +Key: ai:prompt:active:ocr_extraction +TTL: 60 วินาที +Invalidate: หลัง activate สำเร็จ (AiPromptsService.activate()) +``` + +--- + +## Grilling Session Log + +``` +2026-05-25 — grilling session ผ่าน Windsurf Cascade +Q1: prompt_type scope → 'ocr_extraction' เดียว (8 fields) ร่วมกันทั้งคู่ +Q2: activation model → Option A (single is_active flag) +Q3: result storage → Option C (auto-save + manual_note) +Q4: versioning → Option A (immutable, every save = new version, deletable) +Q5: template format → Option A ({{ocr_text}} placeholder, validated) +Bug: AI_TIMEOUT_MS 30s too short → fix: timeoutMs: 120000 for sandbox-extract +``` diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/checklists/requirements.md b/specs/200-fullstacks/229-dynamic-prompt-management/checklists/requirements.md new file mode 100644 index 00000000..373260e4 --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/checklists/requirements.md @@ -0,0 +1,37 @@ +# Specification Quality Checklist: Dynamic Prompt Management for OCR Extraction + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-25 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded (prompt_type = 'ocr_extraction' only) +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows (manage versions, sandbox test, runtime resolution) +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Spec is derived from ADR-029 grilling session (2026-05-25) — high confidence, minimal ambiguity +- `field_schema` column: assumed system-managed metadata (not user-editable) in v1 — documented in Assumptions +- Timeout fix scope (sandbox only) documented in Assumptions to prevent scope creep +- Seed data requirement (FR-011) explicitly stated to prevent deploy-time failure diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/contracts/prompts.yaml b/specs/200-fullstacks/229-dynamic-prompt-management/contracts/prompts.yaml new file mode 100644 index 00000000..eb96dccb --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/contracts/prompts.yaml @@ -0,0 +1,278 @@ +openapi: "3.0.3" +info: + title: AI Prompts Management API + description: ADR-029 — Versioned prompt management for OCR extraction + version: "1.0.0" + +# Base path: /api/ai/prompts (mounted under AiController) + +paths: + /ai/prompts/{promptType}: + get: + operationId: listPromptVersions + summary: ดึง Prompt Versions ทั้งหมดของ prompt_type นั้น + description: Returns all versions sorted by version_number DESC. No pagination (v1). Guarded by system.manage_all. + security: + - BearerAuth: [] + parameters: + - name: promptType + in: path + required: true + schema: + type: string + example: ocr_extraction + responses: + "200": + description: List of prompt versions + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/AiPromptResponse" + "403": + $ref: "#/components/responses/Forbidden" + + post: + operationId: createPromptVersion + summary: สร้าง Prompt Version ใหม่ (inactive) + description: Validates {{ocr_text}} placeholder. Assigns next version_number automatically. Logs to audit_logs. + security: + - BearerAuth: [] + parameters: + - name: promptType + in: path + required: true + schema: + type: string + example: ocr_extraction + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateAiPromptRequest" + responses: + "201": + description: Created prompt version + content: + application/json: + schema: + type: object + properties: + data: + $ref: "#/components/schemas/AiPromptResponse" + "400": + description: Template missing {{ocr_text}} placeholder + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + message: "template ต้องมี {{ocr_text}} placeholder" + userMessage: "กรุณาเพิ่ม {{ocr_text}} ใน template ก่อนบันทึก" + "403": + $ref: "#/components/responses/Forbidden" + + /ai/prompts/{promptType}/{versionNumber}: + delete: + operationId: deletePromptVersion + summary: ลบ Prompt Version (ห้ามลบ active version) + description: Guards against deleting the active version. Logs to audit_logs. + security: + - BearerAuth: [] + parameters: + - name: promptType + in: path + required: true + schema: + type: string + - name: versionNumber + in: path + required: true + schema: + type: integer + minimum: 1 + responses: + "204": + description: Deleted successfully + "400": + description: Cannot delete active version + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + message: "ไม่สามารถลบ active version ได้" + userMessage: "กรุณา activate version อื่นก่อน แล้วจึงลบ version นี้" + "404": + $ref: "#/components/responses/NotFound" + "403": + $ref: "#/components/responses/Forbidden" + + /ai/prompts/{promptType}/{versionNumber}/activate: + post: + operationId: activatePromptVersion + summary: Activate Prompt Version — นำไปใช้จริงทั้ง sandbox และ migrate-document + description: Runs in transaction — deactivates current active, activates this version, invalidates Redis cache. Logs to audit_logs. + security: + - BearerAuth: [] + parameters: + - name: promptType + in: path + required: true + schema: + type: string + - name: versionNumber + in: path + required: true + schema: + type: integer + responses: + "200": + description: Activated successfully + content: + application/json: + schema: + type: object + properties: + data: + $ref: "#/components/schemas/AiPromptResponse" + "404": + $ref: "#/components/responses/NotFound" + "403": + $ref: "#/components/responses/Forbidden" + + /ai/prompts/{promptType}/{versionNumber}/note: + patch: + operationId: updatePromptNote + summary: บันทึก Manual Note สำหรับ Prompt Version + description: Updates manual_note field only. Does not create new version. No audit log required. + security: + - BearerAuth: [] + parameters: + - name: promptType + in: path + required: true + schema: + type: string + - name: versionNumber + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdatePromptNoteRequest" + responses: + "200": + description: Note updated + content: + application/json: + schema: + type: object + properties: + data: + $ref: "#/components/schemas/AiPromptResponse" + "404": + $ref: "#/components/responses/NotFound" + "403": + $ref: "#/components/responses/Forbidden" + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + AiPromptResponse: + type: object + properties: + promptType: + type: string + example: ocr_extraction + versionNumber: + type: integer + example: 3 + template: + type: string + description: Full prompt template with {{ocr_text}} placeholder + isActive: + type: boolean + example: true + testResultJson: + type: object + nullable: true + description: Last sandbox run result (8 fields) + manualNote: + type: string + nullable: true + lastTestedAt: + type: string + format: date-time + nullable: true + activatedAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + required: + - promptType + - versionNumber + - template + - isActive + - createdAt + + CreateAiPromptRequest: + type: object + properties: + template: + type: string + description: "Must contain {{ocr_text}} placeholder" + example: "Extract fields from the following text:\n{{ocr_text}}\nReturn JSON." + required: + - template + + UpdatePromptNoteRequest: + type: object + properties: + manualNote: + type: string + nullable: true + maxLength: 2000 + required: + - manualNote + + ErrorResponse: + type: object + properties: + message: + type: string + userMessage: + type: string + recoveryAction: + type: string + + responses: + Forbidden: + description: "Forbidden — requires system.manage_all permission" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + NotFound: + description: "Prompt version not found" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/data-model.md b/specs/200-fullstacks/229-dynamic-prompt-management/data-model.md new file mode 100644 index 00000000..190aba0c --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/data-model.md @@ -0,0 +1,210 @@ +# Data Model: Dynamic Prompt Management for OCR Extraction + +**Feature**: `229-dynamic-prompt-management` +**Date**: 2026-05-25 + +--- + +## Entity: AiPrompt (`ai_prompts`) + +### SQL Schema (delta file) + +```sql +-- File: specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql +-- ADR-029: Dynamic Prompt Management for OCR Extraction + +CREATE TABLE ai_prompts ( + id INT PRIMARY KEY AUTO_INCREMENT + COMMENT 'Internal INT PK — never exposed in API (ADR-019)', + prompt_type VARCHAR(50) NOT NULL + COMMENT 'ประเภท prompt เช่น ocr_extraction — ใช้เป็น public identifier', + version_number INT NOT NULL + COMMENT 'เลข version ต่อเนื่องต่อ prompt_type (1, 2, 3...) — monotonically increasing, ไม่ fill gaps', + template TEXT NOT NULL + COMMENT 'prompt template ที่มี {{ocr_text}} placeholder บังคับ — validated ก่อน save', + field_schema JSON NULL + COMMENT 'definition ของ fields ที่คาดหวังในผลลัพธ์ JSON (system-managed, ไม่ user-editable ใน v1)', + is_active TINYINT(1) DEFAULT 0 + COMMENT '1 = version นี้ใช้งานจริงทั้ง sandbox และ migrate-document; exactly 1 active ต่อ prompt_type', + test_result_json JSON NULL + COMMENT 'ผลลัพธ์ JSON จาก OCR sandbox run ล่าสุด (auto-save โดย processSandboxExtract)', + manual_note TEXT NULL + COMMENT 'หมายเหตุ/annotation จาก admin (PATCH endpoint)', + last_tested_at TIMESTAMP NULL + COMMENT 'เวลาที่ sandbox รันสำเร็จครั้งล่าสุดสำหรับ version นี้', + activated_at TIMESTAMP NULL + COMMENT 'เวลาที่ version นี้ถูก activate เป็น active — NULL ถ้ายังไม่เคย activate', + created_by INT NOT NULL + COMMENT 'FK → users.user_id — ผู้สร้าง version นี้', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_type_version (prompt_type, version_number) + COMMENT 'ป้องกัน race condition — version_number ต้อง unique ต่อ prompt_type', + INDEX idx_prompt_type_active (prompt_type, is_active) + COMMENT 'ใช้สำหรับ query active prompt (Redis cache miss path)', + CONSTRAINT fk_ai_prompts_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='ADR-029: Versioned prompt templates สำหรับ OCR extraction — ใช้ร่วมกันโดย sandbox และ migrate-document'; + +-- Seed data: default active version จาก hardcoded prompt ปัจจุบัน +-- NOTE: template text ต้องแทนที่ด้วย exact hardcoded prompt จาก ai-batch.processor.ts ก่อน deploy +INSERT INTO ai_prompts (prompt_type, version_number, template, field_schema, is_active, created_by) +VALUES ( + 'ocr_extraction', + 1, + 'You are a document metadata extraction assistant. Extract the following fields from the OCR text below and return a valid JSON object.\n\nFields to extract:\n- documentNumber: document number or reference code\n- subject: document title or subject\n- discipline: engineering discipline (e.g., Civil, Mechanical, Electrical)\n- date: document date (ISO 8601 format if possible)\n- confidence: your confidence score 0.0-1.0\n- category: document category\n- tags: array of relevant tags\n- summary: brief document summary (max 200 chars)\n\nReturn ONLY valid JSON. No explanation text.\n\nOCR Text:\n{{ocr_text}}', + JSON_OBJECT( + -- key = ชื่อ field ใน JSON output ที่ LLM ควร return (ไม่ใช่ column name ของ ai_prompts table) + -- value = type constraint ที่ processor ใช้ validate/document + 'documentNumber', 'string|null', + 'subject', 'string|null', + 'discipline', 'enum:Civil,Mechanical,Electrical,Architectural|null', + 'category', 'enum:Correspondence,Transmittal,Circulation,RFA,Shop Drawing,Contract Drawing|null', + 'date', 'date:YYYY-MM-DD|null', + 'confidence', 'float:0-1', + 'tags', 'string[]', + 'summary', 'string|null' + ), + 1, + 1 +); +``` + +### TypeORM Entity + +```typescript +// File: backend/src/modules/ai/prompts/ai-prompts.entity.ts +// ADR-029: Entity สำหรับ ai_prompts table + +@Entity('ai_prompts') +export class AiPrompt { + @PrimaryGeneratedColumn() + @Exclude() // ADR-019: INT PK ไม่ expose ใน API + id: number; + + @Column({ name: 'prompt_type', length: 50 }) + promptType: string; + + @Column({ name: 'version_number' }) + versionNumber: number; + + @Column({ type: 'text' }) + template: string; + + @Column({ name: 'field_schema', type: 'json', nullable: true }) + fieldSchema: Record | null; + + @Column({ name: 'is_active', type: 'tinyint', width: 1, default: 0 }) + isActive: boolean; + + @Column({ name: 'test_result_json', type: 'json', nullable: true }) + testResultJson: Record | null; + + @Column({ name: 'manual_note', type: 'text', nullable: true }) + manualNote: string | null; + + @Column({ name: 'last_tested_at', type: 'timestamp', nullable: true }) + lastTestedAt: Date | null; + + @Column({ name: 'activated_at', type: 'timestamp', nullable: true }) + activatedAt: Date | null; + + @Column({ name: 'created_by' }) + @Exclude() // FK ไม่ expose โดยตรง + createdBy: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +--- + +## State Transitions + +``` +[CREATE] → is_active = 0 (inactive) + │ + ▼ +[ACTIVATE] → is_active = 1 (active) ── replaces previous active version + │ + ▼ (when another version is activated) +[DEACTIVATE] → is_active = 0 (inactive) + │ + ▼ (only if not active) +[DELETE] → row removed from DB +``` + +**Invariant**: Exactly 1 row with `is_active = 1` per `prompt_type` at all times (enforced by transaction in `AiPromptsService.activate()`) + +--- + +## Redis Cache + +| Key | Value | TTL | Invalidated by | +|-----|-------|-----|---------------| +| `ai:prompt:active:ocr_extraction` | Serialized `AiPrompt` (JSON) | 60s | `AiPromptsService.activate()` — `DEL key` after transaction commit | + +**Fallback**: If Redis unavailable (`ioredis` connection error), `AiPromptsService.getActive()` queries DB directly with `LOG.warn('Redis unavailable, falling back to DB query')` — no throw. + +--- + +## API Response Shape + +```typescript +// AiPromptResponseDto — สำหรับ expose ใน API response +interface AiPromptResponse { + promptType: string; // 'ocr_extraction' + versionNumber: number; // 1, 2, 3... + template: string; // full template text + isActive: boolean; + testResultJson: Record | null; + manualNote: string | null; + lastTestedAt: string | null; // ISO 8601 + activatedAt: string | null; // ISO 8601 + createdAt: string; // ISO 8601 +} +// NOTE: id (INT) ไม่ expose — @Exclude() per ADR-019 +// NOTE: createdBy (INT) ไม่ expose +``` + +--- + +## Relationships + +- `ai_prompts.created_by` → `users.user_id` (FK) +- No relationship to other AI tables (standalone) +- Consumed by: `AiBatchProcessor.processSandboxExtract()` and `AiBatchProcessor.processMigrateDocument()` + +--- + +## Pre-existing Bug (must fix in T024) + +`MigrateDocumentMetadata` interface (บรรทัด 29-37 ใน `ai-batch.processor.ts`) **ขาด `discipline?: string`** — แม้ `processMigrateDocument` prompt จะ extract `discipline` ออกมาได้ แต่ `parseMigrateDocumentMetadata()` ทิ้งค่านี้ทุกครั้งเพราะ interface ไม่รับ field นี้ + +```typescript +// ❌ ปัจจุบัน (ขาด discipline) +interface MigrateDocumentMetadata { + documentNumber?: string; + subject?: string; + category?: string; // มี category + date?: string; + confidence?: number; + tags?: string[]; + summary?: string; + // discipline หายไปเลย! +} + +// ✅ ต้องแก้เป็น (เพิ่ม discipline) +interface MigrateDocumentMetadata { + documentNumber?: string; + subject?: string; + discipline?: string; // เพิ่ม + category?: string; + date?: string; + confidence?: number; + tags?: string[]; + summary?: string; +} +``` + +**Fix**: เพิ่มการแก้ bug นี้เข้าไปใน T026 หรือ T024 เมื่อ implement — ก่อน/หลัง replace hardcoded prompt diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/plan.md b/specs/200-fullstacks/229-dynamic-prompt-management/plan.md new file mode 100644 index 00000000..f59eb8f5 --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/plan.md @@ -0,0 +1,143 @@ +# Implementation Plan: Dynamic Prompt Management for OCR Extraction + +**Branch**: `229-dynamic-prompt-management` | **Date**: 2026-05-25 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `specs/200-fullstacks/229-dynamic-prompt-management/spec.md` +**ADR Reference**: ADR-029, ADR-009, ADR-016, ADR-019, ADR-023/023A, ADR-027 + +--- + +## Summary + +เพิ่มระบบ Versioned Prompt Management สำหรับ OCR extraction — แทนที่ hardcoded prompt ใน `processSandboxExtract` และ `processMigrateDocument` ด้วย DB-driven prompt ที่ Superadmin แก้ไขได้ runtime ผ่าน AI Admin Console พร้อมแก้ bug AI_TIMEOUT_MS และ Redis cache สำหรับ active prompt + +--- + +## Technical Context + +**Language/Version**: TypeScript 5.x — NestJS 11 (backend), Next.js 16 (frontend) +**Primary Dependencies**: TypeORM (MariaDB), Redis (ioredis), BullMQ, TanStack Query v5, shadcn/ui, Zod +**Storage**: MariaDB 11.8 (`ai_prompts` table via SQL delta), Redis (TTL 60s cache) +**Testing**: Jest (backend unit/integration), Vitest (frontend unit) +**Target Platform**: QNAP NAS (backend + frontend containers), Desk-5439 (Ollama + OCR sidecar) +**Project Type**: Web application (backend + frontend) +**Performance Goals**: Cache hit < 5ms; activation cycle < 1s +**Constraints**: ADR-009 no TypeORM migrations; ADR-019 no parseInt on UUID; ADR-016 CASL guard on all mutations; AI_TIMEOUT_MS bug fix scope = sandbox only +**Scale/Scope**: Single `prompt_type = 'ocr_extraction'`; expected < 20 versions total + +--- + +## Constitution Check + +_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._ + +| Principle | Status | Notes | +|-----------|--------|-------| +| ADR-009: No TypeORM migrations | ✅ PASS | Schema change via SQL delta in `specs/03-Data-and-Storage/deltas/` | +| ADR-019: UUID — no parseInt | ✅ PASS | `ai_prompts` uses INT PK (internal only); `prompt_type` + `version_number` are public identifiers (strings + ints, not UUID) | +| ADR-016: CASL guard on mutations | ✅ PASS | All 5 endpoints guarded with `system.manage_all` | +| ADR-007: Error handling | ✅ PASS | `BusinessException` for validation errors; NestJS Logger for technical logs | +| ADR-023/023A: AI boundary | ✅ PASS | Prompt is config data — stored in DB, not in AI model; Ollama call remains via existing `OllamaService` | +| ADR-008: BullMQ for background | ✅ PASS | Sandbox run already in `ai-batch` queue; no change to queue routing | +| TypeScript Strict | ✅ PASS | Zero `any`, zero `console.log` | +| i18n | ✅ PASS | All UI text via i18n keys | + +--- + +## Project Structure + +### Documentation (this feature) + +```text +specs/200-fullstacks/229-dynamic-prompt-management/ +├── plan.md (this file) +├── research.md (Phase 0 output) +├── data-model.md (Phase 1 output) +├── quickstart.md (Phase 1 output) +├── contracts/ (Phase 1 output) +│ └── prompts.yaml +├── checklists/ +│ └── requirements.md +└── tasks.md (Phase 2 output — /speckit-tasks) +``` + +### Source Code (repository root) + +```text +backend/src/modules/ai/ +├── prompts/ [NEW MODULE] +│ ├── ai-prompts.entity.ts +│ ├── ai-prompts.service.ts +│ ├── ai-prompts.controller.ts +│ ├── ai-prompts.module.ts +│ └── dto/ +│ ├── create-ai-prompt.dto.ts +│ ├── update-prompt-note.dto.ts +│ └── ai-prompt-response.dto.ts +├── processors/ +│ └── ai-batch.processor.ts [MODIFY — add resolvePrompt(), fix timeout] +└── ai.module.ts [MODIFY — import AiPromptsModule] + +specs/03-Data-and-Storage/deltas/ +└── 2026-05-25-create-ai-prompts.sql [NEW — SQL delta per ADR-009] + +frontend/ +├── components/admin/ai/ +│ ├── OcrSandboxPromptManager.tsx [NEW — Prompt Editor + Version History] +│ └── PromptVersionHistory.tsx [NEW — Version History panel] +├── lib/services/ +│ └── ai-prompts.service.ts [NEW — API client for prompts] +├── hooks/ +│ └── use-ai-prompts.ts [NEW — TanStack Query hooks] +├── types/ +│ └── ai-prompts.ts [NEW — TypeScript interfaces] +└── public/locales/{en,th}/ + └── ai-admin.json [MODIFY — add prompt management i18n keys] +``` + +**Structure Decision**: Web application (Option 2) — NestJS backend + Next.js frontend, standard LCBP3 monorepo pattern + +--- + +## Phases + +### Phase 0: Research (complete — findings below) + +All unknowns resolved from ADR-029 + existing codebase patterns. + +### Phase 1: Design & Contracts (this document + artifacts) + +1. SQL delta for `ai_prompts` table — see `data-model.md` +2. API contract — see `contracts/prompts.yaml` +3. Seed data strategy — insert hardcoded prompt as version 1 in delta +4. Redis cache key strategy — `ai:prompt:active:{prompt_type}` TTL 60s + +### Phase 2: Implementation + +Follow tasks.md phases. Implementation entry point: see `quickstart.md` + +--- + +## Key Design Decisions + +### D1: AiPromptsService as Standalone Module +`AiPromptsService` lives in `ai/prompts/` sub-module and is imported into `AiModule`. This keeps version management logic separate from the batch processor while sharing the Redis connection. + +### D2: resolvePrompt() Placement +`resolvePrompt(promptType, ocrText)` is a private method inside `AiBatchProcessor` (or extracted to `AiPromptsService.resolveActive()`). It must be accessible from both `processSandboxExtract` and `processMigrateDocument` — placing it in `AiPromptsService` is cleaner (injectable service vs private method). + +### D3: Timeout Fix Scope +`timeoutMs: 120000` passed only to `processSandboxExtract` Ollama call. `processMigrateDocument` retains its existing job-level timeout (controlled by BullMQ job options), which is already longer. + +### D4: Activation Transaction +`activate()` runs in a TypeORM transaction: +1. `UPDATE ai_prompts SET is_active = 0 WHERE prompt_type = ? AND is_active = 1` +2. `UPDATE ai_prompts SET is_active = 1, activated_at = NOW() WHERE id = ?` +3. **After** COMMIT (outside TX): Redis `DEL ai:prompt:active:ocr_extraction` + +**Redis DEL failure behavior**: If Redis DEL fails after DB commit, do nothing — log `WARN` and let TTL 60s expire naturally. Stale-on-Redis-fail is in the same category as normal TTL expiry: max 60s window, acceptable per ADR-029 design intent. No retry, no error surfaced to admin (DB state is already correct). + +### D5: Seed Data Strategy +Seed data inserted in the SQL delta file itself (not separate seed script) so it runs automatically with the schema change. Initial hardcoded prompt content extracted from `ai-batch.processor.ts` before migration. + +### D6: Version Number Assignment +On create: `SELECT MAX(version_number) + 1 FROM ai_prompts WHERE prompt_type = ?` within a transaction. Uses `@VersionColumn` or DB-level unique constraint to prevent race conditions. diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/quickstart.md b/specs/200-fullstacks/229-dynamic-prompt-management/quickstart.md new file mode 100644 index 00000000..ac3ed468 --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/quickstart.md @@ -0,0 +1,148 @@ +# Quickstart: Dynamic Prompt Management for OCR Extraction + +**Feature**: `229-dynamic-prompt-management` +**Date**: 2026-05-25 + +--- + +## Pre-requisites + +- [ ] Backend running: `pnpm --filter backend dev` (port 3001) +- [ ] Frontend running: `pnpm --filter frontend dev` (port 3000) +- [ ] MariaDB running with SQL delta applied +- [ ] Redis running (for cache testing) +- [ ] Logged in as Superadmin (user with `system.manage_all` permission) + +--- + +> **Prerequisites**: User seed (`pnpm --filter backend seed` or `ts-node run-seed.ts`) MUST run before applying this delta. The seed INSERT for `ai_prompts` references `users.username = 'superadmin'` — if the user table is empty, the INSERT will fail with a FK constraint error. + +## Step 1: Apply SQL Delta + +```sql +-- Run in MariaDB: +SOURCE specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql; + +-- Verify: +SELECT id, prompt_type, version_number, is_active, LEFT(template, 50) AS template_preview +FROM ai_prompts; +-- Expected: 1 row, version_number=1, is_active=1 +``` + +--- + +## Step 2: Verify API Endpoint (List Versions) + +```bash +# List all versions for ocr_extraction: +curl -X GET http://localhost:3001/api/ai/prompts/ocr_extraction \ + -H "Authorization: Bearer " + +# Expected: { data: [{ promptType: 'ocr_extraction', versionNumber: 1, isActive: true, ... }] } +``` + +--- + +## Step 3: Create a New Prompt Version + +```bash +curl -X POST http://localhost:3001/api/ai/prompts/ocr_extraction \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "template": "Improved prompt for OCR extraction:\n{{ocr_text}}\nReturn JSON with documentNumber, subject, discipline, date, confidence, category, tags, summary." + }' + +# Expected: { data: { versionNumber: 2, isActive: false, ... } } +``` + +--- + +## Step 4: Activate New Version + +```bash +curl -X POST http://localhost:3001/api/ai/prompts/ocr_extraction/2/activate \ + -H "Authorization: Bearer " + +# Expected: { data: { versionNumber: 2, isActive: true, activatedAt: "...", ... } } + +# Verify old version is now inactive: +curl -X GET http://localhost:3001/api/ai/prompts/ocr_extraction \ + -H "Authorization: Bearer " +# Expected: v1 isActive=false, v2 isActive=true +``` + +--- + +## Step 5: Test Validation (Missing Placeholder) + +```bash +curl -X POST http://localhost:3001/api/ai/prompts/ocr_extraction \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ "template": "This prompt has no placeholder" }' + +# Expected: 400 Bad Request +# { message: "template ต้องมี {{ocr_text}} placeholder", userMessage: "กรุณาเพิ่ม {{ocr_text}} ใน template ก่อนบันทึก" } +``` + +--- + +## Step 6: Verify Redis Cache + +```bash +# After GET or activate, check Redis: +redis-cli GET "ai:prompt:active:ocr_extraction" +# Expected: JSON string of active prompt + +# After activate: +redis-cli TTL "ai:prompt:active:ocr_extraction" +# Expected: number between 1-60 (seconds remaining) +``` + +--- + +## Step 7: UI Verification + +> **Sandbox polling mechanism**: "เริ่มทำ OCR Sandbox" enqueues a BullMQ job to the `ai-batch` queue via the existing sandbox trigger endpoint. The frontend polls the existing job-status endpoint (`GET /api/ai/jobs/:jobId/status`) until the job completes or fails — this is the **existing pattern**, no new polling endpoint is introduced by this feature. + +1. Navigate to `/admin` → AI Admin Console → OCR Sandbox tab +2. Verify: Prompt Editor shows active version template in textarea +3. Verify: Version History panel shows v1 (inactive), v2 (active ✅) +4. Click "Load" on v1 → template should appear in textarea (v2 still active) +5. Upload a PDF → Click "เริ่มทำ OCR Sandbox" → wait for result (up to 120s — cold start allowed) +6. Verify: Result JSON appears with 8 fields; `test_result_json` and `last_tested_at` updated on v2 + +--- + +## Step 8: Delete Inactive Version + +```bash +# Try to delete active version (should fail): +curl -X DELETE http://localhost:3001/api/ai/prompts/ocr_extraction/2 \ + -H "Authorization: Bearer " +# Expected: 400 Bad Request + +# Delete inactive version (should succeed): +curl -X DELETE http://localhost:3001/api/ai/prompts/ocr_extraction/1 \ + -H "Authorization: Bearer " +# Expected: 204 No Content +``` + +--- + +## Acceptance Checklist + +- [ ] SQL delta applied; seed data present (v1, is_active=1) +- [ ] GET returns all versions for prompt_type +- [ ] POST validates {{ocr_text}} placeholder +- [ ] POST creates new inactive version with auto-incremented version_number +- [ ] Activate deactivates old + activates new + invalidates Redis cache (single transaction) +- [ ] Cannot delete active version +- [ ] Can delete inactive version +- [ ] PATCH saves manual_note +- [ ] processSandboxExtract uses 120s timeout (no timeout on cold start) +- [ ] processMigrateDocument uses same resolvePrompt() — no hardcoded prompt +- [ ] UI: Prompt Editor + Version History rendered correctly +- [ ] UI: OCR Sandbox run shows result + auto-saves test_result_json +- [ ] audit_logs records: create, activate, delete events diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/research.md b/specs/200-fullstacks/229-dynamic-prompt-management/research.md new file mode 100644 index 00000000..1983afd2 --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/research.md @@ -0,0 +1,75 @@ +# Research: Dynamic Prompt Management for OCR Extraction + +**Feature**: `229-dynamic-prompt-management` +**Date**: 2026-05-25 + +--- + +## R1: Hardcoded Prompt Location + +**Decision**: Extract hardcoded prompt from `ai-batch.processor.ts` (both `processSandboxExtract` and `processMigrateDocument`) before removing it +**Rationale**: This exact text becomes the seed data for `ai_prompts` version 1 — must preserve exact wording +**Action Required**: Before creating the delta, read the current hardcoded prompt from the processor to capture the exact template + +--- + +## R2: Redis Cache Strategy for Active Prompt + +**Decision**: Cache key `ai:prompt:active:{prompt_type}`, TTL 60s, invalidate on `activate()` with `RedisClient.del()` +**Rationale**: Active prompt changes infrequently (only on admin action). 60s TTL means max 60s delay for processors to pick up new prompt — acceptable per ADR-029. If Redis unavailable, fall back to DB query. +**Alternatives considered**: +- TTL 5min (same as intent patterns, ADR-024): Rejected — prompt activation should propagate faster than intent patterns +- No cache (always DB): Rejected — every BullMQ job would hit DB; ai-batch can process many jobs concurrently + +--- + +## R3: Version Number Race Condition Prevention + +**Decision**: Use `SELECT MAX(version_number) + 1 FROM ai_prompts WHERE prompt_type = ? FOR UPDATE` within a DB transaction; UNIQUE KEY `uk_type_version` on `(prompt_type, version_number)` provides final guard +**Rationale**: Concurrent create requests could generate the same version number. `FOR UPDATE` row lock + unique constraint prevents this cleanly without Redis Redlock (not needed for admin-only low-frequency operations) +**Alternatives considered**: +- Redis Redlock (ADR-002): Recommended for document numbering — overkill for admin-only prompt versioning; admin operations are inherently low-concurrency + +--- + +## R4: AiPromptsService.resolveActive() vs Private Method in Processor + +**Decision**: Implement `resolveActive(promptType: string): Promise` in `AiPromptsService` and inject service into processor +**Rationale**: Both `processSandboxExtract` and `processMigrateDocument` call the same method — centralizing in service enables unit testing independently of processor; processor depends on service (correct direction) +**Alternatives considered**: +- Private method in processor: Cannot be unit tested independently; duplicated if multiple processors need it in the future + +--- + +## R5: Activation Transaction Isolation + +**Decision**: Use TypeORM `EntityManager.transaction()` for activate() — deactivate old + activate new + log to audit_logs in single transaction +**Rationale**: Prevents state where two versions are active simultaneously (even briefly). audit_logs insert inside transaction ensures no audit record without state change. +**Pattern**: Follows existing `WorkflowEngineService` transaction patterns in codebase + +--- + +## R6: OcrSandboxPromptManager Component Architecture + +**Decision**: Single component `OcrSandboxPromptManager` with two panels — left: `PromptEditor` (textarea + save button), right: `PromptVersionHistory` (list + Load/Activate/Delete actions). File upload + sandbox run at bottom. +**Rationale**: Matches ADR-029 UI mockup. Two-column layout on desktop (md:grid-cols-2), stacked on mobile. `useAiPrompts` TanStack Query hook provides version list with optimistic updates on activate. + +--- + +## R7: Existing Patterns to Reuse + +| Pattern | Source | Reuse | +|---------|--------|-------| +| CASL guard decorator | `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` | All 5 endpoints | +| Audit decorator | `@Audit(...)` from audit-log module | activate, create, delete | +| Redis inject | `@InjectRedis()` from `@liaoliaots/nestjs-redis` | AiPromptsService | +| TanStack Query | `useMutation` + `useQuery` | `useAiPrompts` hook | +| BusinessException | `backend/src/common/exceptions/` | Validation errors | + +--- + +## R8: SQL Delta Filename Convention + +**Decision**: `2026-05-25-create-ai-prompts.sql` in `specs/03-Data-and-Storage/deltas/` +**Rationale**: Follows existing delta naming pattern (e.g., `2026-05-22-alter-migration-review-queue.sql`) +**Action**: Include both `CREATE TABLE` and `INSERT` seed data in same file diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/spec.md b/specs/200-fullstacks/229-dynamic-prompt-management/spec.md new file mode 100644 index 00000000..39c5be43 --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/spec.md @@ -0,0 +1,142 @@ +# Feature Specification: Dynamic Prompt Management for OCR Extraction + +**Feature Branch**: `229-dynamic-prompt-management` +**Created**: 2026-05-25 +**Status**: Draft +**ADR Reference**: [ADR-029](../../06-Decision-Records/ADR-029-dynamic-prompt-management.md) +**Input**: Dynamic, runtime-editable OCR extraction prompts shared by OCR Sandbox and Migration processor — replaces hardcoded prompts, adds versioning, timeout bug fix, Redis caching. + +--- + +## User Scenarios & Testing _(mandatory)_ + +### User Story 1 - Prompt Version Management (Priority: P1) + +Superadmin สามารถดูรายการ Prompt Versions ทั้งหมด, สร้าง Prompt Version ใหม่, activate version ที่ต้องการให้ระบบใช้จริง และลบ version ที่ไม่ต้องการ (ยกเว้น active version) ผ่าน AI Admin Console + +**Why this priority**: นี่คือรากฐานของ feature ทั้งหมด — หากไม่มีโครงสร้างการจัดการ Prompt Version, ระบบยังคงใช้ prompt แบบ hardcoded และไม่สามารถแก้ไข runtime ได้ + +**Independent Test**: เปิด AI Admin Console → OCR Sandbox tab → จัดการ Prompt Versions ได้โดยไม่ต้อง upload PDF ใดๆ + +**Acceptance Scenarios**: + +1. **Given** มี Prompt Versions ใน DB, **When** superadmin เปิด OCR Sandbox tab, **Then** เห็น Version History panel ทางขวามือพร้อม active version ที่มีเครื่องหมาย ✅ +2. **Given** superadmin กรอก Prompt Template ที่มี `{{ocr_text}}` placeholder, **When** กด "บันทึก Version ใหม่", **Then** สร้าง version ใหม่ (inactive) และปรากฏใน Version History +3. **Given** superadmin กรอก Prompt Template ที่ไม่มี `{{ocr_text}}`, **When** กด "บันทึก Version ใหม่", **Then** ระบบแสดง error "template ต้องมี {{ocr_text}} placeholder" และไม่บันทึก +4. **Given** มี inactive version, **When** superadmin กด "Activate", **Then** version นั้นกลายเป็น active, version เดิมกลายเป็น inactive, Redis cache ถูก invalidate ทันที +5. **Given** มี active version, **When** superadmin พยายามกด Delete บน active version, **Then** ระบบปฏิเสธพร้อมข้อความ "ไม่สามารถลบ active version ได้" +6. **Given** มี inactive version, **When** superadmin กด Delete, **Then** version ถูกลบออกจาก DB และหายจาก Version History +7. **Given** superadmin กด Load บน version ใดๆ, **When** version โหลด, **Then** template ของ version นั้นปรากฏใน Prompt Editor textarea (ไม่ activate อัตโนมัติ) + +--- + +### User Story 2 - OCR Sandbox Testing with Prompt Evaluation (Priority: P2) + +Superadmin สามารถ upload PDF และทดสอบ OCR + LLM extraction ด้วย Active Prompt ปัจจุบัน เพื่อประเมินคุณภาพผลลัพธ์ก่อนใช้งานจริงกับ Migration batch โดยผลลัพธ์ auto-save ลง active version และรองรับ Manual Note annotation + +**Why this priority**: Sandbox testing ช่วยให้ admin ตรวจสอบและเปรียบเทียบ prompt ก่อน activate จริง — ลดความเสี่ยงที่ prompt ไม่ดีจะกระทบ Migration batch + +**Independent Test**: upload PDF → กด "เริ่มทำ OCR Sandbox" → เห็นผล JSON ครบ 8 fields และผลลัพธ์ auto-save ลง active version + +**Acceptance Scenarios**: + +1. **Given** มี Active Prompt, **When** superadmin upload PDF และกด "เริ่มทำ OCR Sandbox", **Then** ระบบรัน OCR + LLM extraction ด้วย active prompt และแสดงผล JSON ที่มี 8 fields (documentNumber, subject, discipline, date, confidence, category, tags, summary) +2. **Given** sandbox run เสร็จสิ้น, **When** ผลลัพธ์ JSON ออกมา, **Then** ผลลัพธ์ auto-save ลงใน active version's `test_result_json` และ update `last_tested_at` อัตโนมัติ +3. **Given** ผลลัพธ์ sandbox ปรากฏ, **When** superadmin พิมพ์ manual note และกด "บันทึก Manual Note", **Then** note ถูกบันทึกลงใน `manual_note` field ของ active version +4. **Given** OCR Sandbox ถูก trigger และ Ollama ต้องโหลด model ใหม่ (cold start), **When** ระบบรอผล, **Then** ระบบรอได้นานถึง 120 วินาที ไม่ timeout ก่อนกำหนด (แก้ bug จาก AI_TIMEOUT_MS = 30s) +5. **Given** ไม่มี Active Prompt, **When** superadmin กด "เริ่มทำ OCR Sandbox", **Then** ระบบแสดง error ว่าไม่พบ active prompt และไม่รัน sandbox + +--- + +### User Story 3 - Runtime Prompt Resolution (Priority: P3) + +ระบบ (processor layer) สามารถดึง Active Prompt จาก `ai_prompts` table ผ่าน Redis cache (TTL 60s) และใช้ใน `processSandboxExtract` กับ `processMigrateDocument` ได้โดยทั้งสองใช้ prompt เดียวกัน — ไม่มี hardcoded prompt ใน codebase + +**Why this priority**: Backend plumbing ที่ทำให้ US1 และ US2 มีผลจริงในระบบ Production — ถ้าไม่มี US3, prompt ที่ admin set ผ่าน UI ไม่มีผลต่อ processor + +**Independent Test**: activate prompt version ใหม่ → trigger sandbox job → ตรวจสอบ log ว่า job ใช้ prompt version ใหม่ + +**Acceptance Scenarios**: + +1. **Given** มี Active Prompt ใน DB, **When** processor เรียก `resolvePrompt('ocr_extraction', ocrText)`, **Then** ได้รับ resolved prompt ที่แทนที่ `{{ocr_text}}` ด้วย OCR text จริง +2. **Given** Active Prompt ถูก cache ใน Redis TTL 60s, **When** processor เรียก `resolvePrompt()` ภายใน 60s, **Then** ได้รับ prompt จาก Redis cache (ไม่ query DB ซ้ำ) +3. **Given** admin activate version ใหม่ (cache invalidated), **When** processor เรียก `resolvePrompt()` ครั้งถัดไป, **Then** ได้รับ prompt version ล่าสุดจาก DB และ cache ถูก refresh +4. **Given** ไม่มี Active Prompt ใน DB (เช่น deploy ใหม่ก่อน seed), **When** processor พยายาม `resolveActive()`, **Then** `BusinessException` throw → BullMQ mark job **failed** → `migrationService.createError()` บันทึก error → batch หยุด (fail-fast — ไม่ใช้ skip เพราะทำให้เกิด silent data gap) + +--- + +### Edge Cases + +- อะไรเกิดขึ้นถ้า admin สองคน activate พร้อมกัน? → `activate()` ต้องใช้ `SELECT ... FOR UPDATE` เพื่อ lock current active row ก่อน deactivate — serializes concurrent activations ต่อ `prompt_type`; admin คนที่สองจะ block จนกว่าคนแรก COMMIT — ไม่มี double-active +- อะไรเกิดขึ้นถ้า admin activate version ขณะที่ Migration batch กำลังรัน? → job ที่กำลัง run ใช้ prompt ที่ดึงมาแล้ว (per-job resolution); job ถัดไปจะใช้ prompt ใหม่ — acceptable tradeoff +- อะไรเกิดขึ้นถ้า Redis ล่มขณะที่ processor เรียก `resolvePrompt()`? → fallback query DB โดยตรง; ระบบยังทำงานได้ แต่ performance ลด +- อะไรเกิดขึ้นถ้า template ยาวเกิน 4,000 ตัวอักษร? → `create()` reject ด้วย `ValidationException('Template exceeds 4,000 character limit')` — ป้องกัน context window overflow ใน Ollama (FR-015) +- อะไรเกิดขึ้นถ้า PDF ที่ upload ใน sandbox ไม่มี text (scanned image)? → OCR Service รัน PaddleOCR ตาม existing flow; ไม่ใช่ scope ของ feature นี้ +- อะไรเกิดขึ้นถ้า Ollama timeout แม้จะตั้ง 120s? → sandbox job fail พร้อม error message; ไม่กระทบ Migration jobs อื่น +- อะไรเกิดขึ้นถ้า version 1 (seed data) ถูกลบก่อนที่จะมี version อื่น active? → ป้องกันได้ด้วย guard "ห้ามลบ active version" +- อะไรเกิดขึ้นถ้าผลลัพธ์ JSON จาก sandbox ไม่ครบ 8 fields? → save ทุกอย่างที่ได้ใน `test_result_json`, UI แสดงตามที่มี +- อะไรเกิดขึ้นถ้า admin ลบ v2 แล้วสร้าง version ใหม่ → version number กลายเป็น v4 ข้าม v2? → by design (monotonically increasing, ไม่ fill gaps per plan.md D6); UI MUST แสดง version numbers ตามที่เป็น ไม่ reindex — ป้องกัน admin สับสน "v2 หายไปไหน?" +- อะไรเกิดขึ้นถ้า processor crash หลัง Ollama return แต่ก่อน `saveTestResult()` รัน? → `test_result_json` ยังเป็น `NULL` (เหมือนยังไม่ทดสอบ) — acceptable; admin รัน sandbox ใหม่ได้ทันที; ไม่กระทบ Active Prompt หรือ migration batch + +--- + +## Requirements _(mandatory)_ + +### Functional Requirements + +- **FR-001**: ระบบ MUST บันทึก Prompt Template เป็น versioned records ใน `ai_prompts` table — ทุกการบันทึกสร้าง version ใหม่เสมอ (immutable; ไม่มี update template เดิม) +- **FR-002**: ระบบ MUST validate ว่า template มี `{{ocr_text}}` placeholder ก่อน save — reject พร้อม user-friendly error message ถ้าไม่มี +- **FR-003**: ระบบ MUST enforce single active version ต่อ `prompt_type` — activate version ใหม่จะ deactivate version เดิมอัตโนมัติใน transaction เดียว +- **FR-004**: ระบบ MUST ห้ามลบ active version — แสดง error ถ้าพยายามลบ active version +- **FR-005**: ระบบ MUST auto-save `test_result_json` และ update `last_tested_at` ลงใน Prompt Version ที่ **active ณ เวลาที่ job เริ่มทำงาน** (ไม่ใช่เวลาที่ result กลับมา) — กัน race condition ที่ admin activate version อื่นระหว่างที่ sandbox กำลังรันอยู่ +- **FR-006**: ระบบ MUST รองรับ `manual_note` annotation จาก admin ต่อ Prompt Version ผ่าน PATCH endpoint +- **FR-007**: ระบบ MUST invalidate Redis cache (`ai:prompt:active:ocr_extraction`) ทันทีหลัง activate สำเร็จ +- **FR-008**: `processSandboxExtract` MUST ใช้ timeout 120000ms เพื่อรองรับ Ollama cold start (แก้ bug AI_TIMEOUT_MS = 30000ms) +- **FR-009**: ทั้ง `processSandboxExtract` และ `processMigrateDocument` MUST ใช้ `resolvePrompt()` method เดียวกัน — ไม่มี hardcoded prompt ใน processor +- **FR-010**: API endpoints ทั้งหมดสำหรับ Prompt Management MUST ป้องกันด้วย `system.manage_all` CASL permission +- **FR-011**: ระบบ MUST มี seed data (Prompt Version 1 ที่ migrate จาก hardcoded prompt ปัจจุบัน พร้อม `is_active = 1`) ก่อน deploy +- **FR-012**: Redis cache MUST fallback ไป DB query ถ้า Redis ไม่พร้อมใช้งาน (graceful degradation) +- **FR-013**: Prompt activation, creation, deletion events MUST be recorded in standard `audit_logs` table (ไม่ใช่ `ai_audit_logs`) +- **FR-014**: `GET /ai/prompts/:type` MUST return all versions สำหรับ prompt_type นั้น (ไม่ paginate ใน v1) +- **FR-015**: `template` MUST NOT exceed **4,000 characters** — รักษา headroom ใน context window 8192 tokens ของ gemma4:e4b เพื่อให้ OCR text มีที่เหลือ — reject พร้อม user-friendly error ถ้าเกิน + +### Key Entities + +- **Prompt Version** (`ai_prompts`): Immutable snapshot ของ prompt template — มี `prompt_type`, `version_number`, `template`, `is_active`, `test_result_json`, `manual_note`, `last_tested_at`, `activated_at`, `created_by` +- **Active Prompt**: Prompt Version ที่ `is_active = 1` ต่อ `prompt_type` — cached ใน Redis key `ai:prompt:active:{prompt_type}` TTL 60s +- **Prompt Template**: String ที่มี `{{ocr_text}}` placeholder บังคับ — resolved เป็น final prompt โดย processor ก่อนส่งเข้า Ollama + +--- + +## Success Criteria _(mandatory)_ + +### Measurable Outcomes + +- **SC-001**: Superadmin สามารถสร้าง, activate, และลบ Prompt Version ได้ภายใน 30 วินาที ต่อการดำเนินการหนึ่งครั้ง +- **SC-002**: OCR Sandbox รันได้สำเร็จสำหรับ PDF ที่ถูก upload โดยไม่ timeout ก่อน 120 วินาที +- **SC-003**: Processor ดึง Active Prompt จาก Redis cache ภายใน 5ms ในช่วง TTL 60s (ไม่ query DB ซ้ำ) +- **SC-004**: หลัง activate Prompt Version ใหม่, jobs ถัดไปทั้งหมดใช้ prompt version ใหม่ภายใน 60 วินาที (Redis TTL expiry) +- **SC-005**: ไม่มี hardcoded prompt template ใน codebase หลังจาก feature นี้ deploy — 100% DB-driven +- **SC-006**: Version History แสดง Prompt Versions ทั้งหมดพร้อม status (active/inactive) และ last_tested_at ได้อย่างถูกต้อง + +--- + +## Clarifications + +### Session 2026-05-25 + +- Q: Should prompt activation and sandbox run events be recorded in `ai_audit_logs` or standard `audit_logs`? → A: Standard `audit_logs` — these are admin config actions, not AI inference results; keeps `AiPromptsModule` decoupled from `AiAuditLogModule` +- Q: Should `GET /ai/prompts/:type` use pagination? → A: Return all versions (no pagination in v1) — prompt versions are expected to be low-count (single digits to low tens); simplifies UI implementation + +--- + +## Assumptions + +- Scope จำกัดที่ `prompt_type = 'ocr_extraction'` เดียว (8 fields: documentNumber, subject, discipline, date, confidence, category, tags, summary) ตาม ADR-029 core decisions +- Admin ที่ใช้ feature นี้คือ Superadmin ที่มี `system.manage_all` permission เท่านั้น +- OCR Service (PaddleOCR sidecar บน Desk-5439) ยังคงทำงานเหมือนเดิม — feature นี้เปลี่ยนเฉพาะ LLM prompt หลัง OCR +- Seed data (version 1 ที่ migrate จาก hardcoded prompt) ต้องถูก insert ก่อน first deploy ผ่าน SQL delta (ADR-009) +- Redis พร้อมใช้งาน; ถ้า Redis ล่ม ระบบ graceful degrade ไป DB query +- Existing `AiController` / `AiModule` สามารถ extend ได้โดยไม่ต้อง refactor โครงสร้างหลัก +- `field_schema JSON NULL` column ใน `ai_prompts` เป็น system-managed metadata (ไม่ user-editable ใน v1) — ระบุ expected output fields สำหรับ validation +- Timeout fix ใช้กับ `processSandboxExtract` เท่านั้น; `processMigrateDocument` ใช้ default timeout ของ BullMQ job (queue-level timeout ต่างกัน) diff --git a/specs/200-fullstacks/229-dynamic-prompt-management/tasks.md b/specs/200-fullstacks/229-dynamic-prompt-management/tasks.md new file mode 100644 index 00000000..f3d2df5d --- /dev/null +++ b/specs/200-fullstacks/229-dynamic-prompt-management/tasks.md @@ -0,0 +1,201 @@ +# Tasks: Dynamic Prompt Management for OCR Extraction + +**Input**: Design documents from `specs/200-fullstacks/229-dynamic-prompt-management/` +**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/prompts.yaml ✅, quickstart.md ✅ +**Branch**: `229-dynamic-prompt-management` + +## Format: `[ID] [P?] [Story?] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[US#]**: User Story mapping (US1 = Version Management, US2 = Sandbox Testing, US3 = Runtime Resolution) + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Schema, entity, and module scaffolding that all user stories depend on + +- [ ] T001 Read hardcoded prompt from `backend/src/modules/ai/processors/ai-batch.processor.ts` (both `processSandboxExtract` and `processMigrateDocument`) and capture exact template text for seed data +- [ ] T002 Create SQL delta `specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql` with `CREATE TABLE ai_prompts` + seed data INSERT (use exact template from T001) +- [ ] T002b [P] Create rollback delta `specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.rollback.sql` with `DROP TABLE IF EXISTS ai_prompts` — follows existing delta rollback convention +- [ ] T003 [P] Create TypeORM entity `backend/src/modules/ai/prompts/ai-prompts.entity.ts` per data-model.md (INT PK with `@Exclude()`, all columns, no publicId needed) +- [ ] T004 [P] Create `backend/src/modules/ai/prompts/dto/create-ai-prompt.dto.ts` with `template: string` (class-validator `@IsNotEmpty()`) +- [ ] T005 [P] Create `backend/src/modules/ai/prompts/dto/update-prompt-note.dto.ts` with `manualNote: string | null` +- [ ] T006 [P] Create `backend/src/modules/ai/prompts/dto/ai-prompt-response.dto.ts` with `@Expose()` fields per data-model.md API Response Shape + +**Checkpoint**: Schema applied, entity and DTOs compile — T007 can begin + +--- + +## Phase 2: Foundational (Blocking Backend Prerequisites) + +**Purpose**: Core service and module wiring — MUST complete before US1 frontend or US3 processor work + +**⚠️ CRITICAL**: All user story implementation depends on this phase + +- [ ] T007 Create `backend/src/modules/ai/prompts/ai-prompts.service.ts` with: + - `findAll(promptType: string): Promise` — ORDER BY version_number DESC + - `getActive(promptType: string): Promise` — Redis cache first, DB fallback + - `create(promptType, dto, userId): Promise` — validate `{{ocr_text}}` present (FR-002); validate `template.length <= 4000` (FR-015, reject with `ValidationException`); assign `MAX(version_number)+1 FOR UPDATE` + - `activate(promptType, versionNumber, userId): Promise` — transaction: **`SELECT id FROM ai_prompts WHERE prompt_type=? AND is_active=1 FOR UPDATE`** first (serializes concurrent activations) → deactivate old → activate new → COMMIT → Redis DEL + audit_logs + - `delete(promptType, versionNumber, userId): Promise` — guard active version + audit_logs + - `updateNote(promptType, versionNumber, note): Promise` — PATCH manual_note only + - `saveTestResult(promptType, versionNumber, resultJson): Promise` — auto-save from sandbox +- [ ] T008 Create `backend/src/modules/ai/prompts/ai-prompts.controller.ts` — 5 endpoints per contracts/prompts.yaml with `@UseGuards(JwtAuthGuard, CaslAbilityGuard)` + `@Audit()` on mutating endpoints +- [ ] T009 Create `backend/src/modules/ai/prompts/ai-prompts.module.ts` — import `TypeOrmModule.forFeature([AiPrompt])`, `RedisModule`, export `AiPromptsService` +- [ ] T010 Register `AiPromptsModule` in `backend/src/modules/ai/ai.module.ts` imports array +- [ ] T011 Add `AiPrompt` entity to `backend/src/database/` or TypeORM config entities array so it is picked up by the DB connection + +**Checkpoint**: `pnpm --filter backend build` passes; GET /api/ai/prompts/ocr_extraction returns seed data — US1 frontend and US3 processor can proceed in parallel + +--- + +## Phase 3: User Story 1 — Prompt Version Management UI (Priority: P1) 🎯 MVP + +**Goal**: Superadmin จัดการ Prompt Versions ได้ผ่าน AI Admin Console (ไม่ต้อง upload PDF) + +**Independent Test**: เปิด AI Admin Console → OCR Sandbox tab → เห็น Version History, สร้าง version ใหม่, activate, ลบ inactive version ได้ + +### Implementation for User Story 1 + +- [ ] T012 [P] [US1] Create `frontend/types/ai-prompts.ts` — `AiPrompt` interface พร้อม `promptType`, `versionNumber`, `template`, `isActive`, `testResultJson`, `manualNote`, `lastTestedAt`, `activatedAt`, `createdAt`; and `SandboxResult` interface พร้อม `promptVersionUsed: number`, `answer: string`, `completedAt: string` +- [ ] T013 [P] [US1] Create `frontend/lib/services/ai-prompts.service.ts` — `listVersions(promptType)`, `createVersion(promptType, template)`, `activateVersion(promptType, versionNumber)`, `deleteVersion(promptType, versionNumber)`, `updateNote(promptType, versionNumber, note)` — calls `/api/ai/prompts/...` via `apiClient` +- [ ] T014 [US1] Create `frontend/hooks/use-ai-prompts.ts` — TanStack Query `useQuery` (listVersions) + `useMutation` (create, activate, delete, updateNote) with `invalidateQueries` on success +- [ ] T015 [US1] Create `frontend/components/admin/ai/PromptVersionHistory.tsx` — Version list panel ทางขวา แสดง version_number, is_active badge (✅), last_tested_at, buttons: Load / Activate / Delete (Delete ปิดถ้า isActive) +- [ ] T016 [US1] Create `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` — 2-column layout: left (Prompt Editor textarea + "บันทึก Version ใหม่" button) + right (`PromptVersionHistory`) — โหลด active version template เข้า textarea เมื่อ mount +- [ ] T017 [US1] Wire `OcrSandboxPromptManager` into existing AI Admin Console OCR Sandbox tab (replace or extend existing component in `frontend/app/(admin)/admin/...`) +- [ ] T018 [P] [US1] Add i18n keys to `frontend/public/locales/th/ai-admin.json` and `frontend/public/locales/en/ai-admin.json` — keys: `prompt.saveVersion`, `prompt.activate`, `prompt.delete`, `prompt.load`, `prompt.activeLabel`, `prompt.placeholderError`, `prompt.deleteActiveError` + +**Checkpoint**: US1 fully functional — version list, create, activate, delete work in UI without PDF upload + +--- + +## Phase 4: User Story 2 — OCR Sandbox Testing with Prompt Evaluation (Priority: P2) + +**Goal**: Superadmin upload PDF → ทดสอบ active prompt → ผลลัพธ์ auto-save ลง active version + +**Independent Test**: upload PDF → รัน sandbox → เห็นผล 8 fields; test_result_json updated; manual note saved + +### Implementation for User Story 2 + +- [ ] T019 [US2] Extend `OcrSandboxPromptManager.tsx` — เพิ่ม File Upload section (PDF only) + "เริ่มทำ OCR Sandbox" button + ผลลัพธ์ JSON display panel + "บันทึก Manual Note" field (textarea + button) +- [ ] T020 [US2] Add `runSandbox(promptType, file)` to `frontend/lib/services/ai-prompts.service.ts` — calls existing sandbox endpoint (POST /api/ai/ocr-sandbox or existing BullMQ trigger endpoint) passing PDF file +- [ ] T021 [US2] Add `useSandboxRun` mutation to `frontend/hooks/use-ai-prompts.ts` — triggers sandbox run, polls for result, updates version list on completion (to reflect new `testResultJson`); read `promptVersionUsed` from job result and display as "ผลจาก Prompt Version X" badge in the result panel +- [ ] T022 [US2] Add i18n keys: `prompt.runSandbox`, `prompt.sandboxResult`, `prompt.saveNote`, `prompt.noActivePrompt`, `prompt.timeoutInfo`, `prompt.versionUsed` to both locale files + +**Checkpoint**: US2 fully functional — upload PDF, run sandbox, see results, save note + +--- + +## Phase 5: User Story 3 — Runtime Prompt Resolution in Processor (Priority: P3) + +**Goal**: `processSandboxExtract` + `processMigrateDocument` ใช้ `resolvePrompt()` จาก DB — ไม่มี hardcoded prompt + +**Independent Test**: activate version 2 → trigger sandbox job → log shows "Using prompt version 2" — no hardcoded string in processor + +### Implementation for User Story 3 + +- [ ] T023 [US3] Inject `AiPromptsService` into `backend/src/modules/ai/processors/ai-batch.processor.ts` constructor +- [ ] T024 [US3] Add `resolveActive(promptType: string, ocrText: string): Promise<{ resolvedPrompt: string; versionNumber: number }>` method to `AiPromptsService` — calls `getActive(promptType)`, replaces `{{ocr_text}}` with `ocrText`; if no active prompt: throw `BusinessException('No active prompt for type: ocr_extraction')` → caller (processor) lets it propagate → BullMQ marks job failed (fail-fast; do NOT catch-and-skip as that creates silent data gaps); returns both resolved string and versionNumber on success (FR-005) +- [ ] T025 [US3] Replace hardcoded prompt string in `processSandboxExtract` with `const { resolvedPrompt, versionNumber } = await this.aiPromptsService.resolveActive('ocr_extraction', ocrResult.text)` — pass `resolvedPrompt` to `OllamaService.generate()` with `timeoutMs: 120000` (fix AI_TIMEOUT_MS bug); carry `versionNumber` forward to T027; **add `promptVersionUsed: versionNumber` to the completed Redis result shape** so frontend can display which version produced the result +- [ ] T026 [US3] Replace hardcoded prompt string in `processMigrateDocument` with `const { resolvedPrompt } = await this.aiPromptsService.resolveActive('ocr_extraction', ocrResult.text)` — pass `resolvedPrompt` to `OllamaService.generate()`; keep existing timeout (no change); **also fix pre-existing bug: add `discipline?: string` to `MigrateDocumentMetadata` interface and add `discipline: readString(source.discipline)` to `parseMigrateDocumentMetadata()`** (see data-model.md "Pre-existing Bug"); NOTE: BullMQ job-level timeout is adequate for batch workloads — no change needed per ADR-029 D3 +- [ ] T027 [US3] In `processSandboxExtract` — `versionNumber` is captured at job start via `resolveActive()` (T025); after run completes call `this.aiPromptsService.saveTestResult('ocr_extraction', versionNumber, resultJson)` — no re-query needed (FR-005 race condition already guarded) + +**Checkpoint**: US3 complete — `pnpm --filter backend build` passes; no hardcoded prompt strings in processor; resolvePrompt() uses DB/Redis + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Seed data verification, error boundary, tests + +- [ ] T028 [P] Write unit tests for `AiPromptsService` in `backend/src/modules/ai/prompts/ai-prompts.service.spec.ts`: + - `create()`: rejects template without `{{ocr_text}}`; assigns correct version_number + - `activate()`: deactivates old version; invalidates Redis cache; **calls `AuditLogService` (or `@Audit()` decorator) — verifies audit record created** (FR-013) + - `delete()`: throws BusinessException when deleting active version; **calls AuditLogService on successful delete** (FR-013) + - `delete()`: succeeds for inactive version + - `getActive()`: returns from Redis cache when cache hit + - `getActive()`: falls back to DB query when Redis unavailable (mock Redis to throw connection error) +- [ ] T029 [P] Verify SQL delta syntax — run against a local MariaDB copy and confirm seed data is present with correct `is_active = 1` +- [ ] T030 [P] Run `pnpm --filter backend lint` and `pnpm --filter frontend lint` — fix any issues in new files +- [ ] T031 Run quickstart.md acceptance checklist end-to-end and confirm all checkboxes pass +- [ ] T032 Update `CONTEXT.md` "System readiness summary" table — change ADR-029 row status from "🟡 ADR Accepted" to "✅ พร้อม" (if applicable) + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1 (Setup)**: No dependencies — start immediately; T002b/T003/T004/T005/T006 parallel after T002 +- **Phase 2 (Foundational)**: Depends on T002 (SQL delta) + T003/T004/T005/T006; BLOCKS all user stories +- **Phase 3 (US1)**: Depends on Phase 2 complete; T012/T013 parallel with T014 +- **Phase 4 (US2)**: Depends on Phase 2 + most of Phase 3 (needs `OcrSandboxPromptManager` base) +- **Phase 5 (US3)**: Depends on Phase 2 (AiPromptsService); **can run in parallel with Phases 3 & 4** +- **Phase 6 (Polish)**: Depends on Phases 3, 4, 5 complete + +### User Story Dependencies + +- **US1 (P1)**: Needs Phase 2 complete — independently testable +- **US2 (P2)**: Needs Phase 2 + US1 UI base — builds on US1 component +- **US3 (P3)**: Needs Phase 2 only — **fully independent from US1/US2 frontend**; can be done in parallel + +### Within Each Phase + +- Phase 1: T001 → T002 (sequential, T001 informs seed data); T002b/T003/T004/T005/T006 parallel after T002 +- Phase 2: T007 first → T008 (needs service) → T009 → T010/T011 parallel +- Phase 3: T012/T013 parallel → T014 → T015 → T016 → T017; T018 anytime +- Phase 4: T019 → T020 → T021; T022 anytime +- Phase 5: T023 → T024 → T025 → T026 → T027 (sequential — each builds on previous) + +--- + +## Parallel Example: Phase 2 Backend + Phase 5 Processor + +```bash +# Once Phase 1 complete, these can run in parallel: + +# Developer A — Phase 2 (backend service/controller/module) +T007: AiPromptsService +T008: AiPromptsController +T009: AiPromptsModule +T010: Register in AiModule + +# Developer B — Phase 5 (processor changes) +# NOTE: Developer B needs AiPromptsService injectable (T007 done) +# so Phase 5 starts after T007 completes +T023: Inject AiPromptsService +T024: resolvePrompt() method +T025: fix sandbox timeout + replace hardcoded prompt +T026: replace migrate-document hardcoded prompt +T027: auto-save test_result_json +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 — Version Management) + +1. Complete Phase 1: Setup (T001–T006) +2. Complete Phase 2: Foundational (T007–T011) +3. Complete Phase 3: US1 (T012–T018) +4. **STOP and VALIDATE**: Admin can manage prompt versions without PDF upload +5. Deploy/demo US1 independently + +### Incremental Delivery + +1. Phase 1 + 2 → Foundation ready +2. Phase 3 (US1) → Admin can manage versions (**MVP delivered**) +3. Phase 5 (US3) in parallel with Phase 4 (US2) → Runtime resolution + Sandbox testing +4. Phase 6 → Polish and release + +--- + +## Notes + +- **T001 is critical**: Must read exact hardcoded prompt before deleting it — seed data depends on it +- **T025 + T026**: The hardcoded prompt removal is the "100% DB-driven" success criterion (SC-005) +- **T028**: Service unit tests should mock Redis to test cache hit + fallback scenarios +- **[P] tasks**: T003/T004/T005/T006 can all run in parallel (separate files) +- **Avoid**: modifying `ai-batch.processor.ts` (T025/T026) before `AiPromptsService` (T007) is injectable