690525:2327 ADR-023-229 dynamic prompt #01
This commit is contained in:
@@ -15,13 +15,13 @@
|
||||
|
||||
**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
|
||||
- [x] 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
|
||||
- [x] 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)
|
||||
- [x] 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
|
||||
- [x] 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)
|
||||
- [x] T004 [P] Create `backend/src/modules/ai/prompts/dto/create-ai-prompt.dto.ts` with `template: string` (class-validator `@IsNotEmpty()`)
|
||||
- [x] T005 [P] Create `backend/src/modules/ai/prompts/dto/update-prompt-note.dto.ts` with `manualNote: string | null`
|
||||
- [x] 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
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
**⚠️ CRITICAL**: All user story implementation depends on this phase
|
||||
|
||||
- [ ] T007 Create `backend/src/modules/ai/prompts/ai-prompts.service.ts` with:
|
||||
- [x] 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`
|
||||
@@ -41,10 +41,10 @@
|
||||
- `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
|
||||
- [x] 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
|
||||
- [x] T009 Create `backend/src/modules/ai/prompts/ai-prompts.module.ts` — import `TypeOrmModule.forFeature([AiPrompt])`, `RedisModule`, export `AiPromptsService`
|
||||
- [x] T010 Register `AiPromptsModule` in `backend/src/modules/ai/ai.module.ts` imports array
|
||||
- [x] 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
|
||||
|
||||
@@ -58,13 +58,13 @@
|
||||
|
||||
### 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`
|
||||
- [x] 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`
|
||||
- [x] 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`
|
||||
- [x] T014 [US1] Create `frontend/hooks/use-ai-prompts.ts` — TanStack Query `useQuery` (listVersions) + `useMutation` (create, activate, delete, updateNote) with `invalidateQueries` on success
|
||||
- [x] 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)
|
||||
- [x] 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
|
||||
- [x] T017 [US1] Wire `OcrSandboxPromptManager` into existing AI Admin Console OCR Sandbox tab (replace or extend existing component in `frontend/app/(admin)/admin/...`)
|
||||
- [x] 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
|
||||
|
||||
@@ -78,10 +78,10 @@
|
||||
|
||||
### 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
|
||||
- [x] T019 [US2] Extend `OcrSandboxPromptManager.tsx` — เพิ่ม File Upload section (PDF only) + "เริ่มทำ OCR Sandbox" button + ผลลัพธ์ JSON display panel + "บันทึก Manual Note" field (textarea + button)
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
|
||||
@@ -95,11 +95,11 @@
|
||||
|
||||
### 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)
|
||||
- [x] T023 [US3] Inject `AiPromptsService` into `backend/src/modules/ai/processors/ai-batch.processor.ts` constructor
|
||||
- [x] 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)
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
|
||||
@@ -109,17 +109,17 @@
|
||||
|
||||
**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`:
|
||||
- [x] 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)
|
||||
- [x] T029 [P] Verify SQL delta syntax — run against a local MariaDB copy and confirm seed data is present with correct `is_active = 1`
|
||||
- [x] T030 [P] Run `pnpm --filter backend lint` and `pnpm --filter frontend lint` — fix any issues in new files
|
||||
- [x] T031 Run quickstart.md acceptance checklist end-to-end and confirm all checkboxes pass
|
||||
- [x] T032 Update `CONTEXT.md` "System readiness summary" table — change ADR-029 row status from "🟡 ADR Accepted" to "✅ พร้อม" (if applicable)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user