690606:1253 ADR-035-135 #03
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
// - 2026-05-21: เพิ่ม checkHealth สำหรับตรวจสอบสุขภาพและความเร็ว (Latency) ของ Ollama
|
// - 2026-05-21: เพิ่ม checkHealth สำหรับตรวจสอบสุขภาพและความเร็ว (Latency) ของ Ollama
|
||||||
// - 2026-06-02: เพิ่ม loadModel() preloading, ดึงจริงจาก /api/ps และเพิ่ม unloadModel() เพื่อล้างหน่วยความจำ GPU/VRAM (ADR-033, Suggestion 1)
|
// - 2026-06-02: เพิ่ม loadModel() preloading, ดึงจริงจาก /api/ps และเพิ่ม unloadModel() เพื่อล้างหน่วยความจำ GPU/VRAM (ADR-033, Suggestion 1)
|
||||||
// - 2026-06-03: ADR-034 — เปลี่ยน default model เป็น typhoon2.5-np-dms; เพิ่ม ocrModel field, keepAlive param ใน loadModel(), model option ใน OllamaGenerateOptions, getOcrModelName()
|
// - 2026-06-03: ADR-034 — เปลี่ยน default model เป็น typhoon2.5-np-dms; เพิ่ม ocrModel field, keepAlive param ใน loadModel(), model option ใน OllamaGenerateOptions, getOcrModelName()
|
||||||
|
// - 2026-06-06: เพิ่ม system prompt support ใน OllamaGenerateOptions และ generate() method เพื่อรองรับ Typhoon model ที่ต้องการ system prompt แยกต่างหาก
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
@@ -14,6 +15,8 @@ export interface OllamaGenerateOptions {
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
/** ชื่อ model ที่ต้องการใช้ — ถ้าไม่ระบุ จะใช้ mainModel เป็นค่าเริ่มต้น (ADR-034) */
|
/** ชื่อ model ที่ต้องการใช้ — ถ้าไม่ระบุ จะใช้ mainModel เป็นค่าเริ่มต้น (ADR-034) */
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** System prompt สำหรับ Typhoon model ที่ต้องการ system prompt แยกต่างหาก (ใช้ triple quotes) */
|
||||||
|
system?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** บริการเรียก Ollama local-only บน Admin Desktop ตาม ADR-023A */
|
/** บริการเรียก Ollama local-only บน Admin Desktop ตาม ADR-023A */
|
||||||
@@ -57,6 +60,7 @@ export class OllamaService {
|
|||||||
{
|
{
|
||||||
model: options.model ?? this.mainModel,
|
model: options.model ?? this.mainModel,
|
||||||
prompt,
|
prompt,
|
||||||
|
system: options.system,
|
||||||
stream: false,
|
stream: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py
|
# File: specs/04-Infrastructure-OPS/04-00-docker-compose\Desk-5439\ocr-sidecar\app.py
|
||||||
# Tesseract OCR HTTP Sidecar API — รับ POST /ocr แล้วคืนข้อความที่สกัดจาก PDF/Image
|
# Tesseract OCR HTTP Sidecar API — รับ POST /ocr แล้วคืนข้อความที่สกัดจาก PDF/Image
|
||||||
# ตาม ADR-023A: OCR auto-detect (PyMuPDF chars > 100 → Fast path, else Tesseract)
|
# ตาม ADR-023A: OCR auto-detect (PyMuPDF chars > 100 → Fast path, else Tesseract)
|
||||||
# Change Log:
|
# Change Log:
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
# - 2026-06-04: รับค่า temperature/top_p/repeat_penalty จาก frontend sandbox ได้ (optional override)
|
# - 2026-06-04: รับค่า temperature/top_p/repeat_penalty จาก frontend sandbox ได้ (optional override)
|
||||||
# - 2026-06-04: แก้ bug prompt="" ทำให้ Ollama ไม่ generate — เปลี่ยนเป็น minimal trigger prompt
|
# - 2026-06-04: แก้ bug prompt="" ทำให้ Ollama ไม่ generate — เปลี่ยนเป็น minimal trigger prompt
|
||||||
# - 2026-06-04: เพิ่ม alias normalization สำหรับ engine name เก่า (typhoon-ocr1.5-3b → typhoon-np-dms-ocr)
|
# - 2026-06-04: เพิ่ม alias normalization สำหรับ engine name เก่า (typhoon-ocr1.5-3b → typhoon-np-dms-ocr)
|
||||||
# - 2026-06-04: เปลี่ยน keep_alive จาก 0 เป็น 300s เพื่อไม่ให้ unload model ระหว่าง sandbox session (ลด cold-start)
|
|
||||||
# - 2026-06-04: เพิ่ม TYPHOON_OCR_DPI=150 (แยกจาก Tesseract DPI=300) — ลด image token count 4x เพื่อเร่ง CPU inference (model >8GB ไม่พอ VRAM)
|
# - 2026-06-04: เพิ่ม TYPHOON_OCR_DPI=150 (แยกจาก Tesseract DPI=300) — ลด image token count 4x เพื่อเร่ง CPU inference (model >8GB ไม่พอ VRAM)
|
||||||
# - 2026-06-04: ส่ง color image (ไม่ผ่าน preprocess_image) ไปยัง Typhoon OCR — vision model ต้องการ color ไม่ใช่ binarized grayscale
|
# - 2026-06-04: ส่ง color image (ไม่ผ่าน preprocess_image) ไปยัง Typhoon OCR — vision model ต้องการ color ไม่ใช่ binarized grayscale
|
||||||
# - 2026-06-04: เพิ่ม num_gpu:99 ใน Ollama options เพื่อบังคับ GPU layers (แก้ device=CPU ทั้งที่ VRAM พอ)
|
# - 2026-06-04: เพิ่ม num_gpu:99 ใน Ollama options เพื่อบังคับ GPU layers (แก้ device=CPU ทั้งที่ VRAM พอ)
|
||||||
# - 2026-06-02: เพิ่มการตรวจสอบ API Key (X-API-Key Header) สำหรับ endpoints หลัก เพื่อความมั่นคงปลอดภัยตามข้อเสนอแนะ Code Review
|
# - 2026-06-02: เพิ่มการตรวจสอบ API Key (X-API-Key Header) สำหรับ endpoints หลัก เพื่อความมั่นคงปลอดภัยตามข้อเสนอแนะ Code Review
|
||||||
# - 2026-06-05: เพิ่ม Option 2 (aggressive preprocessing: deskew + Otsu threshold + morphology) และ Option 3 (smart post-processing: regex-based hallucination removal) เพื่อลด Tesseract noise/hallucination (T025)
|
# - 2026-06-05: เพิ่ม Option 2 (aggressive preprocessing: deskew + Otsu threshold + morphology) และ Option 3 (smart post-processing: regex-based hallucination removal) เพื่อลด Tesseract noise/hallucination (T025)
|
||||||
|
# - 2026-06-06: เปลี่ยน keep_alive จาก 300s เป็น 0 เพื่อ unload model ทันทีหลังเสร็จงาน (แก้ปัญหา VRAM ไม่พอเมื่อ typhoon2.5-np-dms load พร้อมกัน)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@@ -385,7 +385,7 @@ Extract all text from this image.""",
|
|||||||
"images": [image_base64],
|
"images": [image_base64],
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"options": options,
|
"options": options,
|
||||||
"keep_alive": 300, # คง model ไว้ใน VRAM/RAM 5 นาที เพื่อลด cold-start ระหว่าง sandbox session
|
"keep_alive": 0, # Unload model ทันทีหลังเสร็จงานเพื่อคืน VRAM ให้ typhoon2.5-np-dms ใช้งานได้
|
||||||
}
|
}
|
||||||
with httpx.Client(timeout=TYPHOON_OCR_TIMEOUT) as client:
|
with httpx.Client(timeout=TYPHOON_OCR_TIMEOUT) as client:
|
||||||
response = client.post(f"{OLLAMA_API_URL}/api/generate", json=payload)
|
response = client.post(f"{OLLAMA_API_URL}/api/generate", json=payload)
|
||||||
@@ -489,13 +489,13 @@ def embed_text(req: EmbedRequest):
|
|||||||
output = bge_model.encode([req.text], return_dense=True, return_sparse=True)
|
output = bge_model.encode([req.text], return_dense=True, return_sparse=True)
|
||||||
dense_vector = [float(x) for x in output['dense_vecs'][0]]
|
dense_vector = [float(x) for x in output['dense_vecs'][0]]
|
||||||
lexical_dict = output['lexical_weights'][0]
|
lexical_dict = output['lexical_weights'][0]
|
||||||
|
|
||||||
indices = []
|
indices = []
|
||||||
values = []
|
values = []
|
||||||
for token_id, weight in lexical_dict.items():
|
for token_id, weight in lexical_dict.items():
|
||||||
indices.append(int(token_id))
|
indices.append(int(token_id))
|
||||||
values.append(float(weight))
|
values.append(float(weight))
|
||||||
|
|
||||||
return EmbedResponse(
|
return EmbedResponse(
|
||||||
dense=dense_vector,
|
dense=dense_vector,
|
||||||
sparse={"indices": indices, "values": values}
|
sparse={"indices": indices, "values": values}
|
||||||
@@ -518,11 +518,11 @@ def rerank_chunks(req: RerankRequest):
|
|||||||
scores = [scores]
|
scores = [scores]
|
||||||
else:
|
else:
|
||||||
scores = [float(s) for s in scores]
|
scores = [float(s) for s in scores]
|
||||||
|
|
||||||
indexed_scores = list(enumerate(scores))
|
indexed_scores = list(enumerate(scores))
|
||||||
indexed_scores.sort(key=lambda x: x[1], reverse=True)
|
indexed_scores.sort(key=lambda x: x[1], reverse=True)
|
||||||
ranked_indices = [idx for idx, _ in indexed_scores]
|
ranked_indices = [idx for idx, _ in indexed_scores]
|
||||||
|
|
||||||
return RerankResponse(
|
return RerankResponse(
|
||||||
scores=scores,
|
scores=scores,
|
||||||
ranked_indices=ranked_indices
|
ranked_indices=ranked_indices
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
[Nest] 1 - 06/06/2026, 11:29:32 AM LOG [RouterExplorer] Mapped {/api/dashboard/stats, GET} route +0ms
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:32 AM LOG [RouterExplorer] Mapped {/api/dashboard/activity, GET} route +1ms
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:32 AM LOG [RouterExplorer] Mapped {/api/dashboard/pending, GET} route +0ms
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:33 AM LOG [SearchService] Elasticsearch connection successful
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:33 AM LOG [AiQdrantService] Qdrant collection lcbp3_vectors already exists with correct Hybrid schema (1024 dims) — skipping recreation
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:33 AM LOG [AiQdrantService] Created payload indexes for lcbp3_vectors
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:33 AM LOG [NestApplication] Nest application successfully started +94ms
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:33 AM LOG [Bootstrap] Application is running on: http://127.0.0.1:3000/api
|
||||||
|
[Nest] 1 - 06/06/2026, 11:29:33 AM LOG [Bootstrap] Swagger UI is available at: http://127.0.0.1:3000/docs
|
||||||
|
[Nest] 1 - 06/06/2026, 11:48:39 AM LOG [AiBatchProcessor] Sandbox OCR-Only job processing — jobId=019e9b42-fe09-702c-837d-233152908606
|
||||||
|
[Nest] 1 - 06/06/2026, 11:48:39 AM LOG [SandboxOcrEngineService] detectAndExtract called — engine="typhoon-np-dms-ocr" pdfPath="/app/uploads/temp/73243b30-b339-4a45-9a99-e08bb206b320.pdf" typhoonOptions={"temperature":0.1,"topP":0.1,"repeatPenalty":1.1}
|
||||||
|
[Nest] 1 - 06/06/2026, 11:48:39 AM LOG [SandboxOcrEngineService] engine="typhoon-np-dms-ocr" → calling sidecar at http://192.168.10.100:8765/ocr-upload
|
||||||
|
[Nest] 1 - 06/06/2026, 11:48:39 AM LOG [SandboxOcrEngineService] File read OK — 1405031 bytes from "/app/uploads/temp/73243b30-b339-4a45-9a99-e08bb206b320.pdf"
|
||||||
|
[Nest] 1 - 06/06/2026, 11:48:39 AM LOG [SandboxOcrEngineService] Sending to sidecar — engine=typhoon-np-dms-ocr options={"temperature":0.1,"topP":0.1,"repeatPenalty":1.1}
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:09 AM LOG [SandboxOcrEngineService] Sidecar response OK — engineUsed="typhoon-np-dms-ocr" ocrUsed=true textLen=7457
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:43 AM LOG [AiBatchProcessor] Sandbox AI-Extract job processing — jobId=019e9b42-fe09-702c-837d-233152908606
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:49 AM DEBUG [AiBatchProcessor] Raw LLM response: {
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:49 AM WARN [AiBatchProcessor] JSON parse attempt 1 failed, retrying...
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM WARN [GlobalExceptionFilter] Error occurred
|
||||||
|
path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0 exception={"name":"ThrottlerException","message":"ThrottlerException: Too Many Requests","stack":"ThrottlerException: ThrottlerException: Too Many Requests\n at ThrottlerGuard.throwThrottlingException (/app/node_modules/.pnpm/@nestjs+throttler@6.4.0_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@_a956326c7b57589d550b3ce38b9f0fca/node_modules/@nestjs/throttler/dist/throttler.guard.js:147:15)\n at process.processTicksAndRejections (node:internal/process/task_queues:104:5)\n at async ThrottlerGuard.handleRequest (/app/node_modules/.pnpm/@nestjs+throttler@6.4.0_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@_a956326c7b57589d550b3ce38b9f0fca/node_modules/@nestjs/throttler/dist/throttler.guard.js:119:13)\n at async ThrottlerGuard.canActivate (/app/node_modules/.pnpm/@nestjs+throttler@6.4.0_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@_a956326c7b57589d550b3ce38b9f0fca/node_modules/@nestjs/throttler/dist/throttler.guard.js:86:28)\n at async GuardsConsumer.tryActivate (/app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/guards/guards-consumer.js:22:17)\n at async canActivateFn (/app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/router/router-execution-context.js:135:33)\n at async /app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/router/router-execution-context.js:42:31\n at async /app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/router/router-proxy.js:9:17"}
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM WARN [GlobalExceptionFilter]
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM WARN [GlobalExceptionFilter] Error occurred
|
||||||
|
path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 path=/api/ai/admin/sandbox/job/019e9b42-fe09-702c-837d-233152908606 method=GET ip=172.29.0.21 userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0 exception={"name":"ThrottlerException","message":"ThrottlerException: Too Many Requests","stack":"ThrottlerException: ThrottlerException: Too Many Requests\n at ThrottlerGuard.throwThrottlingException (/app/node_modules/.pnpm/@nestjs+throttler@6.4.0_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@_a956326c7b57589d550b3ce38b9f0fca/node_modules/@nestjs/throttler/dist/throttler.guard.js:147:15)\n at process.processTicksAndRejections (node:internal/process/task_queues:104:5)\n at async ThrottlerGuard.handleRequest (/app/node_modules/.pnpm/@nestjs+throttler@6.4.0_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@_a956326c7b57589d550b3ce38b9f0fca/node_modules/@nestjs/throttler/dist/throttler.guard.js:119:13)\n at async ThrottlerGuard.canActivate (/app/node_modules/.pnpm/@nestjs+throttler@6.4.0_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@_a956326c7b57589d550b3ce38b9f0fca/node_modules/@nestjs/throttler/dist/throttler.guard.js:86:28)\n at async GuardsConsumer.tryActivate (/app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/guards/guards-consumer.js:22:17)\n at async canActivateFn (/app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/router/router-execution-context.js:135:33)\n at async /app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/router/router-execution-context.js:42:31\n at async /app/node_modules/.pnpm/@nestjs+core@11.1.19_@nestjs+common@11.1.19_class-transformer@0.5.1_class-validator@0.1_cbe1fb75cf716ace88130e83fa30d064/node_modules/@nestjs/core/router/router-proxy.js:9:17"}
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM WARN [GlobalExceptionFilter]
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM ERROR [AiBatchProcessor] Failed to parse LLM response as JSON after 2 attempts. Raw: {
|
||||||
|
, Cleaned: {
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM ERROR [AiBatchProcessor] Sandbox AI-extract failed: Failed to parse LLM response as JSON after 2 attempts. Raw: {
|
||||||
|
, Cleaned: {
|
||||||
|
[Nest] 1 - 06/06/2026, 11:52:50 AM ERROR [AiBatchProcessor] Batch job failed — jobType=sandbox-ai-extract, documentPublicId=019e9b42-fe09-702c-837d-233152908606
|
||||||
|
Error: Failed to parse LLM response as JSON after 2 attempts. Raw: {
|
||||||
|
| ||||||