690525:1720 ADR-028-228-migration-OCR #06 dynamic prompt
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+32
-17
@@ -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
|
||||
|
||||
+76
-19
@@ -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 (งานหลักที่เหลือ)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Rollback: ลบตาราง ai_prompts (ADR-029)
|
||||
-- Date: 2026-05-25
|
||||
|
||||
DROP TABLE IF EXISTS ai_prompts;
|
||||
@@ -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;
|
||||
@@ -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, '<current hardcoded prompt with {{ocr_text}}>', 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<string> {
|
||||
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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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<string, unknown> | null;
|
||||
|
||||
@Column({ name: 'is_active', type: 'tinyint', width: 1, default: 0 })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ name: 'test_result_json', type: 'json', nullable: true })
|
||||
testResultJson: Record<string, unknown> | 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<string, unknown> | 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
|
||||
@@ -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.
|
||||
@@ -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 <superadmin_token>"
|
||||
|
||||
# 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 <superadmin_token>" \
|
||||
-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 <superadmin_token>"
|
||||
|
||||
# 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 <superadmin_token>"
|
||||
# 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 <superadmin_token>" \
|
||||
-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 <superadmin_token>"
|
||||
# Expected: 400 Bad Request
|
||||
|
||||
# Delete inactive version (should succeed):
|
||||
curl -X DELETE http://localhost:3001/api/ai/prompts/ocr_extraction/1 \
|
||||
-H "Authorization: Bearer <superadmin_token>"
|
||||
# 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
|
||||
@@ -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<AiPrompt>` 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
|
||||
@@ -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 ต่างกัน)
|
||||
@@ -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<AiPrompt[]>` — ORDER BY version_number DESC
|
||||
- `getActive(promptType: string): Promise<AiPrompt | null>` — Redis cache first, DB fallback
|
||||
- `create(promptType, dto, userId): Promise<AiPrompt>` — 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<AiPrompt>` — 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<void>` — guard active version + audit_logs
|
||||
- `updateNote(promptType, versionNumber, note): Promise<AiPrompt>` — PATCH manual_note only
|
||||
- `saveTestResult(promptType, versionNumber, resultJson): Promise<void>` — 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
|
||||
Reference in New Issue
Block a user