CORETH — Architecture & Installation

CORETH — Current System Architecture & Installation

เอกสารฉบับเดียวที่รวมสถาปัตยกรรมปัจจุบัน ขั้นตอนติดตั้ง การตั้งค่า การตรวจสอบความพร้อม และงาน operations ของ CORETH Context Processor ทุกหัวข้อถอดย้อนกลับมาจาก source code, migrations, Docker Compose และ management commands ที่มีอยู่จริงใน repository ไม่ได้คัดลอกจากเอกสารสถาปัตยกรรมรุ่นก่อน

Repository
Core-technology-Co-Ltd/ai-coreth-context-processor
Branch
main
Commit
8d2a8657d595d084597f3f0cfea14c77bbcfe2ec
Commit date
2026-09-18 11:45 +07:00
Application version
0.2.0 pyproject.toml
Runtime
Python 3.12 · Django 5.2 · PostgreSQL 17
Doc updated
2026-09-18
Architecture basis
current implementation
Architecture Source of Truth: Current repository implementation at commit 8d2a8657d595d084597f3f0cfea14c77bbcfe2ec (branch main). เมื่อเอกสารเก่ากับโค้ดขัดกัน ให้ยึดโค้ด/migration/compose/พฤติกรรม runtime เป็นหลักเสมอ และปรับเอกสารฉบับนี้ตามเมื่อสถาปัตยกรรมเปลี่ยนอีก

1. Overview#

CORETH Context Processor คือระบบที่ติดตั้งในฝั่งลูกค้า (customer-local) ทำหน้าที่รับไฟล์จากต้นทาง แปลงเป็น Structured Data ที่มี schema ชัดเจน แล้ว project ต่อเป็น Structured Context (Entity / Fact / Relationship / Evidence) สำหรับให้ agent และแอปพลิเคชันปลายทางเรียกใช้

สิ่งที่ระบบนี้เป็นเจ้าของ

  • Source configuration แบบ immutable + versioned
  • ผลการสกัดไฟล์และผล AI ต่อไฟล์
  • Dataset / SchemaVersion / Record ทุกเวอร์ชัน
  • Governance ของการตรวจทาน (Submission, Candidate, Approval)
  • Canonical Context และ ContextVersion
  • Readiness ของ deployment

สิ่งที่ระบบนี้ไม่ได้เป็นเจ้าของ

  • ไฟล์ดิบ — เป็นของ File Ingestion ภายนอก
  • ความลับ — อยู่ใน AWS Secrets Manager
  • ตัวตนผู้ใช้ OpenWebUI — เป็นของ OpenWebUI
  • Public TLS / DNS — เป็นของ Cloudflare Tunnel

ช่องทางเข้าข้อมูลที่ใช้งานจริงวันนี้

  • FILE_READY — ดึงจาก File Ingestion (pull, inventory + event replay)
  • OPENWEBUI_UPLOAD — ผู้ใช้อัปโหลดตรงผ่าน Service JWT (push)
  • POST /api/v1/ingestion/file-ready — ต้นทาง push แจ้งไฟล์พร้อม

Django applications

Appขอบเขตฐานข้อมูล (router)Migrations
coreth_context Source, signal, extraction, semantic, canonical Context, provider API, doctor, backup default0001–0017
coreth_data Dataset, SchemaVersion, Record, governance/review, schema design, OpenWebUI archive data ถ้ามี มิฉะนั้น default0001–0018
coreth_control EffectivePolicy, PolicyOverride, WorkAdmission, Admin Monitor UI, AdminMembership default0001–0006

การแยกนี้บังคับด้วย config/database_router.py → DomainRouter ซึ่ง route ทุก model ของ coreth_data ไป alias data เมื่อมี DATA_DATABASE_URL และ route ที่เหลือ (รวม django.contrib.auth) ไป default

สถานะของฟีเจอร์ในเอกสารนี้

IMPLEMENTED มีโค้ดและใช้งานได้จริงตาม default OPTIONAL มีโค้ดครบ แต่ต้องตั้งค่าเพิ่มจึงทำงาน PARTIAL มีโค้ดบางส่วน / ใช้เฉพาะเส้นทางเดิม CLI-ONLY มีใน management command แต่ไม่มีใน Admin UI

2. Architecture#

ทุกกล่องและทุกลูกศรในหัวข้อนี้ตรวจกลับได้กับไฟล์จริงใน repository ถ้า component ไม่ปรากฏในโค้ด จะไม่อยู่ในไดอะแกรม

2.1 System Context#

ขอบเขตของระบบ: อะไรอยู่ใน Compose stack ของลูกค้า และอะไรเป็นบริการภายนอกที่ต้องพึ่งพา

ต้นทาง / ผู้ใช้ / ปลายทาง CORETH Context Processor · Docker Compose (เครื่องลูกค้า) บริการภายนอกที่ต้องพึ่งพา OpenWebUI อัปโหลดไฟล์ + ตรวจทาน (Service JWT) Reviewer / Admin browser Django session · /admin Agent / แอปปลายทาง Context Slice + Data Provider API File Ingestion service เจ้าของไฟล์ดิบ · inventory + events HTTPS + RS256 JWT caddy 127.0.0.1:8080 → :8080 api gunicorn REST + health admin Monitor UI 127.0.0.1:8081 worker · runworker poll → process → project → review jobs → doctor PostgreSQL alias default context + control PostgreSQL alias data coreth_data record-artifacts shared volume backup-data volume cloudflared + cloudflare-origin profile cloudflare · ไม่บังคับ profiles: local · admin · worker ops · cloudflare · test · wizard AWS Secrets Manager aws-secretsmanager://… OpenAI API Responses + Agents SDK AWS S3 + KMS objects · input snapshots AWS CloudWatch Logs Admin activity · ไม่บังคับ Cloudflare edge TLS + DNS · ไม่บังคับ pull + verify + fetch /* /admin* (overlay)
ที่มา: compose.yaml, compose.cloudflare.yaml, deploy/Caddyfile*, config/settings.py, coreth_context/file_ingestion.py, coreth_context/secrets.py, coreth_data/snapshots.py, coreth_control/logs.py
ข้อเท็จจริงสำคัญ: service api ไม่ได้ publish port ออกนอก container ทางเข้าเดียวคือ caddy ที่ 127.0.0.1:8080 ส่วน admin publish 127.0.0.1:8081 แยกต่างหาก และ deploy/Caddyfile (โหมดปกติ) proxy ทุก path ไป api:8000 เท่านั้น — จึงเปิด /admin ผ่าน :8080 ไม่ได้ ต้องใช้ :8081 หรือเปิด overlay Cloudflare ที่ใช้ deploy/Caddyfile.cloudflare ซึ่ง route /admin* ไป admin:8000

2.2 Runtime / Deployment Topology#

ทุก service มาจาก compose.yaml (และ compose.cloudflare.yaml สำหรับ overlay) service ที่ไม่มี profile จะขึ้นเสมอเมื่อสั่ง docker compose up

ServiceProfileคำสั่งที่รันจริงPort ที่เปิดNetworksสถานะ
setup— (ขึ้นเสมอ) python -m coreth_context.compose_setup --auto —noneIMPLEMENTED
migrate— (ขึ้นเสมอ) manage.py migrate_domains —backend, egressIMPLEMENTED
api— (ขึ้นเสมอ) gunicorn config.wsgi --workers 2 --timeout 120 ไม่เปิดbackend, egressIMPLEMENTED
caddy— (ขึ้นเสมอ) caddy:2.10-alpine + deploy/Caddyfile 127.0.0.1:${CORETH_HTTP_PORT:-8080}frontend, backendIMPLEMENTED
postgreslocal postgres:17-alpine —backendOPTIONAL
adminadmin, local gunicorn + CORETH_ADMIN_ENABLED=true 127.0.0.1:${CORETH_ADMIN_PORT:-8081}backend, egressIMPLEMENTED
workerworker manage.py runworker —backend, egressIMPLEMENTED
processorworker manage.py runworker --process-only —backend, egressOPTIONAL
backupops manage.py runbackup_scheduler —backend, egressOPTIONAL
opsops ฐานสำหรับ docker compose run --rm ops … (command ที่ประกาศไว้เป็น context_doctor --help) —backend, egressOPTIONAL
wizardwizard compose_setup โหมดโต้ตอบ · mount .:/setup · user 0:0 —backend, egressIMPLEMENTED
testtest pytest -q (build arg INSTALL_DEV=true) —backend, egressOPTIONAL
cloudflare-origincloudflare caddy + deploy/Caddyfile.cloudflare —backend, tunnelOPTIONAL
cloudflaredcloudflare tunnel --no-autoupdate run —tunnel, egressOPTIONAL
network: frontend caddy reverse proxy 127.0.0.1:8080 network: backend · internal: true (ไม่มีทางออกอินเทอร์เน็ต) api :8000 · health/live admin :8000 · Monitor worker profile worker migrate run-once backup / ops profile ops postgres profile local cloudflare-origin profile cloudflare · backend + tunnel volumes ร่วม: record-artifacts (api + worker + admin + backup + ops) backup-data (api = ro, ops = rw) postgres-data (profile local) network: egress (ออกอินเทอร์เน็ต) api · admin · worker migrate · backup · ops ต่อ AWS Secrets Manager, S3/KMS, OpenAI, CloudWatch และ File Ingestion ผ่าน network นี้เท่านั้น network: tunnel · internal (overlay) cloudflared tunnel + egress ไม่มี public port บนเครื่อง ทราฟฟิกออกทาง Cloudflare
network backend ถูกประกาศ internal: true — container ที่อยู่เฉพาะ backend ออกอินเทอร์เน็ตไม่ได้ ทุก service ที่ต้องเรียก AWS/OpenAI/File Ingestion จึงถูกต่อกับ egress ด้วย (ดู networks: ของแต่ละ service ใน compose.yaml)

Secret injection ตอน container เริ่มทำงาน

Dockerfile ตั้ง ENTRYPOINT ["python","-m","coreth_context.runtime_secrets"] ทุกคำสั่งจึงถูกห่อด้วย launcher ที่ resolve OPENAI_API_KEY_REF (ต้องขึ้นต้น aws-secretsmanager://) เข้าสู่ os.environ["OPENAI_API_KEY"] แล้วค่อย execvp คำสั่งจริง ถ้า resolve ไม่สำเร็จ process จบด้วย exit code 78 และไม่พิมพ์ค่า/reference ออก log

ตั้ง OPENAI_API_KEY กับ OPENAI_API_KEY_REF พร้อมกันไม่ได้ — launcher จะปฏิเสธ ส่วน secret ของ connector, webhook, backup key และ File Ingestion reader จะ resolve เมื่อต้องใช้จริงผ่าน AwsSecretsManagerResolver ไม่ถูกเขียนลงฐานข้อมูลหรือ log

2.3 Main Data Flow#

เส้นทางจริงจากไฟล์ต้นทางจนถึง canonical Context ทุกขั้นขับเคลื่อนโดย runworker ซึ่งเรียก WorkerCycle.run_once() วนทุก WORKER_POLL_SECONDS (default 2 วินาที)

1 · ทางเข้าข้อมูล FILE_READY: poll_file_ingestion() ดึง inventory + event OPENWEBUI_UPLOAD: POST /api/v1/submissions (Service JWT) coreth_context/file_ingestion.py coreth_context/provider/ingress.py coreth_data/review_api.py 2 · SourceSignal (คิวถาวรในฐานข้อมูล) RECEIVED → PROCESSING → APPLIED / NO_CHANGE / FAILED coreth_context.models TrustedArtifactReference 3 · worker · process_pending() ตรวจ pause / archive / AGENTIC_REQUIRED / AI hold แล้ว fetch object coreth_context/sync.py signal_control.supervise() 4a · File Agent Agents SDK อ่าน schema เดิม แล้วเสนอ table + row coreth_data/file_agent.py 4b · เส้นทางเดิม SafeExtractor + Semantic Processor → pipeline ปิดเมื่อ AGENTIC_ONLY=true เงื่อนไข file_agent.enabled(): FILE_AGENT_ENABLED + AGENTIC_ONLY + source_type FILE_READY/OPENWEBUI + structured_data.enabled 5 · Submission + CandidateGroup + CandidateRow เกิด SchemaImprovement เมื่อ schema ยังไม่มี/ต้องเพิ่ม field record_key + operation (UPSERT/DELETE) ถูกคำนวณตรงนี้ coreth_data/governance.py stage_submission() coreth_data/governance_models.py 6 · ประตูอนุมัติ คนกดยืนยัน → ReviewReceipt + ReviewJob(APPLY) หรือ auto-approve แถวใหม่ → RowApproval + ReviewJob(AUTO_NEW) ไฟล์จาก OpenWebUI ต้องมีคนยืนยันเสมอ ไม่มีทางลัด confirm_submission() coreth_data/new_data.py CORETH_AUTO_APPROVE_NEW_DATA HUMAN_UPLOAD_CONFIRMATION_ REQUIRED 7 · apply_dataset() → publish_review_group() Record + RecordVersion (ประวัติครบทุกเวอร์ชัน) ตารางผลลัพธ์ใน schema app_output.<dataset> DatasetVersion + TransformRun บันทึก lineage ฐานข้อมูล alias data coreth_data/app_output.py prepare_output() สร้าง DDL 8 · DatasetChangedEvent (outbox, ฐาน data) PENDING → DELIVERED · เปิดอ่านที่ /api/v1/data/events worker เก็บทีละ batch_size CORETH_SYNC_BATCH_SIZE 9 · ContextProjector.project_event() → ContextWriter ต้องมี ContextProjectionDefinition ที่ active มิฉะนั้น event ถูกปิดเฉย ๆ coreth_context/projection.py services/context_writer.py 10 · Entity / Fact / Relationship / Evidence + ContextVersion + ContextChangedEvent (outbox) ฐานข้อมูล alias default ส่งออกผ่าน webhook หรือ replay
ลำดับใน WorkerCycle.run_once(): poll ทุก source → process_pending → project DatasetChangedEvent ที่ยัง PENDING → run_review_jobs() → ContextDoctor → OutboxDispatcher

สิ่งที่ worker ทำในหนึ่งรอบ

#งานโค้ดผลที่นับใน output JSON
1poll ทุก source ล่าสุด (File Ingestion + connector)poll_file_ingestion(), synchronizer.poll_source()polled
2reconcile เมื่อครบรอบreconcile_source()reconciled
3ประมวลผล signal ที่ค้างprocess_pending(limit=batch_size)processed
4project dataset event → ContextContextProjector.project_event()projected
5รัน review job ที่ค้างcoreth_data.governance.run_review_jobs()review_jobs
6ประเมิน readiness ของทุก customerContextDoctor().evaluate(persist=True)— (เขียน CustomerContextReadiness)
7ส่ง Context Changed outboxOutboxDispatcher.dispatch_one()dispatched
ผลที่ตามมาเชิงสถาปัตยกรรม: /health/ready อ่านค่า CustomerContextReadiness ซึ่งเขียนโดย ContextDoctor ที่รันอยู่ใน runworker เท่านั้น ถ้าไม่สั่ง docker compose up -d worker ระบบจะไม่มีวันขึ้น READY และจะตอบ 503 DOCTOR_NOT_EVALUATED ตลอด
ReviewJob ทุกชนิดที่ worker รันได้
kindทำอะไรถูกสร้างเมื่อ
ANALYZEanalyze_submission() — วิเคราะห์ไฟล์ที่อัปโหลดด้วย File Agentมี Submission ใหม่ที่ยังไม่มี candidate
APPLY (ค่าเริ่มต้น)apply_dataset(target_id) — เผยแพร่กลุ่มที่ยืนยันแล้วคนกดยืนยัน / อนุมัติ schema
AUTO_NEWapply_dataset(auto_group_ids=[group]) — เฉพาะแถวใหม่ที่ผ่านนโยบายschedule_group() เมื่อเปิด auto-approve
BATCH_REVIEWrun_batch() — ตรวจหลาย submission พร้อมกันต่อศูนย์สร้าง batch ที่ /admin/review-batches
FEEDBACK / FEEDBACK_APPLYrun_feedback_job() — ให้ AI แก้ข้อเสนอ schema ตาม commentPOST /api/v1/schema-improvements/{id}/feedback

หลังจบทุก job รอบหนึ่ง run_review_jobs() จะ mirror GovernanceAudit ไป OperatorActionAudit, เรียก reconcile_openwebui_history() และ reconcile_openwebui_archives() ต่อทันที เมื่อ CORETH_REVIEW_PAUSE_APPLY=true ฟังก์ชันนี้จะ return ทันทีโดยไม่รันอะไรเลย

2.4 Submission / Review Flow#

เส้นทางนี้ใช้ทั้งไฟล์ที่มาจาก File Ingestion และไฟล์ที่ผู้ใช้อัปโหลดตรงจาก OpenWebUI ต่างกันที่ว่า OpenWebUI สร้าง Submission ผ่าน API โดยตรง ขณะที่ FILE_READY สร้างจาก SourceSignal ในรอบของ worker

1 · สร้าง Submission POST /api/v1/submissions ตรวจ SourceBinding + ToolServiceGrant status = ANALYZING 2 · ReviewJob(ANALYZE) analyze_submission() ตรวจ checksum แล้วเรียก File Agent run_analysis() · Agents SDK 3 · Candidates CandidateGroup + CandidateRow + SchemaImprovement ถ้าจำเป็น status = REVIEW 4 · Review manifest GET …/{id}/review PATCH …/{id}/candidate เพื่อแก้ค่า manifest_digest · revision 5 · ยืนยัน POST …/{id}/confirm ReviewReceipt + ReviewJob(APPLY) หรือ RowApproval เมื่อ auto-approve 6 · Canonical write apply_dataset() ใน worker RecordVersion + app_output.<table> + DatasetChangedEvent ไปต่อแถวล่าง
coreth_data/review_api.py, coreth_data/governance.py (stage_submission, confirm_submission, apply_dataset, publish_review_group), coreth_data/new_data.py

API ของ workflow นี้

Method + pathใครเรียกได้ผล
GET /api/v1/submissionssession หรือ Service JWT (submissions:read)รายการ submission ล่าสุด 100 รายการ
POST /api/v1/submissionsService JWT (submissions:write) หรือ sessionสร้าง Submission + ReviewJob(ANALYZE)
GET /api/v1/submissions/{id}เหมือนข้างบนสรุป submission
GET /api/v1/submissions/{id}/reviewเหมือนข้างบนmanifest ของแถวที่รอตรวจ + digest
PATCH /api/v1/submissions/{id}/candidateเหมือนข้างบนแก้ค่าแถว → เพิ่ม revision
POST /api/v1/submissions/{id}/confirmsession ของคนเท่านั้น (HUMAN_SESSION_REQUIRED)ReviewReceipt + ReviewJob(APPLY)
GET/PATCH/POST /api/v1/openwebui/submissions…Service JWT ของ OpenWebUIมุมมองเฉพาะผู้ใช้ OpenWebUI คนนั้น
POST /api/v1/data-conflicts/{id}/resolvesessionตัดสิน DataConflict ที่ยัง OPEN
GET /api/v1/submissions-openapi.jsonsession / Service JWTOpenAPI ให้ OpenWebUI ใช้เป็น tool

นโยบายอนุมัติที่ทำงานจริง

กรณีต้องมีคนยืนยันไหมเงื่อนไขในโค้ด
ไฟล์จาก OpenWebUI (มี OpenWebUIUploadReceipt หรือ actor ขึ้นต้น openwebui:) ต้องมีเสมอ new_data.manual_origin() → HUMAN_UPLOAD_CONFIRMATION_REQUIRED
แถว ใหม่ จาก FILE_READY ที่ผ่านนโยบาย ไม่ต้อง CORETH_AUTO_APPROVE_NEW_DATA=true + structured_data.auto_approve_new_data + ไม่ pause
แถวที่ แก้ค่าเดิม หรือมีค่าขัดแย้ง ต้องมี classify_rows() ปล่อยเฉพาะ kind new เข้า auto path
Dataset ใหม่ (ยังไม่มี schema) ต้องมี ต้องอนุมัติ SchemaImprovement ก่อนเสมอ
เปิด CORETH_REVIEW_PAUSE_APPLY=true run_review_jobs() return ทันที — ไม่มีการเผยแพร่ใด ๆ จนกว่าจะปิด flag
การป้องกัน race: ทุกการยืนยันผูกกับ revision และ manifest_digest ถ้า candidate เปลี่ยนระหว่างที่คนกำลังตรวจ API จะตอบ 409 REVISION_CONFLICT ส่วน RowApproval ผูกกับ row_digest + policy_digest ถ้านโยบายหรือแถวเปลี่ยน จะได้ ROW_APPROVAL_STALE และไม่เผยแพร่

ความขัดแย้งของข้อมูล (DataConflict)

เมื่อสองแหล่งเสนอค่าต่างกันในฟิลด์เดียวกันของ record เดียวกัน ระบบสร้าง DataConflict (unique ที่ dataset + record_key + field) สถานะ OPEN แถวนั้นจะไม่ถูกเผยแพร่จนกว่าคนจะตัดสินที่ /admin/data-conflicts หรือเรียก POST /api/v1/data-conflicts/{id}/resolve ลำดับความน่าเชื่อถือกำหนดด้วย SourceAuthorityPolicy (สร้างด้วย manage.py configure_source_authority)

2.5 Schema Improvement Flow#

SchemaImprovement คือข้อเสนอเปลี่ยนสัญญาโครงสร้างข้อมูลหนึ่งชุด มี unique constraint ที่ customer + environment + concept_key จึงมีข้อเสนอที่เปิดอยู่ได้เพียงหนึ่งต่อหนึ่งแนวคิด

PENDING_REVIEW File Agent เสนอ schema revision = 1 REBUILDING AI แก้ตาม feedback ReviewJob(FEEDBACK) APPROVED SchemaVersion ใหม่ + DDL app_output REJECTED group ทั้งหมดถูกปิด SCHEMA_REJECTED audit Dataset ACTIVE active_schema ถูกตั้ง พร้อมรับแถวใหม่ กลับมาให้คนตรวจอีกครั้ง อนุมัติ (POST …/decision) ปฏิเสธ ประตูตรวจก่อนอนุมัติ (decide_schema) • DESIGN_PLAN_DECISION_REQUIRED — มี SchemaDesignPlan ค้างอยู่ ต้องตัดสินผ่าน plan นั้น • REVISION_CONFLICT / SCHEMA_ALREADY_DECIDED — มีคนตัดสินไปแล้วหรือ revision ไม่ตรง • SCHEMA_BASE_CHANGED — base schema เปลี่ยน ระบบพยายาม rebase ให้ถ้าเป็นการเพิ่ม field อย่างเดียว • IDENTITY_MIGRATION_REVIEW_REQUIRED — เปลี่ยน identity field ต้องส่ง migration digest มายืนยัน
coreth_data/governance.py::decide_schema(), coreth_data/schema_evolution.py, coreth_data/schema_feedback.py, coreth_data/schema_design_plans.py

อนุมัติอัตโนมัติได้แค่ไหน

schema_evolution.enabled() จะยอมให้อนุมัติอัตโนมัติก็ต่อเมื่อครบทุกข้อพร้อมกัน:

  • CORETH_AUTO_APPROVE_ADDITIVE_SCHEMA=true (ค่าเริ่มต้น true)
  • CORETH_REVIEW_PAUSE_APPLY=false
  • source มี structured_data.auto_approve_additive ไม่ถูกตั้งเป็น false
  • semantic_processing.mode เป็น AI_REQUIRED หรือ AI_OPTIONAL
  • semantic_processing.allow_external_model = true
  • EffectivePolicy มี ai.enabled = true
แม้ครบทุกข้อ ระบบก็อนุมัติอัตโนมัติได้เฉพาะการเปลี่ยนแปลงชนิด ADDITIVE คือ เพิ่ม field ใหม่ที่ไม่บังคับกรอกเท่านั้น (classify() คืน MODIFY หรือ UNSAFE_ADDITION เมื่อไม่ใช่) การแก้ชนิดข้อมูล ลบ field เปลี่ยน identity หรือสร้าง dataset ใหม่ ต้องให้คนอนุมัติเสมอ

Schema Design (ออกแบบ schema ล่วงหน้าด้วย AI)

นอกจากข้อเสนอที่เกิดจากไฟล์จริง ยังมีเส้นทางออกแบบล่วงหน้า: POST /api/v1/schema-design รับไฟล์ตัวอย่าง/คำอธิบายแล้วสร้าง SchemaDesignPlan + SchemaDesignSource ให้ตรวจที่ /admin/schema-improvements เมื่อ source มี design ที่อนุมัติแล้ว (approved_designs()) การประมวลผลไฟล์จะยึด design นั้นแทนการเลือก field แบบเดิม IMPLEMENTED

2.6 Storage / Source-of-Truth Map#

แต่ละโดเมนมีเจ้าของเดียว อย่าเขียนข้ามโดเมนโดยตรง

โดเมนเจ้าของCanonical storeDerived / mirror
ไฟล์ดิบ + file versionFile Ingestion (ภายนอก) S3 ของ File Ingestion + /v1/file-versions/{id} TrustedArtifactReference, SourceObject
ความลับทุกชนิดAWS Secrets Manager (ภายนอก) aws-secretsmanager://<secret-id> เก็บเฉพาะ reference ใน SourceIdentity.secret_ref และ .env
Source configurationcoreth_context SourceIdentity (immutable ต่อ config_version) latest_source_configurations(), ArchivedSource
คิวงานนำเข้าcoreth_context SourceSignal, SourceCheckpoint, FileIngestionCursor SourceSyncState, IngestionHistory
ผลสกัดไฟล์ / ผล AIcoreth_context ExtractionRecord, SemanticProcessingRecord ไฟล์ใน volume record-artifacts เมื่อ CORETH_COMPACT_RECORDS=true
สัญญา schemacoreth_data SchemaVersion + Dataset.active_schema ตารางจริงใน schema app_output
แถวข้อมูลcoreth_data Record + RecordVersion (ทุกเวอร์ชัน) app_output.<dataset>, DatasetVersion, TransformRun
Governance การตรวจทานcoreth_data Submission, CandidateGroup, CandidateRow, ReviewReceipt, RowApproval GovernanceAudit → OperatorActionAudit
Canonical Contextcoreth_context Entity, Fact, Relationship, Evidence, ContextVersion ContextSliceRecord, CanonicalContextState
นโยบายการทำงานcoreth_control EffectivePolicy (versioned) + PolicyOverride WorkAdmissionState, WorkAdmissionLease
ผู้ใช้ AdminDjango auth + coreth_control auth_user + AdminMembership (customer + environment + role) AdminBranding
ความพร้อมใช้งานcoreth_context CustomerContextReadiness, DoctorEvaluation GET /health/ready, ServiceHeartbeat
สำรองข้อมูลcoreth_context ไฟล์เข้ารหัสใน CORETH_BACKUP_DIRECTORY (volume backup-data) BackupArtifact, RestoreRehearsal
Replay snapshotcoreth_data S3 CORETH_SNAPSHOT_BUCKET + SSE-KMS InputSnapshotArtifact
สิทธิ์ของ tool ภายนอกcoreth_context ToolServiceGrant (scopes + actions + expiry) ProviderAccessAudit
การแยกฐานข้อมูล: ถ้าไม่ตั้ง DATA_DATABASE_URL ทุก app จะใช้ฐานเดียวกัน (DomainRouter.data_database() คืน "default") ถ้าตั้ง จะมีสอง connection แยกกันจริง — จึง ไม่มี transaction เดียวที่คร่อมสองฐาน การส่งข้อมูลจาก data → context ทำผ่าน outbox DatasetChangedEvent เท่านั้น

2.7 Component Responsibilities#

api IMPLEMENTED

หน้าที่
REST สำหรับ ingress, provider, data, submission และ health
Input
HTTP ผ่าน caddy · Service JWT หรือ Django session
Output
JSON · เขียน Submission / TrustedArtifactReference / SourceSignal
เป็นเจ้าของ
ไม่มี state เฉพาะตัว — เขียนลงฐานร่วม
ต้องพึ่ง
PostgreSQL, Secrets Manager (เมื่อ verify File Ingestion)

worker IMPLEMENTED

หน้าที่
poll · process · project · review jobs · doctor · outbox
Input
SourceSignal, ReviewJob, DatasetChangedEvent
Output
Dataset/Record, canonical Context, ContextChangedEvent
เป็นเจ้าของ
ServiceHeartbeat(worker) และ CustomerContextReadiness
ต้องพึ่ง
PostgreSQL, Secrets Manager, OpenAI, File Ingestion, volume record-artifacts

admin (Monitor) IMPLEMENTED

หน้าที่
ตั้งค่า source, ตรวจทานข้อมูล/โครงสร้าง, สำรวจข้อมูล, จัดการผู้ใช้
Input
Django session + AdminMembership
Output
เขียน source config, ตัดสิน schema/conflict, สั่ง retry/recover
เป็นเจ้าของ
AdminBranding, OperatorActionAudit
ต้องพึ่ง
CORETH_ADMIN_ENABLED=true · CloudWatch Logs (ไม่บังคับ)

caddy IMPLEMENTED

หน้าที่
reverse proxy + security headers (X-Frame-Options DENY, nosniff)
Input
127.0.0.1:8080 เท่านั้น ไม่ผูก 0.0.0.0
Output
ส่งต่อ api:8000 (และ admin:8000 ใน overlay)
เป็นเจ้าของ
ไม่มี state

migrate IMPLEMENTED

หน้าที่
migrate_domains = migrate ทุก alias แล้วรัน secure_database
Output
ตาราง + เปิด RLS + ถอนสิทธิ์ PUBLIC/anon/authenticated
รันเมื่อ
ก่อน api/worker ทุกครั้ง (service_completed_successfully)

backup OPTIONAL

หน้าที่
runbackup_scheduler — สำรองเข้ารหัส + ซ้อมกู้คืนตามรอบ
Output
ไฟล์ CORETHBK1 (AES-GCM) + BackupArtifact + RestoreRehearsal
ต้องพึ่ง
CORETH_BACKUP_KEY_FILE หรือ CORETH_BACKUP_KEY_SECRET_REF, pg_dump

Connector ที่มีในโค้ด

source_typeAdapterตั้งค่าได้ที่สถานะ
FILE_READYconnectors/file_ready.py + file_ingestion.pyAdmin /admin/setup และ CLIIMPLEMENTED
OPENWEBUI_UPLOADconnectors/openwebui_upload.pyAdmin /admin/setup และ CLIIMPLEMENTED
S3connectors/s3.pymanage.py configure_source เท่านั้นCLI-ONLY
GOOGLE_DRIVEconnectors/drive.pymanage.py configure_source เท่านั้นCLI-ONLY
POSTGRESQL / MYSQL / MARIADBconnectors/sql.py (read-only, SQLAlchemy)manage.py configure_source เท่านั้นCLI-ONLY
RESTconnectors/rest.pymanage.py configure_source เท่านั้นCLI-ONLY
เมื่อ CORETH_AGENTIC_ONLY=true (ค่าเริ่มต้น) signal ของ source ที่ semantic_processing.mode ไม่ใช่ AI_REQUIRED จะถูกปิดด้วย error AGENTIC_REQUIRED ทันที — connector กลุ่ม CLI-ONLY ข้างบนจึงใช้งานไม่ได้จริง จนกว่าจะตั้ง source ให้เป็น AI_REQUIRED หรือปิด CORETH_AGENTIC_ONLY ระหว่างย้ายระบบ

3. Prerequisites#

ตรวจให้ครบก่อนเริ่ม ทุกข้อในตารางนี้มาจากสิ่งที่โค้ดเรียกใช้จริง

สิ่งที่ต้องมีบังคับ?ทำไม
Docker Engine + Compose plugin v2บังคับทั้งระบบรันผ่าน compose.yaml และใช้ profiles, extends, depends_on.condition
Gitบังคับต้อง clone repo ลงเครื่องเพราะ compose ใช้ build: .
Python 3บังคับสคริปต์ในคู่มือนี้ใช้ python3 สร้าง UUID · ประกอบ JSON ของ secret · และอ่านผล JSON จาก management command (Ubuntu 22.04+ มีติดตั้งมาแล้ว)
PostgreSQL 17 (หรือใช้ profile local)บังคับsecure_database ใช้ RLS และ app_output schema; SQLite ใช้ได้เฉพาะตอนพัฒนา
Customer UUIDบังคับCORETH_DEPLOYMENT_CUSTOMER_ID — /health/ready ตอบ CUSTOMER_NOT_CONFIGURED ถ้าไม่มี
AWS credentials (IAM role หรือ key)บังคับAwsSecretsManagerResolver เป็นทางเดียวที่ระบบอ่านความลับ
OpenAI API key เก็บใน Secrets ManagerบังคับCORETH_AGENTIC_ONLY=true ทำให้ทุก signal ต้องผ่าน AI
File Ingestion base URL + reader secretถ้าใช้ FILE_READYFileIngestionClient ต้องมี base_url + secret_ref
S3 bucket + KMS keyไม่บังคับเฉพาะ replay snapshot (CORETH_SNAPSHOT_BUCKET) ซึ่งบังคับใช้ SSE-KMS
Cloudflare Tunnelไม่บังคับเปิด Admin ผ่าน HTTPS สาธารณะโดยไม่เปิด port บนเครื่อง
CloudWatch log groupไม่บังคับหน้า /admin/activity อ่าน log ผ่าน filter_log_events

3.1 ตรวจสิ่งที่ต้องมีด้วยสคริปต์เดียว

คัดลอกทั้งบล็อกไปวางใน shell บนเครื่องที่จะติดตั้ง ผลลัพธ์จะเป็น PASS / WARN / FAIL ต่อบรรทัด

bash · prerequisite check
# ตรวจเครื่องมือและสิทธิ์ที่ CORETH Context Processor ต้องใช้
AWS_REGION="${AWS_REGION:-ap-southeast-1}"
ok(){ printf 'PASS  %-26s %s\n' "$1" "$2"; }
wn(){ printf 'WARN  %-26s %s\n' "$1" "$2"; }
no(){ printf 'FAIL  %-26s %s\n' "$1" "$2"; }

command -v git   >/dev/null && ok git   "$(git --version)"            || no git   "ยังไม่ได้ติดตั้ง"
command -v docker >/dev/null && ok docker "$(docker --version)"         || no docker "ยังไม่ได้ติดตั้ง"
command -v python3 >/dev/null && ok python3 "$(python3 -V 2>&1)"      || no python3 "ยังไม่ได้ติดตั้ง (คู่มือนี้ใช้ในหลายขั้นตอน)"
docker compose version >/dev/null 2>&1 \
  && ok "docker compose" "$(docker compose version --short)" \
  || no "docker compose" "ต้องใช้ Compose plugin v2"
docker info >/dev/null 2>&1 && ok "docker daemon" "เข้าถึงได้" || no "docker daemon" "ผู้ใช้ปัจจุบันเรียก docker ไม่ได้"

if command -v aws >/dev/null; then
  ok "aws cli" "$(aws --version 2>&1 | cut -d' ' -f1)"
  ARN=$(aws sts get-caller-identity --query Arn --output text 2>/dev/null) \
    && ok "aws sts" "$ARN" || no "aws sts" "ไม่มี credential ที่ใช้ได้"
  aws secretsmanager list-secrets --max-results 1 --region "$AWS_REGION" >/dev/null 2>&1 \
    && ok "secretsmanager" "อ่านรายการ secret ได้ใน $AWS_REGION" \
    || no "secretsmanager" "ไม่มีสิทธิ์ list/GetSecretValue ใน $AWS_REGION"
else
  wn "aws cli" "ไม่มีบนเครื่องนี้ (container ใช้ IAM role ได้โดยไม่ต้องมี cli)"
fi

FREE=$(df -Pm . | awk 'NR==2{print $4}')
[ "$FREE" -ge 20480 ] && ok disk "เหลือ ${FREE} MB" || wn disk "เหลือ ${FREE} MB (แนะนำอย่างน้อย 20 GB)"

getent hosts api.openai.com >/dev/null 2>&1 && ok dns "api.openai.com แปลงชื่อได้" || wn dns "แปลงชื่อ api.openai.com ไม่ได้"

ผลลัพธ์ที่ควรเห็น

PASS git git version 2.43.0 PASS docker Docker version 27.3.1 PASS python3 Python 3.12.3 PASS docker compose 2.29.7 PASS docker daemon เข้าถึงได้ PASS aws cli aws-cli/2.17.0 PASS aws sts arn:aws:sts::123456789012:assumed-role/coreth-processor/i-0abc PASS secretsmanager อ่านรายการ secret ได้ใน ap-southeast-1 PASS disk เหลือ 48210 MB WARN dns แปลงชื่อ api.openai.com ไม่ได้
สิทธิ์ IAM ขั้นต่ำที่ container ต้องมี (ถอดจากทุกจุดที่เรียก boto3)
ActionResourceใช้ที่บังคับ?
secretsmanager:GetSecretValueARN ของ secret ที่อ้างถึงทุกตัวcoreth_context/secrets.pyบังคับ
kms:DecryptKMS key ที่เข้ารหัส secretผ่าน Secrets Managerถ้าใช้ CMK
s3:GetObject, s3:GetObjectVersionbucket ของ File Ingestion / S3 sourceconnectors/factory.pyถ้าใช้ FILE_READY/S3
s3:PutObject, s3:DeleteObject, s3:GetObjectCORETH_SNAPSHOT_BUCKETcoreth_data/snapshots.pyไม่บังคับ
kms:GenerateDataKey, kms:DecryptCORETH_SNAPSHOT_KMS_KEY_IDSSE-KMS ของ snapshotไม่บังคับ
logs:FilterLogEventsCORETH_ADMIN_LOG_GROUPcoreth_control/logs.pyไม่บังคับ

ระบบไม่เรียก secretsmanager:CreateSecret เอง — การสร้าง secret เป็นงานของผู้ติดตั้ง และควรใช้ identity คนละตัวกับ role ของ container

4. Install#

ทำตามลำดับ แต่ละขั้นมีคำสั่งเดียวและผลลัพธ์ที่ต้องเห็นก่อนไปขั้นถัดไป

ระบบมีตัวเลือกแบบเมนูให้อยู่แล้ว — management command เกือบทุกตัวใช้ coreth_context/operator_prompt.py เมื่อรันจาก terminal จริง (มี TTY) และยังไม่ได้ส่ง flag มันจะ ดึงค่าจริงจากฐานข้อมูลมาเป็นเมนูตัวเลข ให้เลือก ถ้ามีตัวเลือกเดียวจะเลือกให้อัตโนมัติพร้อมบอกว่าเลือกอะไร และถ้ามีค่าใน .env อยู่แล้ว จะนำมาใช้ซ้ำโดยไม่ถาม จึงไม่ต้องจำ UUID เอง

STEP 1 Clone repository

Compose ใช้ build: . จึงต้องมีซอร์สอยู่บนเครื่อง

bash
sudo mkdir -p /opt/coreth && sudo chown "$USER":"$USER" /opt/coreth
git clone git@github.com:Core-technology-Co-Ltd/ai-coreth-context-processor.git /opt/coreth/app
cd /opt/coreth/app && git rev-parse --short HEAD

ผลที่ต้องเห็น

8d2a865

STEP 2 เตรียม secret ของ OpenAI ใน Secrets Manager

โค้ดอ่าน API key จาก secret ที่เป็น JSON object และต้องมี key ชื่อ OPENAI_API_KEY (ดู semantic/credentials.py::resolve_api_key) สคริปต์นี้แสดง secret ที่มีอยู่ให้เลือกก่อน ถ้ายังไม่มีจึงสร้างใหม่โดยไม่แสดง key บนจอ

bash · เลือกหรือสร้าง secret
AWS_REGION="${AWS_REGION:-ap-southeast-1}"

# ---- ตัวช่วยเลือกจากรายการจริง ใช้ซ้ำได้ในขั้นตอนอื่น ----
select_one() {
  local title="$1" i=1 line n; local -a items=()
  while IFS= read -r line; do [ -n "$line" ] && items+=("$line"); done
  printf '\n%s\n' "$title"
  if [ "${#items[@]}" -eq 0 ]; then
    printf '  (ยังไม่มีข้อมูลในระบบ)\n'; read -r -p '  พิมพ์ค่าเอง: ' SELECTED </dev/tty; return
  fi
  for line in "${items[@]}"; do printf '  %d. %s\n' "$i" "$line"; i=$((i+1)); done
  if [ "${#items[@]}" -eq 1 ]; then
    SELECTED="${items[0]}"; printf '  เลือกอัตโนมัติ: %s\n' "$SELECTED"; return
  fi
  while :; do
    read -r -p "  เลือก [1-${#items[@]}]: " n </dev/tty
    case "$n" in ''|*[!0-9]*) ;; *)
      [ "$n" -ge 1 ] && [ "$n" -le "${#items[@]}" ] && { SELECTED="${items[$((n-1))]}"; return; };;
    esac
  done
}

# ---- 1) เลือก secret ที่มีอยู่ หรือสร้างใหม่ ----
NEW_LABEL='+ สร้าง secret ใหม่'
select_one "AWS Secrets Manager — เลือก secret ของ OpenAI" < <(
  aws secretsmanager list-secrets --region "$AWS_REGION" \
    --query 'SecretList[].Name' --output text 2>/dev/null | tr '\t' '\n' | sort
  printf '%s\n' "$NEW_LABEL"
)

# ---- 2) ถ้าเลือก "สร้างใหม่" ให้ถามชื่อและ key โดยไม่ echo ค่า ----
if [ "$SELECTED" = "$NEW_LABEL" ]; then
  read -r  -p '  ชื่อ secret ใหม่ (เช่น org/production/openai): ' SELECTED </dev/tty
  read -rs -p '  OpenAI API key (ไม่แสดงบนจอ): ' OPENAI_KEY </dev/tty; echo
  aws secretsmanager create-secret --region "$AWS_REGION" --name "$SELECTED" \
    --secret-string "$(OPENAI_KEY="$OPENAI_KEY" python3 -c \
        'import json,os;print(json.dumps({"OPENAI_API_KEY":os.environ["OPENAI_KEY"]}))')" >/dev/null
  unset OPENAI_KEY
fi

# ---- 3) ยืนยันว่า secret ที่เลือกมี key OPENAI_API_KEY จริง (ไม่พิมพ์ค่า) ----
if aws secretsmanager get-secret-value --region "$AWS_REGION" --secret-id "$SELECTED" \
     --query SecretString --output text 2>/dev/null \
   | python3 -c 'import json,sys
try: d=json.load(sys.stdin)
except Exception: sys.exit(1)
sys.exit(0 if isinstance(d,dict) and str(d.get("OPENAI_API_KEY","")).strip() else 1)'
then
  echo "PASS  $SELECTED มี key OPENAI_API_KEY"
else
  echo "FAIL  $SELECTED ไม่มี key OPENAI_API_KEY ที่ใช้ได้ — แก้ก่อนไปขั้นถัดไป"
  exit 1
fi

CORETH_AI_SECRET_REF="aws-secretsmanager://$SELECTED"
echo "CORETH_AI_SECRET_REF=$CORETH_AI_SECRET_REF"

ผลที่ต้องเห็น

AWS Secrets Manager — เลือก secret ของ OpenAI 1. thinkberry/production/file-ingestion-reader 2. thinkberry/production/openai 3. + สร้าง secret ใหม่ เลือก [1-3]: 2 PASS thinkberry/production/openai มี key OPENAI_API_KEY CORETH_AI_SECRET_REF=aws-secretsmanager://thinkberry/production/openai
ห้ามใส่ API key ลงใน .env หรือส่งเป็น --secret-string แบบพิมพ์ตรง ๆ normalize_secret_reference() ปฏิเสธค่าที่ขึ้นต้นด้วย sk- อยู่แล้ว ขั้นที่ 3 ของสคริปต์ตรวจเฉพาะว่า มี key OPENAI_API_KEY ที่ไม่ว่าง ไม่ได้ยืนยันว่า key ใช้เรียก OpenAI ได้จริง — wizard ในขั้นที่ 4 จะโหลดรายการโมเดลจริง ซึ่งเป็นการยืนยันการเข้าถึง

STEP 3 หา Customer UUID

ถ้าเป็นการติดตั้งใหม่ ให้สร้าง UUID ขึ้นมาหนึ่งค่าและใช้ค่านี้ตลอดอายุ deployment ถ้าเป็นการติดตั้งซ้ำ/ย้ายเครื่อง ให้ดึงจากระบบเดิม

ติดตั้งใหม่
bash
python3 -c 'import uuid; print(uuid.uuid4())'
มีระบบเดิมอยู่แล้ว — ดึงค่าจริงมาเลือก
bash · ต้องมี stack เดิมรันอยู่
# list_source_policies --all-customers คืน JSON ของทุก source พร้อม customer_id
docker compose exec -T api python manage.py list_source_policies --all-customers \
  | python3 -c 'import json,sys
d=json.load(sys.stdin)
for c in sorted({s["customer_id"] for s in d["sources"]}): print(c)'

ผลที่ต้องเห็น

18b3f0c2-6a41-4e7d-9c22-0f5a4b8e1d77
ค่านี้จะถูกเขียนลง .env เป็น CORETH_DEPLOYMENT_CUSTOMER_ID หลังจากนั้นทุกคำสั่งที่ต้องใช้ customer จะ หยิบค่านี้ให้อัตโนมัติ (operator_fields.customer() อ่านจาก settings.CORETH_OPERATIONS) จึงไม่ต้องพิมพ์ --customer-id อีกเลย

STEP 4 รัน setup wizard

Wizard เขียน .env แบบ atomic สิทธิ์ 600 ทดสอบการต่อฐานข้อมูลจริง และดึงรายชื่อ model จาก OpenAI มาให้เลือกเป็นเมนูตัวเลข

bash · โต้ตอบ
docker compose run --rm wizard

บทสนทนาที่จะเจอ

Organization name [CORETH]: Thinkberry Customer UUID []: 18b3f0c2-6a41-4e7d-9c22-0f5a4b8e1d77 Environment [production]: AWS region [ap-southeast-1]: Context database [local / external; local]: local Data database [local / external / same (Context DB); same]: same Enable Monitor on subsequent deploys [yes / no; yes]: yes AI provider [openai / skip; openai]: AWS Secrets Manager secret name or ARN []: thinkberry/production/openai Loading OpenAI models (metadata only; no inference request)... Choose a model supporting Responses and Structured Output. 1. gpt-4.1 2. gpt-4.1-mini 3. gpt-4o OpenAI model number [1-3; q = cancel]: 2 Saved .env (mode 600). Run docker compose up --build -d to apply web services. Start/update ingestion separately with docker compose up -d worker when idle.
คำตอบเรื่องฐานข้อมูลผลที่ wizard เขียน
Context local + Data sameฐานเดียวใน Compose · เพิ่ม profile local · CONTEXT_DATABASE_URL และ DATA_DATABASE_URL ว่าง
Context external + Data sameตั้ง CONTEXT_DATABASE_URL · ไม่เปิด profile local
Context external + Data externalตั้งทั้งสองตัวแปร · แยกสองฐานจริง · router เปิด alias data
Context local + Data externalCompose PostgreSQL สำหรับ context · Data ชี้ภายนอก
Wizard จะ ไม่ ใส่ worker ลงใน COMPOSE_PROFILES โดยเจตนา เพื่อไม่ให้การ deploy เว็บตามปกติไปรีสตาร์ต ingestion ที่กำลังทำงานอยู่ (ดูคอมเมนต์ใน compose.yaml และ tunnel_preflight.EXCLUDED_PROFILES)

STEP 5 Build และเริ่ม service ฝั่งเว็บ

คำสั่งนี้รัน setup → migrate (migrate ทุกฐาน + เปิด RLS) → api → caddy และ postgres/admin ตาม profile

bash
docker compose up --build -d && docker compose ps

ผลที่ต้องเห็น

NAME SERVICE STATUS coreth-context-processor-admin-1 admin Up 12 seconds coreth-context-processor-api-1 api Up 12 seconds (healthy) coreth-context-processor-caddy-1 caddy Up 10 seconds coreth-context-processor-migrate-1 migrate Exited (0) coreth-context-processor-postgres-1 postgres Up 25 seconds (healthy) coreth-context-processor-setup-1 setup Exited (0)

ตรวจว่า migration ทำงานครบและ RLS ถูกเปิด:

bash
docker compose logs migrate --tail=6
default: protected 61 managed tables Applying coreth_data.0018_openwebui_archive... OK

STEP 6 ตรวจ liveness ผ่าน Caddy

api ไม่เปิด port เอง ทางเข้าคือ caddy ที่ 127.0.0.1:8080

bash
curl -sS http://127.0.0.1:"${CORETH_HTTP_PORT:-8080}"/health/live; echo
{"status": "alive", "ready": false, "phase": "FOUNDATION"}
/health/live ตอบ ready:false เสมอโดยเจตนา — เป็น liveness probe ของ process เท่านั้น ความพร้อมจริงอยู่ที่ /health/ready ซึ่งจะยังไม่ผ่าน จนกว่าจะเริ่ม worker ในขั้นที่ 8

STEP 7 สร้างผู้ดูแลระบบคนแรก

bootstrap_root_user สร้าง Django superuser พร้อม AdminMembership บทบาท ADMIN ผูกกับ customer + environment ของ deployment นี้ (ถ้าไม่ส่ง flag จะใช้ค่าจาก .env ให้อัตโนมัติ)

bash · เมื่อเปิด Monitor (profile admin หรือ local)
docker compose exec admin python manage.py bootstrap_root_user
bash · เมื่อไม่ได้เปิด Monitor
docker compose run --rm ops python manage.py bootstrap_root_user

ผลที่ต้องเห็น

Admin email: ops@thinkberry.co.th Password: Password (again): created root user root; membership=6c1d9a0e-3b52-4f0a-9c77-2ad51f8e4b31

เปลี่ยนรหัสผ่านภายหลังด้วย docker compose exec admin python manage.py changepassword root

STEP 8 เริ่ม worker

worker คือสิ่งที่ทำให้ระบบ "มีชีวิต" — นำเข้า ประมวลผล project และประเมิน readiness ต้องสั่งแยกเสมอ และควรสั่งตอนที่ไม่มีงานค้างเพื่อไม่ตัดงานที่กำลังทำ

bash
docker compose up -d worker && docker compose logs --tail=5 worker

ผลที่ต้องเห็น

{"dispatched": 0, "polled": 0, "processed": 0, "projected": 0, "reconciled": 0, "review_jobs": []}
อย่าใส่ worker หรือ * ลงใน COMPOSE_PROFILES และอย่า deploy ด้วย --profile '*' — การ up ตามปกติจะไป recreate worker และตัด ingestion ที่กำลังทำงานอยู่กลางคัน

STEP 9 เปิด Admin Monitor

โหมดURLเงื่อนไข
เฉพาะเครื่องhttp://127.0.0.1:8081/admin/profile admin หรือ local
ผ่าน Cloudflarehttps://<CORETH_PUBLIC_HOST>/admin/profile cloudflare + overlay
ผ่าน Caddy :8080ใช้ไม่ได้deploy/Caddyfile ส่งทุก path ไป api ซึ่งไม่เปิด CORETH_ADMIN_ENABLED

เข้าจากเครื่องอื่นผ่าน SSH tunnel:

bash · รันบนเครื่องของคุณ
ssh -N -L 8081:127.0.0.1:8081 ubuntu@YOUR_HOST
STEP 10 (ไม่บังคับ) · เปิด HTTPS สาธารณะด้วย Cloudflare Tunnel

Overlay นี้ไม่เปิด port ใด ๆ บนเครื่อง ทราฟฟิกวิ่งออกจาก cloudflared ไป Cloudflare แล้วย้อนเข้ามาที่ cloudflare-origin (Caddy อีกตัว) ซึ่ง route /admin* และ API ของหน้า Admin ไป admin:8000

bash
# 1) สร้าง tunnel + public hostname ใน Cloudflare Zero Trust dashboard
#    ตั้ง service ปลายทางเป็น http://cloudflare-origin:8080

# 2) เติมค่าลง .env (hostname เปล่า ๆ ห้ามมี https:// หรือ path)
printf 'COMPOSE_FILE=compose.yaml:compose.cloudflare.yaml\n' >> .env
printf 'CORETH_PUBLIC_HOST=context.example.com\n' >> .env
read -rs -p 'Cloudflare tunnel token: ' T; echo; printf 'CLOUDFLARE_TUNNEL_TOKEN=%s\n' "$T" >> .env; unset T
# เพิ่ม profile cloudflare เข้า COMPOSE_PROFILES ที่มีอยู่ (อย่าลบ local/admin เดิม)

# 3) ตรวจแล้ว deploy
docker compose config --quiet && docker compose up -d --build

ตรวจผล

bash
docker compose logs --tail=20 cloudflared | grep -i "registered tunnel connection"
coreth_context/tunnel_preflight.py จะตรวจสองค่านี้ทุกครั้งที่รัน wizard และถ้าเคย deploy สำเร็จมาก่อน มันจะ กู้ค่ากลับมาเอง จาก container ที่รันอยู่ (docker inspect coreth-context-processor-cloudflared-1) จึงไม่ต้องไปหา token ใน dashboard ใหม่

5. Configuration#

การตั้งค่าทั้งหมดผ่านตัวแปรสภาพแวดล้อมใน .env ที่ราก repository compose.yaml ประกาศ anchor &app-env แล้วทุก service ใช้ซ้ำด้วย *app-env ตัวแปรที่ไม่ปรากฏใน anchor นั้นจะไม่ถูกส่งเข้า container

5.1 บังคับต้องมี

ตัวแปรทำอะไรตัวอย่าง / วิธีหาค่า
CORETH_DEPLOYMENT_CUSTOMER_IDcustomer ของ deployment นี้ · /health/ready ใช้ตัวนี้หา readiness · operator command ใช้เป็นค่าเริ่มต้นwizard ถาม / python3 -c 'import uuid;print(uuid.uuid4())'
DJANGO_SECRET_KEYเซ็น session และ CSRFwizard สุ่มให้ (token_urlsafe(48))
POSTGRES_PASSWORDรหัสของ Compose PostgreSQL และประกอบเป็น DATABASE_URLwizard สุ่มให้ (token_urlsafe(32))
AWS_REGIONregion ของ Secrets Manager / S3 / CloudWatchap-southeast-1
CORETH_ENVIRONMENTแยกขอบเขต data, grant และ AdminMembershipproduction
CORETH_AI_SECRET_REFsecret ที่มี key OPENAI_API_KEY ใช้เป็นค่าเริ่มต้นของ source ใหม่aws-secretsmanager://org/production/openai
CORETH_AI_MODELโมเดลเริ่มต้น · wizard ให้เลือกจากรายการจริงของ providergpt-4.1-mini
COMPOSE_PROFILESเลือก service ที่จะขึ้น (local, admin, cloudflare)wizard ตั้งให้ · ห้ามใส่ worker หรือ *

5.2 ฐานข้อมูล

ตัวแปรเมื่อว่างเมื่อมีค่า
CONTEXT_DATABASE_URLใช้ DATABASE_URL ของ Compose (ต้องเปิด profile local)alias default ชี้ไปฐานภายนอก · conn_max_age=0
DATA_DATABASE_URLไม่มี alias data → coreth_data ใช้ฐานเดียวกับ contextเปิด alias data แยกจริง · router แยกทุก model ของ coreth_data
ใช้ SSL และ URL-encode รหัสผ่านเสมอสำหรับฐานภายนอก setup_context.check_database() จะต่อจริงด้วย psycopg และ SELECT 1 ก่อนบันทึก ถ้าต่อไม่ได้ wizard จะไม่เขียน .env

5.3 นโยบายการประมวลผลและอนุมัติ

ตัวแปรค่าเริ่มต้นผลจริงในโค้ด
CORETH_AGENTIC_ONLYtruesignal ของ source ที่ไม่ใช่ AI_REQUIRED จะ fail ด้วย AGENTIC_REQUIRED · ปิดเส้นทาง streaming เดิม
CORETH_FILE_AGENT_ENABLEDtrueเปิด File Agent สำหรับ FILE_READY และ OPENWEBUI_UPLOAD
CORETH_FILE_AGENT_MAX_BYTES5242880ไฟล์ใหญ่กว่านี้ fail ด้วย AI_FILE_TOO_LARGE (ไม่ตัดทิ้งเงียบ ๆ)
CORETH_FILE_AGENT_MAX_PAGES10PDF เกินหน้านี้หรือถูกเข้ารหัส fail ด้วย AI_PDF_LIMIT_OR_ENCRYPTED
CORETH_AUTO_APPROVE_NEW_DATAtrueอนุญาต auto-approve เฉพาะ แถวใหม่ และเฉพาะไฟล์ที่ไม่ใช่ human upload
CORETH_AUTO_APPROVE_ADDITIVE_SCHEMAtrueอนุมัติอัตโนมัติได้เฉพาะการเพิ่ม field ที่ไม่บังคับ
CORETH_REVIEW_PAUSE_APPLYfalseสวิตช์หยุดทั้งหมด — run_review_jobs() return ทันที ไม่มีการเผยแพร่ข้อมูลหรือ schema
CORETH_AI_ENRICHMENT_ENABLEDtrueให้ AI เติมค่าลงแถว app_output ที่มีอยู่จาก PDF/ข้อความ (fill-only)
CORETH_COMPACT_RECORDStrueเก็บรายละเอียดลงไฟล์ใน record-artifacts แทนการสะสมแถวในฐานข้อมูล
CORETH_OCR_ALLOWEDfalseเปิด TesseractOcrEngine (image มี tesseract tha+eng ติดตั้งไว้แล้ว)

5.4 ค่าที่เป็น secret reference (ห้ามใส่ค่าจริง)

ตัวแปรต้องมี key อะไรใน secretอ่านโดย
CORETH_AI_SECRET_REFOPENAI_API_KEYsemantic/credentials.py
OPENAI_API_KEY_REFOPENAI_API_KEYruntime_secrets.py ตอน entrypoint
CONTEXT_CHANGED_WEBHOOK_SECRET_REFbearer_tokenruntime.build_dispatcher()
CORETH_BACKUP_KEY_SECRET_REFคีย์เข้ารหัสสำรองข้อมูลoperations/backup.py
CORETH_OPENWEBUI_ARCHIVE_SECRET_REFissuer, audience, subject, kid, private_keyopenwebui_archive.py → FileIngestionClient
secret ของ source (secret_ref ใน source config)ขึ้นกับ connector — เช่น FILE_READY ต้องมี file_ingestion.secret_ref พร้อม JWT keyconnectors/factory.py
ตั้ง OPENAI_API_KEY (ค่าจริง) พร้อมกับ OPENAI_API_KEY_REF ไม่ได้ — entrypoint จะจบด้วย exit 78 และ container ไม่ขึ้น แนะนำให้ใช้ _REF เสมอในสภาพแวดล้อมจริง

5.5 ค่าที่ wizard สร้างหรือจัดการให้

ตัวแปรที่มา
DJANGO_SECRET_KEY, POSTGRES_PASSWORDสุ่มใหม่เมื่อยังว่างหรือขึ้นต้นด้วย replace-with-
COMPOSE_PROFILESคำนวณจากคำตอบเรื่องฐานข้อมูลและ Monitor
CONTEXT_DATABASE_URL, DATA_DATABASE_URLถามแบบซ่อนข้อความ (getpass) และทดสอบต่อจริงก่อนบันทึก
CORETH_PUBLIC_HOST, CLOUDFLARE_TUNNEL_TOKENtunnel_preflight กู้จาก container ที่รันอยู่ หรือถามใหม่
CORETH_AI_PROVIDER, CORETH_AI_SECRET_REF, CORETH_AI_MODELเลือกจากรายการโมเดลจริงของ provider

5.6 ไม่มีผลเมื่อรันด้วย Docker Compose

ตัวแปรกลุ่มนี้อยู่ใน .env.advanced.example และถูกอ่านใน config/settings.py แต่ ไม่ได้อยู่ในบล็อก &app-env ของ compose.yaml จึงไปไม่ถึง container การแก้ใน .env จึงไม่มีผลใด ๆ

ตัวแปรสาเหตุถ้าต้องเปลี่ยนจริงต้องทำอย่างไร
DJANGO_DEBUGcompose ตรึงเป็น "false"แก้ compose.yaml หรือ override file
DATABASE_URLcompose ประกอบเองจาก POSTGRES_PASSWORDใช้ CONTEXT_DATABASE_URL แทน
CORETH_BACKUP_DIRECTORYcompose ตรึงเป็น /var/lib/coreth/backups (ผูกกับ volume)เปลี่ยน mount ของ volume backup-data
CORETH_LOG_LEVELไม่ได้ส่งต่อเพิ่มบรรทัดใน &app-env ถ้าต้องการ
DATABASE_CONN_MAX_AGEไม่ได้ส่งต่อ · ค่าจริงคือ 0เพิ่มใน &app-env
CORETH_APPLICATION_VERSION, CORETH_SCHEMA_VERSION, CORETH_TAXONOMY_VERSION, CORETH_PARSER_VERSION, CORETH_PROVIDER_CONTRACT_VERSIONไม่ได้ส่งต่อ · ใช้ค่า default ใน settings.py เสมอปกติไม่ควรเปลี่ยน — เป็นค่าที่ Doctor รายงาน
DJANGO_SETTINGS_MODULEimage ตั้งไว้แล้ว—
ค่าปรับแต่งอื่น ๆ ที่ส่งถึง container จริง (ดู .env.advanced.example)
กลุ่มตัวแปรค่าเริ่มต้น
ขีดจำกัดตัวแยกไฟล์CORETH_PARSER_MAX_BYTES · _MAX_PAGES · _MAX_ROWS · _MAX_ARCHIVE_ENTRIES · _MAX_UNCOMPRESSED_BYTES · _MAX_COMPRESSION_RATIO · _MAX_PROCESSING_SECONDS25 MB · 300 · 100000 · 10000 · 200 MB · 100 · 60 s
รอบ workerWORKER_POLL_SECONDS · CORETH_SYNC_BATCH_SIZE · CORETH_RECONCILIATION_INTERVAL_SECONDS2 · 100 · 3600
ReadinessCORETH_DOCTOR_INTERVAL_SECONDS · CORETH_DOCTOR_MAX_AGE_SECONDS · CORETH_WORKER_HEARTBEAT_MAX_AGE_SECONDS60 · 300 · 120
Provider (Service JWT)CORETH_SERVICE_JWT_ISSUER · _AUDIENCE · _PUBLIC_KEYS_JSON · _MAX_TTL_SECONDS · _CLOCK_SKEW_SECONDS— · — · {} · 300 · 30
Context SliceCORETH_SLICE_DEFAULT_LIMIT · _MAX_ENTITIES · _MAX_FACTS · _MAX_RELATIONSHIPS · _MAX_EVIDENCE_REFS50 · 100 · 500 · 250 · 500
Data Provider APICORETH_DATA_DEFAULT_PAGE_SIZE · _MAX_PAGE_SIZE · _EVENT_RETENTION_DAYS100 · 1000 · 30
SnapshotCORETH_SNAPSHOT_BUCKET · _PREFIX · _KMS_KEY_ID · _MAX_BYTES · _RETENTION_DAYSว่าง · coreth-input-snapshots · ว่าง · 512 MB · 30
สำรองข้อมูลCORETH_BACKUP_INTERVAL_SECONDS · _RETENTION_DAYS · _MAX_AGE_SECONDS · CORETH_RESTORE_AFTER_BACKUP · CORETH_RESTORE_REHEARSAL_MAX_AGE_SECONDS86400 · 30 · 129600 · true · 604800
Monitor logsCORETH_ADMIN_LOG_GROUP · _STREAM_PREFIX · _LIMIT · CORETH_ADMIN_PORTว่าง · ว่าง · 100 (สูงสุด 500) · 8081

6. Source / Connector Setup#

Source configuration เป็น immutable — ทุกการแก้ไขสร้าง config_version ใหม่ และ latest_source_configurations() จะเลือกเฉพาะเวอร์ชันสูงสุดมาทำงาน ประวัติเดิมยังอยู่ครบเพื่อสืบย้อน lineage

6.1 File Ingestion (FILE_READY) IMPLEMENTED

ช่องทางหลักของไฟล์ที่เข้ามาอัตโนมัติ (Gmail, LINE, Web, API upload ฯลฯ) Context Processor ไม่ได้ต่อเข้ากล่องอีเมลเอง — มันคุยกับ File Ingestion service เท่านั้น

สิ่งที่ต้องเตรียมก่อน

  • HTTPS base URL ของ File Ingestion — ต้องพิมพ์เอง (เป็นบริการภายนอก ระบบค้นหาให้ไม่ได้)
  • Secret ของ service reader ใน Secrets Manager ที่มี key: issuer, audience, subject, kid, private_key และ (ไม่บังคับ) algorithm ค่าเริ่มต้น RS256
  • สิทธิ์ file:read และ events:read ฝั่ง File Ingestion

ตั้งค่าผ่าน Admin (แนะนำ)

  1. เปิด /admin/setup ด้วยบัญชีบทบาท ADMIN
  2. เลือกประเภท File Ingestion แล้วกรอก base URL + secret reference
  3. กดค้นหา — ระบบเรียก inventory จริงและ แสดงรายชื่อแหล่งข้อมูลที่พบให้เลือก (ingestion_source_options() แปลง source_type เป็นชื่อที่อ่านออก เช่น “Gmail — ฝ่ายขาย”) พร้อมเดา bucket ให้จาก inventory
  4. เลือกโหมด schema: AI จัดการอัตโนมัติ หรือ กำหนด fields เอง
  5. เลือกโมเดลจากปุ่มโหลดรายการ (/admin/setup/models) แล้วบันทึก
ถ้าไม่กรอก Source UUID ระบบจะเลือกให้อัตโนมัติเมื่อ inventory มีแหล่งเดียวที่ใช้ได้ ถ้ามีมากกว่าหนึ่งจะขึ้นข้อความให้เลือกเอง — เป็นพฤติกรรมเดียวกับเมนู CLI

ตั้งค่าผ่าน CLI (สำหรับ automation)

bash · รันแบบมี TTY เพื่อให้ขึ้นเมนู
docker compose exec api python manage.py configure_source \
  --source-type FILE_READY \
  --settings-file /app/examples/file-ingestion/thinkberry.json
ไฟล์ตัวอย่างใน examples/file-ingestion/ ยังตั้ง "semantic_processing": {"mode": "DETERMINISTIC_ONLY"} ซึ่ง ใช้ไม่ได้แล้ว เมื่อ CORETH_AGENTIC_ONLY=true — ต้องแก้เป็น "mode": "AI_REQUIRED" พร้อม allow_external_model และ secret_ref ก่อนใช้จริง

พฤติกรรมเมื่อไม่ส่ง flag ครบ

Required: ✓ Customer: 18b3f0c2-6a41-4e7d-9c22-0f5a4b8e1d77 ? Source id ? Approved scope ? Secret ref ? Source id Example: sales-mail Value: _

ตรวจว่าเชื่อมต่อได้จริง

bash
docker compose exec -T api python manage.py list_source_policies --issues-only
{"customer_id": "18b3f0c2-…", "raw_file_types": ["application/pdf", "image/*"], "sources": []}

sources: [] = ไม่มี source ที่ policy ผิดพลาด ปิดกั้นไฟล์ดิบ หรือมี last_error_code ค้าง

นำไฟล์เดิมทั้งหมดเข้าครั้งแรก

onboard_file_ingestion ดึง inventory ทั้งหมด ตาม event ให้ทัน ประมวลผล และ project จนหมด — รันซ้ำได้ ไม่ทำงานซ้ำซ้อน เพราะมี FileIngestionCursor

bash · ควรหยุด worker ก่อนเพื่อลดการแย่งงาน
docker compose exec api python manage.py onboard_file_ingestion
{"status": "IMPORTED", "cycles": 37, "note": "Run context_doctor and validate source totals"}
ถ้าได้ ONBOARDING_FAILED ให้ดู SourceSignal.error_code ที่หน้า /admin/ingestion-history · ถ้าได้ ONBOARDING_INCOMPLETE แปลว่าครบ --max-cycles (ค่าเริ่มต้น 1000) ให้รันซ้ำเพื่อทำต่อจากเดิม

6.2 OpenWebUI direct upload (OPENWEBUI_UPLOAD) IMPLEMENTED

ผู้ใช้อัปโหลดไฟล์จาก OpenWebUI เข้ามาโดยตรงผ่าน POST /api/v1/submissions ด้วย Service JWT แล้วตรวจทานผลผ่าน /api/v1/openwebui/submissions ทุกไฟล์กลุ่มนี้ต้องมีคนกดยืนยันเสมอ ไม่มีเส้นทาง auto-approve

#สิ่งที่ต้องทำเครื่องมือ
1มีศูนย์ (Center) และ source ต้นแบบที่ผูกกับศูนย์นั้นอยู่แล้ว/admin/centers หรือ configure_review_scope
2สร้าง source แบบ push โดยคัดลอกการตั้งค่าประมวลผลจาก source ต้นแบบconfigure_openwebui_source
3ผูก OpenWebUI user id เข้ากับศูนย์ (ไม่สร้าง login ในระบบนี้)configure_openwebui_reviewer (ถูกเรียกอัตโนมัติจากขั้นที่ 2)
4ออก Service grant ให้ toolmanage_tool_grant configure
5ตั้ง public key ของผู้ออก JWTCORETH_SERVICE_JWT_PUBLIC_KEYS_JSON

สร้าง source และผูกผู้ใช้ — เลือกจากรายการจริง

คำสั่งนี้ถามสามอย่าง: source ต้นแบบ, ศูนย์ และ OpenWebUI user id สองอย่างแรกมาเป็นเมนูตัวเลขจากฐานข้อมูล ส่วน user id ต้องพิมพ์เองเพราะเป็นตัวตนของระบบภายนอก

bash · ต้องมี TTY (ไม่ใส่ -T)
docker compose exec api python manage.py configure_openwebui_source

บทสนทนาจริง

Required: ? Template source ? Center ? OpenWebUI user id ? Template source 1. sales-mail (FILE_READY) [7a41c0de-2b19-4f5e-9d33-6c02a8be5417] 2. stockroom-line (FILE_READY) [c93f2a70-58e1-4d6b-b0aa-19f4d3e7c821] Select [1-2]: 1 ? Center 1. Central Rama 9 (center_id=41d0c7b2-9e35-4a18-8f6c-b2705d1e9a34) 2. Thonglor (center_id=88e5b1a4-70cd-4c92-a3f1-5d6e2c908b47) Select [1-2]: 1 ? OpenWebUI user id Example: 0b6d1c1e-2f3a-4b5c-8d9e-1f2a3b4c5d6e Value: 2f9c5b81-43ad-4e70-9a12-cb8d60e4f735 { "2f9c5b81-43ad-4e70-9a12-cb8d60e4f735": { "identity_provider": "openwebui", "customer_id": "18b3f0c2-6a41-4e7d-9c22-0f5a4b8e1d77", "center_id": "41d0c7b2-9e35-4a18-8f6c-b2705d1e9a34", "source_id": "d7104e69-1c2b-4a85-9f30-7ea5b6c418d2", "environment": "production" } }

นำ JSON ที่ได้ไปตั้งเป็น user map ฝั่ง OpenWebUI เพื่อให้ออก JWT ที่มี claim ตรงกัน

ออก Service grant

bash · ไม่ใส่ operation จะมีเมนูให้เลือก configure / revoke
docker compose exec api python manage.py manage_tool_grant
? Operation 1. configure 2. revoke Select [1-2]: 1 Customer: 18b3f0c2-6a41-4e7d-9c22-0f5a4b8e1d77 (reused) Environment: production (reused) ? Tool type Example: OPENWEBUI Value: openwebui ? Tool instance id Example: openwebui-main Value: openwebui-main
OpenWebUI ฝั่ง client ใช้ Filter ที่อยู่ใน repo นี้: integrations/openwebui/submit_filter.py (ติดตั้งเป็น global Filter) มันอ่าน credential และ user map จากไฟล์/ตัวแปรที่ผู้ดูแลตั้งไว้เท่านั้น ไม่รับค่าจากข้อความแชทหรือ argument ของโมเดล
Grant กำหนด allowed_scopes (เช่น submissions:read, submissions:write) และ allowed_actions ทุก request ต้องมี scope ครบทั้งใน token และใน grant มิฉะนั้นได้ SUBMISSION_GRANT_REQUIRED

Service JWT ที่ระบบยอมรับ

ข้อกำหนดค่า
AlgorithmEdDSA, RS256 หรือ ES256 เท่านั้น และต้องมี kid
Claim ที่บังคับiss, aud, sub, customer_id, tool_type, tool_instance_id, scopes, action, iat, exp, jti
Issuer / Audienceต้องตรงกับ CORETH_SERVICE_JWT_ISSUER / _AUDIENCE
อายุสูงสุดCORETH_SERVICE_JWT_MAX_TTL_SECONDS (300 วินาที)
Public keyอยู่ใน CORETH_SERVICE_JWT_PUBLIC_KEYS_JSON เป็น map {"<kid>": "<PEM>"}

ทดสอบหนึ่งไฟล์แบบครบวงจร

  1. อัปโหลดไฟล์จาก OpenWebUI (ระบบสร้าง Submission + OpenWebUIUploadReceipt)
  2. รอ worker รัน ReviewJob(ANALYZE) — ดูสถานะที่ /admin/submissions
  3. ตรวจแถวที่เสนอ แก้ค่าที่ผิด แล้วกดยืนยัน
  4. ตรวจข้อมูลที่เผยแพร่แล้วที่ /admin/data-explorer
bash · ดูงานที่ค้างโดยไม่เปลี่ยนแปลงอะไร
docker compose exec api python manage.py review_new_data
ส่งไฟล์ดิบของ OpenWebUI กลับไปเก็บที่ File Ingestion (ไม่บังคับ)

เมื่อผู้ใช้อัปโหลดไฟล์ตรงจาก OpenWebUI ไฟล์ดิบจะอยู่ในตัว Context Processor ฟีเจอร์นี้ส่งไฟล์นั้นกลับไปเก็บที่ File Ingestion เพื่อให้ไฟล์ทุกใบ ไม่ว่ามาทางไหน อยู่ภายใต้ lifecycle และ trust state เดียวกัน

ขั้นที่ 1 · ให้สิทธิ์ฝั่ง File Ingestion

การอัปโหลดใช้ action file:archive-openwebui ซึ่งต้องถูกอนุญาตไว้ที่ File Ingestion ก่อน มิฉะนั้นทุกครั้งจะได้ ARCHIVE_HTTP_403 คำสั่งนี้ รันบนเครื่อง File Ingestion ไม่ใช่เครื่อง Context Processor

bash · รันบนเครื่อง File Ingestion
docker exec -it coreth-file-ingestion-api-1 \
  python manage.py configure_openwebui_archive \
  --subject service:context-processor
ค่าที่ส่งให้ --subject ต้อง ตรงกับฟิลด์ subject ใน secret ที่ CORETH_OPENWEBUI_ARCHIVE_SECRET_REF ชี้อยู่ เพราะ archive_receipt() ใส่ค่านั้นเป็น claim sub ของ JWT ทุกใบ ถ้าสองค่าไม่ตรงกัน File Ingestion จะปฏิเสธแม้จะให้สิทธิ์ไว้แล้ว

subject เดียวกันนี้ต้องมีสิทธิ์ สองชุด เพราะการ archive หนึ่งครั้งเรียก File Ingestion สองขา:

ขาEndpointAction ใน JWTใช้ทำอะไร
อัปโหลด POST /v1/openwebui/archives file:archive-openwebui ส่งไฟล์ดิบ (multipart) · ต้องได้ HTTP 202 และ JSON ที่มี file_version_id
ตรวจซ้ำ GET /v1/file-versions/{id} file:read, events:read ยืนยันว่าไฟล์ถูกรับจริง · ตรวจ provenance และ sha256 ก่อนปิดเป็น COMPLETED
ถ้าให้เฉพาะ file:archive-openwebui แต่ลืม file:read ไฟล์จะอัปโหลดขึ้นไปได้ แต่ขาตรวจซ้ำจะล้มเสมอ ทำให้ receipt ค้างอยู่ที่ PROCESSING / RETRY วนไม่จบ

ขั้นที่ 2 · เตรียม secret

secret ต้องเป็น JSON object ที่มีคีย์เหล่านี้ (โครงเดียวกับ service reader ของ FILE_READY) และ subject ต้องตรงกับที่ให้สิทธิ์ไว้ในขั้นที่ 1

โครงสร้าง secret
{
  "issuer": "https://context-processor.internal",
  "audience": "coreth-file-ingestion",
  "subject": "service:context-processor",
  "kid": "cp-archive-2026-01",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
  "algorithm": "RS256"
}

algorithm ไม่บังคับ ค่าเริ่มต้นคือ RS256 · token มีอายุ 120 วินาที และมี jti ใหม่ทุกใบ

ขั้นที่ 3 · เปิดใช้งานฝั่ง Context Processor

ตั้งสองค่าพร้อมกันเท่านั้นจึงเปิดใช้งาน ถ้าเว้นตัวใดตัวหนึ่งว่าง ฟีเจอร์นี้จะปิดเงียบ ๆ

.env
CORETH_OPENWEBUI_ARCHIVE_URL=https://files.example.com
CORETH_OPENWEBUI_ARCHIVE_SECRET_REF=aws-secretsmanager://org/production/file-ingestion-writer

URL ต้องเป็น https ล้วน ไม่มี path, query, fragment หรือ credential (ARCHIVE_INVALID_URL) การโอนย้ายทำโดย worker รอบปกติผ่าน reconcile_openwebui_archives() ซึ่งถูกเรียกท้าย run_review_jobs() ทุกรอบ ไม่ต้องตั้ง cron แยก

ขั้นที่ 4 · ตรวจผล

สถานะอยู่ที่ฟิลด์ archive_state ของ OpenWebUIUploadReceipt เฉพาะ submission ที่ COMPLETED แล้วเท่านั้นที่เข้าคิว archive

archive_stateความหมาย
PENDINGรอส่งครั้งแรก
PROCESSINGFile Ingestion รับไฟล์แล้ว (ได้ 202) แต่ยังไม่ consumable · ตรวจซ้ำทุก 60 วินาที
RETRYล้มชั่วคราว · backoff 30 × 2^attempts สูงสุด 3600 วินาที
COMPLETEDไฟล์ consumable และ sha256 ตรงกับตอนอัปโหลด
BLOCKEDไฟล์ถูกปฏิเสธ/กักกัน/ปิด lifecycle ที่ปลายทาง · ต้องสั่งลองใหม่เอง

ปกติไม่ต้องสั่งอะไร worker จะลองใหม่ให้เองตาม backoff สองคำสั่งข้างล่างใช้เมื่อต้องการเร่งให้เกิดขึ้นเดี๋ยวนี้ ทั้งคู่รันบนเครื่อง Context Processor และอ่าน customer UUID จาก .env ผ่าน environment ของคอนเทนเนอร์ จึงไม่ต้องพิมพ์ UUID เอง

bash · บังคับให้ลองใหม่ทันที (PENDING / RETRY)
docker exec coreth-context-processor-worker-1 python manage.py shell -c '
import os
from coreth_data.models import OpenWebUIUploadReceipt
from coreth_data.openwebui_archive import reconcile_openwebui_archives

customer = os.environ["CORETH_DEPLOYMENT_CUSTOMER_ID"]   # มาจาก .env ผ่าน compose
assert customer, "CORETH_DEPLOYMENT_CUSTOMER_ID ว่างในคอนเทนเนอร์"

pending = OpenWebUIUploadReceipt.all_objects.filter(
    customer_id=customer,
    submission__status="COMPLETED",
    revoked_at__isnull=True,
    archive_state__in=["PENDING", "RETRY"],
)
print("Queued:", pending.update(archive_next_attempt_at=None))
reconcile_openwebui_archives(limit=20)
'

ผลที่ต้องเห็น

Queued: 3

archive_next_attempt_at=None คือการล้างนาฬิกา backoff ให้รายการที่ค้างอยู่ ถูกหยิบทันทีในรอบนี้ ไม่ได้ข้ามเงื่อนไขอื่น — receipt ที่ถูก revoke หรือ submission ที่ยังไม่ COMPLETED ยังไม่ถูกแตะ · ตัวกรองนี้ครอบทุก environment ของ customer นั้น ถ้าใช้ฐานเดียวร่วมกับ staging ให้เพิ่ม environment="production" ใน filter

bash · ปลดล็อกรายการที่ติด BLOCKED
# BLOCKED ไม่ถูกหยิบเองอีก ต้องรีเซ็ตกลับเป็น PENDING ก่อน
# sh -c + single quote: ตัวแปรถูกขยายในคอนเทนเนอร์ ไม่ใช่บน host
docker exec coreth-context-processor-worker-1 sh -c 'python manage.py reconcile_openwebui_archives \
  --retry-blocked --customer-id "$CORETH_DEPLOYMENT_CUSTOMER_ID"'
Queued 2 blocked archives for retry Archive reconciliation cycle finished

สิ่งที่ Context Processor ส่งไปและตรวจกลับ

ขาอัปโหลดส่ง form fields: instance_id, user_id, email, submission_id, processor_source_id, environment พร้อมไฟล์ในฟิลด์ file ขาตรวจซ้ำบังคับว่า source_provenance ของ file version ต้องมีค่าตรงกันทุกช่อง มิฉะนั้นได้ ARCHIVE_REFERENCE_MISMATCH

ฟิลด์ใน source_provenanceต้องเท่ากับ
processor_submission_idSubmission ที่เป็นต้นทาง
processor_source_idsource ของ submission นั้น
openwebui_user_idส่วนท้ายของ actor_id หลัง openwebui:<instance>:
openwebui_instance_idreceipt.instance_id
processor_environmentreceipt.environment
archive_sha256checksum ของไฟล์ตอนอัปโหลดเข้า Processor

6.3 Connector อื่น ๆ ที่มีโค้ดแต่ไม่มีใน Admin UI

S3, GOOGLE_DRIVE, POSTGRESQL, MYSQL, MARIADB, REST มี adapter ครบใน coreth_context/connectors/ และตั้งค่าได้ด้วย manage.py configure_source --source-type … แต่หน้า /admin/setup เปิดให้เลือกแค่ FILE_READY กับ OPENWEBUI_UPLOAD

ก่อนใช้กลุ่มนี้ ต้องตั้ง semantic_processing.mode = AI_REQUIRED ใน source config มิฉะนั้น CORETH_AGENTIC_ONLY=true จะทำให้ทุก signal fail ด้วย AGENTIC_REQUIRED ตรวจได้ด้วย list_source_policies

6.4 การหยุด / เปิดใหม่ / เก็บเข้าคลัง

ต้องการทำที่ผล
หยุด source ชั่วคราวPOST /admin/setup/<source_id>/pauseสร้าง SourcePause · งานที่เริ่มไปแล้วทำต่อจนจบ · signal ใหม่และ reconcile ถูกข้ามจนกว่าจะ resume
เปิดใหม่POST /admin/setup/<source_id>/resumeลบ pause
เก็บเข้าคลัง (เลิกใช้ถาวร)POST /admin/setup/archiveสร้าง ArchivedSource โดยไม่แก้ source config เดิม · signal ใหม่ถูกข้าม
ปลดล็อกหลังเครดิต AI หมดmanage.py resume_aiไม่ใส่ flag = แสดงรายการ hold ทั้งหมดเป็น JSON · ใส่ --source-id + --customer-id เพื่อปลดล็อก
หยุดงานที่ค้างกลางคันPOST /admin/source-signals/<id>/force-stopเขียนไฟล์ stop ใน record-artifacts/signal-control/ และฆ่า child process ของ signal นั้น (ต้องมีบทบาท OPERATOR ขึ้นไป)

7. Validation / Doctor / Health#

ระบบมีกลไกตรวจความพร้อมของตัวเองอยู่แล้ว ใช้มันแทนการเดา ContextDoctor ประเมินจากข้อเท็จจริงที่บันทึกไว้ในฐานข้อมูลเท่านั้น ผลลัพธ์จึงทำซ้ำได้ (deterministic)

7.1 ระดับความพร้อม

Endpoint / คำสั่งบอกอะไรค่าที่ถือว่าผ่าน
GET /health/liveprocess ยังอยู่ (liveness ของ container)HTTP 200 · "phase":"FOUNDATION" · ready เป็น false เสมอ
GET /health/readyreadiness ที่ Doctor บันทึกไว้ล่าสุดHTTP 200 · "ready":true · "phase":"READY"
manage.py context_statusสรุป source + Initial Build + อายุผล Doctor"ready": true
manage.py context_doctorรันตรวจทุกข้อและบันทึกผลใหม่"status":"PASS"
manage.py structured_data_statusสถานะ dataset และเวอร์ชันล่าสุดที่ปลอดภัยมี latest_ready_version ทุก dataset ที่ใช้งาน
/admin/server-statusปุ่มทดสอบ Context DB / Data DB / AI / File Ingestion แบบสดขึ้น “SELECT 1 สำเร็จ”

7.2 ลำดับเฟสของ readiness

INITIALIZING ยังไม่มี source CONNECTING source ยังต่อไม่ได้ SCANNING Initial Build STRUCTURING ยังไม่มี Context RECONCILING ตามให้ตรงกัน READY ทุก check PASS DEGRADED BLOCKED FAILED มี WARN มี FAIL MIGRATIONS/Build ล้ม เฉพาะ phase READY เท่านั้นที่ /health/ready ตอบ 200 และผลต้องใหม่กว่า CORETH_DOCTOR_MAX_AGE_SECONDS (300 วินาที)
coreth_context/operations/doctor.py::_phase() และ coreth_context/views.py::deployment_readiness()

7.3 รัน Doctor

ไม่ต้องใส่ --customer-id เพราะอ่านจาก CORETH_DEPLOYMENT_CUSTOMER_ID ให้เอง

bash
docker compose exec -T api python manage.py context_doctor | python3 -m json.tool | head -40

ผลที่ต้องเห็น (ตัดมาบางส่วน)

{ "customer_id": "18b3f0c2-6a41-4e7d-9c22-0f5a4b8e1d77", "status": "PASS", "phase": "READY", "reason_codes": [], "checks": [ {"code": "POSTGRESQL", "status": "PASS", "message": "Canonical database is reachable"}, {"code": "MIGRATIONS", "status": "PASS", "message": "Migrations are current"}, {"code": "WORKER_HEARTBEAT", "status": "PASS", "message": "Worker heartbeat is current"}, ... ] }

สรุปเฉพาะข้อที่ไม่ผ่าน:

bash · สรุปแบบ PASS / WARN / FAIL
docker compose exec -T api python manage.py context_doctor | python3 -c '
import json,sys
r=json.load(sys.stdin)
for c in r["checks"]:
    print("{:5} {:26} {}".format(c["status"], c["code"], c["message"]))
print()
print("PHASE:", r["phase"], "| STATUS:", r["status"])
print("REASONS:", ", ".join(r["reason_codes"]) or "-")'
PASS POSTGRESQL Canonical database is reachable PASS MIGRATIONS Migrations are current PASS SOURCES_CONFIGURED Approved sources are configured PASS SOURCE_SCOPES Every source has an explicit approved scope PASS CREDENTIAL_REFERENCES Credential references are syntactically safe PASS WORKER_HEARTBEAT Worker heartbeat is current WARN BACKUP_HEALTH Encrypted backup is current and present WARN RESTORE_REHEARSAL Isolated restore rehearsal is current PHASE: DEGRADED | STATUS: WARN REASONS: BACKUP_HEALTH, RESTORE_REHEARSAL

7.4 รายการตรวจทั้งหมดของ Doctor

Codeตรวจอะไรกระทบ phase?
POSTGRESQLต่อฐาน canonical ได้—
MIGRATIONSไม่มี migration ค้างFAILED
DATA_DATABASEฐาน data ต่อได้และ migrate แล้ว—
SPLIT_DATABASE_RECOVERYbackup/restore ครอบคลุมทั้งสองฐานเมื่อแยกฐาน—
SOURCES_CONFIGUREDมี source ที่อนุมัติแล้วอย่างน้อยหนึ่งINITIALIZING
SOURCE_SCOPESทุก source มี approved scope ชัดเจน—
CREDENTIAL_REFERENCESsecret_ref เป็น reference ไม่ใช่ค่าจริง—
SOURCE_HEALTHสุขภาพ source ตามระดับ criticalityCONNECTING
SOURCE_VERIFICATIONcredential + scope ถูกยืนยันภายในอายุที่กำหนด—
SOURCE_CHECKPOINTSมี checkpoint ทั้ง INITIAL_SCAN และ RECONCILIATION—
INITIAL_BUILDInitial Context Build เคยรันและสถานะล่าสุดSCANNING
CANONICAL_CONTEXTมี Context + ContextVersion จริงSTRUCTURING
CONTINUOUS_SYNCsync ล่าสุดไม่เก่าเกิน CORETH_SYNC_MAX_AGE_SECONDS—
RECONCILIATIONreconcile ล่าสุดไม่เก่าเกินกำหนดRECONCILING
WORKER_HEARTBEATServiceHeartbeat(worker) ใหม่กว่า 120 วินาที—
EXTRACTION_FAILURESสถานะสกัดล่าสุดยอมรับได้—
CONTEXT_QUALITYความขัดแย้ง/ความสดของ Fact—
CONTEXT_PROVIDERpublic key + grant ของ provider ใช้ได้—
CONTEXT_CHANGED_DELIVERYoutbox ไม่ค้างเกิน CORETH_EVENT_PENDING_MAX_AGE_SECONDS—
DATA_OUTBOX_PROJECTIONความล่าช้าของ projection จาก data → context—
STRUCTURED_DATADataset readiness และเวอร์ชันที่ปลอดภัยล่าสุด—
TRANSFORM_RUNSTransformRun ที่ล้มหรือถูกกักไว้—
AI_POLICYpolicy ของ semantic processing ใช้งานได้—
BACKUP_HEALTHมี backup ใหม่กว่า CORETH_BACKUP_MAX_AGE_SECONDS—
RESTORE_REHEARSALซ้อมกู้คืนใหม่กว่า CORETH_RESTORE_REHEARSAL_MAX_AGE_SECONDS—
CONTROL_PLANEEffectivePolicy + WorkAdmission มองเห็นได้—
V11_RECONCILIATIONlineage ข้ามโดเมนสอดคล้องกัน—

ข้อที่ไม่ได้ทำให้เปลี่ยน phase โดยตรง ยังทำให้ phase เป็น BLOCKED (ถ้ามี FAIL) หรือ DEGRADED (ถ้ามี WARN) ได้ตามกฎรวมท้ายสุด

7.5 Smoke test ครบวงจรหนึ่งรอบ

ทดสอบเส้นทางจริงตั้งแต่ไฟล์เข้าจนถึงข้อมูลที่เผยแพร่ ใช้เวลาไม่กี่นาที

bash · end-to-end smoke test
# 0) ค่าพื้นฐานจาก .env
set -a; . ./.env; set +a
PORT="${CORETH_HTTP_PORT:-8080}"

# 1) liveness ผ่าน Caddy
curl -fsS "http://127.0.0.1:$PORT/health/live" >/dev/null \
  && echo "PASS  api liveness" || echo "FAIL  api liveness"

# 2) migration ครบ (ไม่มีบรรทัดที่ยังไม่ติ๊ก)
docker compose exec -T api python manage.py migrate --check >/dev/null 2>&1 \
  && echo "PASS  migrations" || echo "FAIL  migrations (รัน docker compose run --rm migrate)"

# 3) worker เต้นอยู่จริง — บังคับหนึ่งรอบแล้วดูผลนับ
docker compose exec -T api python manage.py runworker --once

# 4) Doctor + readiness
docker compose exec -T api python manage.py context_doctor >/dev/null
curl -sS -o /tmp/ready.json -w 'HTTP %{http_code}\n' "http://127.0.0.1:$PORT/health/ready"
cat /tmp/ready.json; echo

# 5) dataset ที่เผยแพร่แล้ว
docker compose exec -T api python manage.py structured_data_status

# 6) งานตรวจทานที่ยังค้าง (อ่านอย่างเดียว ไม่เปลี่ยนสถานะ)
docker compose exec -T api python manage.py review_new_data

ผลที่ต้องเห็น

PASS api liveness PASS migrations {"dispatched": 0, "polled": 3, "processed": 1, "projected": 1, "reconciled": 1, "review_jobs": ["…"]} HTTP 200 {"ready": true, "phase": "READY", "reason": "READY"} {"customer_id": "18b3f0c2-…", "datasets": [{"dataset_key": "sales_records", "latest_version": 12, "latest_readiness": "READY", "latest_ready_version": 12}]}
ถ้า /health/ready ตอบ 503 ให้ดูค่า reason ก่อนเสมอ — CUSTOMER_NOT_CONFIGURED = .env ไม่มี UUID ที่ถูกต้อง · DOCTOR_NOT_EVALUATED = worker ยังไม่เคยรัน · DOCTOR_STALE = worker หยุดไปนานกว่า 300 วินาที · ค่าอื่นคือ reason_codes[0] จาก Doctor
ตรวจว่า RLS ถูกเปิดจริงบน PostgreSQL
bash
docker compose exec -T postgres psql -U coreth -d coreth_context -c \
  "SELECT count(*) FILTER (WHERE relrowsecurity) AS rls_on, count(*) AS total
   FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
   WHERE c.relkind='r' AND n.nspname='public';"
rls_on | total --------+------- 61 | 61

secure_database เปิด RLS ให้ทุกตารางที่ Django จัดการ (รวม django_migrations) และถอนสิทธิ์ PUBLIC, anon, authenticated — สำคัญมากเมื่อฐานข้อมูลอยู่บน Supabase ที่เปิด REST API ให้ role เหล่านี้โดยปริยาย

8. Operations#

งานประจำหลังติดตั้ง ทุกคำสั่งมีอยู่จริงใน repository

8.1 Deploy โค้ดใหม่โดยไม่ตัด ingestion

worker อยู่ใน profile แยกโดยเจตนา จึงอัปเดตเว็บได้โดยไม่แตะงานที่กำลังทำ แล้วค่อยรีสตาร์ต worker ตอนที่ว่าง

bash · ขั้นที่ 1 — อัปเดตเว็บ
git pull
docker compose config --quiet                 # ตรวจไวยากรณ์ก่อนเสมอ
docker compose up -d --build --no-deps --wait --wait-timeout 300 api admin caddy
bash · ขั้นที่ 2 — เมื่อไม่มีงานค้าง จึงรีสตาร์ต worker
docker compose exec -T api python manage.py review_new_data   # ดูว่ามีอะไรค้างไหม
docker compose up -d --build --no-deps worker
docker compose logs --tail=10 worker
อย่าใช้ docker compose up -d เปล่า ๆ พร้อม COMPOSE_PROFILES ที่มี worker หรือ * — จะ recreate worker และตัดงานกลางคัน ถ้าต้องหยุดงานที่ค้างจริง ๆ ให้ใช้ปุ่ม force stop ที่ /admin/ingestion-history ซึ่งฆ่าเฉพาะ child process ของ signal นั้นและบันทึก FORCE_STOPPED_BY_OPERATOR

8.2 Upgrade ที่มี migration

scripts/upgrade.sh ห่อลำดับทั้งหมดไว้แล้ว: สำรอง → ซ้อมกู้คืน → Doctor → preflight → migrate → check → Doctor อีกครั้ง ทุกขั้นหยุดทันทีเมื่อผิดพลาด (set -eu)

bash
docker compose exec api sh -c 'sh scripts/upgrade.sh "$CORETH_DEPLOYMENT_CUSTOMER_ID"'

สำหรับการย้อนกลับ (rollback) ต้องอ้างเอกสาร ADR ที่อนุมัติแล้ว:

bash
docker compose exec api sh -c 'sh scripts/rollback-preflight.sh \
  "$CORETH_DEPLOYMENT_CUSTOMER_ID" "docs/adr/0003-multi-entity-schema-design.md"'
คำสั่งกลุ่มนี้ใช้ sh -c '…' ครอบด้วย single quote โดยตั้งใจ เพื่อให้ $CORETH_DEPLOYMENT_CUSTOMER_ID ถูกขยายในคอนเทนเนอร์ ไม่ใช่บน host shell ซึ่งตัวแปรจะว่างถ้ายังไม่ได้ source .env

upgrade_preflight ปฏิเสธถ้า --customer-id ไม่ตรงกับ CORETH_DEPLOYMENT_CUSTOMER_ID และตรวจว่ามี backup + restore rehearsal ที่ยังไม่หมดอายุจริง จึงข้ามขั้นตอนสำรองข้อมูลไม่ได้

8.3 สำรองและกู้คืน

งานคำสั่งผลลัพธ์
สำรองทันทีmanage.py backup_contextไฟล์ CORETHBK1 (AES-GCM) + BackupArtifact พร้อม checksum
ซ้อมกู้คืนmanage.py restore_rehearsalกู้เข้าฐานแยกต่างหากแล้วตรวจ · RestoreRehearsal
สำรองตามรอบอัตโนมัติdocker compose --profile ops up -d backuprunbackup_scheduler ทำงานทุก CORETH_BACKUP_INTERVAL_SECONDS
ตรวจสถานะหลังกู้คืนจริงmanage.py reconcile_restored_stategate ตรวจอย่างเดียว ไม่แก้ข้อมูล
ต้องตั้ง CORETH_BACKUP_KEY_FILE หรือ CORETH_BACKUP_KEY_SECRET_REF อย่างใดอย่างหนึ่งก่อน มิฉะนั้น backup_context จะ error และ Doctor จะรายงาน BACKUP_HEALTH เป็น WARN ตลอด เก็บคีย์ไว้คนละที่กับไฟล์สำรอง

8.4 คำสั่งสำหรับงานประจำ

เมื่อไหร่คำสั่งทำอะไร
ต้องการดูสถานะรวมcontext_statussource + Initial Build + readiness + sync metrics
ต้องการดู datasetstructured_data_statusเวอร์ชันล่าสุด/ล่าสุดที่ปลอดภัยของทุก dataset
ตรวจนโยบาย AI ของทุก sourcelist_source_policies --issues-onlyเฉพาะ source ที่ policy ผิดหรือบล็อกไฟล์ดิบ
เครดิต AI หมดresume_ai แล้ว resume_ai --source-id … --customer-id …ดูรายการ hold แล้วปลดล็อกทีละ source
Transform ค้างกลางคันrecover_interrupted_transformsกู้โดยไม่ลบข้อมูลหรือประวัติที่ commit แล้ว
ต้องประมวลผลไฟล์เดิมใหม่reprocess_structured_dataสร้าง TransformRun/DatasetVersion ใหม่แบบ immutable
Snapshot กินพื้นที่purge_input_snapshotsลบเฉพาะ snapshot ที่ CP เป็นเจ้าของและหมดอายุ · ไม่แตะไฟล์ของ File Ingestion
ยอดรวมไฟล์ไม่ตรงreconcile_file_collectionsดู/คำนวณยอดใหม่ต่อ customer + environment
ต้นทางเปลี่ยน Source UUIDremap_file_ingestion_sourceดู/ซ่อม mapping โดยคงประวัติเดิม
ส่ง Context Changed ค้างdispatch_context_eventsส่ง outbox ที่ถึงกำหนดแบบ at-least-once
รัน review job ทันทีrun_review_jobsรันหนึ่งรอบ · ลองใหม่เฉพาะ job ที่ล้มได้

คำสั่งข้างต้นเรียกด้วยรูปแบบเดียวกัน: docker compose exec api python manage.py <command> (ใส่ -T เมื่อไม่ต้องการให้ถามโต้ตอบ เช่นในสคริปต์ CI)

8.5 ดู log

bash
docker compose logs -f worker                       # JSON บรรทัดละ event
docker compose logs --tail=200 api | grep -i error
docker compose logs --tail=50 cloudflared           # เมื่อใช้ overlay

Log เป็น JSON เสมอ (coreth_context/logging.py::JsonLogFormatter) ฟิลด์ที่มีประโยชน์: event, customer_id, source_id, error_code, status · ระหว่างประมวลผลหนึ่ง signal จะเห็น signal_progress พร้อม phase (FETCH_METADATA → DOWNLOAD_CONTENT → FILE_AGENT_ANALYSIS หรือ EXTRACT_DOCUMENT) และเวลาที่ใช้

ถ้าตั้ง CORETH_ADMIN_LOG_GROUP หน้า /admin/activity จะดึง log จาก CloudWatch มาแสดง โดยกรองเฉพาะ event ของ customer นี้ และแสดงเฉพาะฟิลด์ที่อยู่ใน allowlist ไม่แสดงข้อความดิบ

8.6 รันชุดทดสอบ

bash
docker compose --profile test run --rm --build test

มีเทสต์ Python 64 ไฟล์ใน tests/ และเทสต์ UI ด้วย node --test (npm run test:explorer, npm run test:schema-design)

9. Troubleshooting#

อาการที่เกิดขึ้นได้จริงจากเส้นทาง error ที่มีในโค้ด เรียงตามสิ่งที่พบบ่อยตอนติดตั้ง

9.1 ติดตั้งและเริ่มระบบ

อาการตรวจแก้
api ขึ้นแล้วดับทันที exit 78 docker compose logs api เห็น runtime secret injection failed OPENAI_API_KEY_REF resolve ไม่ได้ หรือถูกตั้งพร้อม OPENAI_API_KEY · ตรวจ IAM, AWS_REGION และให้ secret มี key OPENAI_API_KEY
docker compose config error ว่า CORETH_PUBLIC_HOST ไม่ถูกตั้ง grep COMPOSE_FILE .env overlay Cloudflare ถูกเปิดแต่ยังไม่มีค่า · เติม CORETH_PUBLIC_HOST + CLOUDFLARE_TUNNEL_TOKEN หรือเอา compose.cloudflare.yaml ออกจาก COMPOSE_FILE
เปิด http://127.0.0.1:8080/admin/ แล้วได้ 404 cat deploy/Caddyfile เป็นพฤติกรรมที่ถูกต้อง — Caddyfile ปกติส่งทุก path ไป api · ใช้ 127.0.0.1:8081 แทน หรือเปิด overlay Cloudflare
เปิด :8081/admin/ แล้วได้ 404 ทุกหน้า docker compose exec admin env | grep CORETH_ADMIN_ENABLED admin_access() โยน 404 เมื่อ CORETH_ADMIN_ENABLED ไม่ใช่ true · ต้องเข้าผ่าน service admin ไม่ใช่ api
ล็อกอินได้แต่ทุกหน้า Admin ขึ้น 404 มี AdminMembership ที่ active=true ไหม รัน bootstrap_root_user ใหม่ หรือเพิ่มสมาชิกที่ /admin/users · ตรวจว่า customer_id และ environment ตรงกับ .env
migrate ล้มเพราะต่อฐานไม่ได้ docker compose logs migrate ฐานภายนอกต้องใช้ SSL และ URL-encode รหัสผ่าน · รัน docker compose run --rm wizard ใหม่ซึ่งจะ SELECT 1 ก่อนบันทึก
Wizard บอก Configuration changed during setup; rerun wizard มีใครแก้ .env พร้อมกันหรือไม่ เป็นการป้องกันเขียนทับ · ปิดตัวแก้ไขอื่นแล้วรัน wizard ใหม่

9.2 ความพร้อม (readiness)

อาการตรวจแก้
/health/ready → 503 CUSTOMER_NOT_CONFIGURED grep CORETH_DEPLOYMENT_CUSTOMER_ID .env ค่าว่างหรือไม่ใช่ UUID · แก้แล้ว docker compose up -d --force-recreate api
/health/ready → 503 DOCTOR_NOT_EVALUATED docker compose ps worker worker ยังไม่เคยรัน · docker compose up -d worker (Doctor รันอยู่ใน runworker เท่านั้น)
/health/ready → 503 DOCTOR_STALE docker compose logs --tail=30 worker worker ตายหรือค้าง · ผลเก่ากว่า CORETH_DOCTOR_MAX_AGE_SECONDS (300 วิ) · รีสตาร์ต worker และดู exception
phase ค้างที่ SCANNING context_status ดู initial_build_status Initial Context Build ยังไม่จบ · รัน onboard_file_ingestion จนได้ IMPORTED
phase DEGRADED เพราะ BACKUP_HEALTH มี CORETH_BACKUP_KEY_* หรือยัง ตั้งคีย์แล้วรัน backup_context + restore_rehearsal หรือเปิด --profile ops service backup
worker heartbeat เป็น DOCTOR_ERROR หา context_doctor_failed ใน log Doctor โยน exception ระหว่างประเมิน · ดู error_code ใน log JSON

9.3 การนำเข้าและประมวลผลไฟล์

Error codeความหมายแก้
AGENTIC_REQUIREDsource ไม่ได้ตั้ง semantic_processing.mode = AI_REQUIRED ขณะที่ CORETH_AGENTIC_ONLY=trueแก้ source config (สร้าง config_version ใหม่) หรือปิด CORETH_AGENTIC_ONLY ชั่วคราวระหว่างย้ายระบบ
AI_QUOTA_EXHAUSTEDเครดิต/โควตา OpenAI หมด · ระบบสร้าง AISourcePause อัตโนมัติเพื่อไม่ทิ้งงานเติมเครดิต แล้ว resume_ai --source-id … --customer-id … (ดูรายการด้วย resume_ai เปล่า ๆ)
AI_SECRET_KEY_MISSING / AI_SECRET_UNAVAILABLEอ่าน secret ไม่ได้ หรือใน secret ไม่มี key OPENAI_API_KEYตรวจรูปแบบ JSON ของ secret และสิทธิ์ secretsmanager:GetSecretValue + kms:Decrypt
AI_FILE_TOO_LARGEไฟล์ใหญ่กว่า CORETH_FILE_AGENT_MAX_BYTES (5 MB)เพิ่มค่าใน .env หรือแยกไฟล์ต้นทาง · ระบบไม่ตัดเนื้อหาทิ้งเงียบ ๆ โดยเจตนา
AI_PDF_LIMIT_OR_ENCRYPTEDPDF เกิน CORETH_FILE_AGENT_MAX_PAGES หรือถูกใส่รหัสปลดรหัส/แยกไฟล์ หรือเพิ่มค่า max pages
AI_RAW_FILE_REDACTION_REQUIREDpolicy บังคับปกปิดข้อมูล แต่ PDF/ภาพปกปิดไม่ได้เปิด “อนุญาตส่ง PDF/ภาพต้นฉบับให้ AI” ที่ /admin/setup หรือเอา redaction rule ออก
FILE_INGESTION_HTTP_<status>
(เช่น …_401, …_403)
File Ingestion ตอบไม่ใช่ 200 · ระบบไม่เปิดเผย body/URL ของต้นทางใน diagnosticsตรวจ issuer/audience/subject/kid/private_key ใน secret และสิทธิ์ file:read, events:read
ARCHIVE_HTTP_403subject ยังไม่มีสิทธิ์ file:archive-openwebui ที่ File Ingestion หรือ subject ใน secret ไม่ตรงกับที่ให้สิทธิ์ไว้รันบนเครื่อง File Ingestion: docker exec -it coreth-file-ingestion-api-1 python manage.py configure_openwebui_archive --subject service:context-processor แล้วตรวจว่า subject ใน secret ตรงกัน
ARCHIVE_HTTP_<status> อื่น ๆขาอัปโหลดไม่ได้ HTTP 202401 = JWT ผิด/หมดอายุ · 413 = ไฟล์ใหญ่เกินขีดของ File Ingestion · 5xx = ปลายทางมีปัญหา ระบบจะ retry ให้เอง
receipt ค้าง PROCESSING / RETRY ไม่จบสักทีอัปโหลดผ่านแล้ว (มี archive_file_version_id) แต่ขาตรวจซ้ำล้ม — มักเพราะ subject มีแค่ file:archive-openwebui ไม่มี file:readให้สิทธิ์ file:read + events:read กับ subject เดียวกัน
ARCHIVE_REFERENCE_MISMATCHsource_provenance ของ file version ไม่ตรงกับ submission ต้นทางตรวจว่า File Ingestion บันทึก provenance ครบทั้ง 6 ช่องตามตารางในหัวข้อ 6.2
ARCHIVE_CUSTOMER_MISMATCH / ARCHIVE_CHECKSUM_MISMATCHปลายทางตอบ customer หรือ sha256 ไม่ตรงกับที่ส่งไปหยุดใช้ endpoint นั้นแล้วตรวจว่า CORETH_OPENWEBUI_ARCHIVE_URL ชี้ไป File Ingestion ของ customer ที่ถูกต้อง
INPUT_CHECKSUM_MISMATCHเนื้อหาที่เก็บไว้ไม่ตรง checksum ตอนอัปโหลดให้ผู้ใช้อัปโหลดใหม่ · submission เดิมใช้ต่อไม่ได้
ONBOARDING_INCOMPLETEครบ --max-cycles แต่ยังไม่หมดคิวรัน onboard_file_ingestion ซ้ำ — ทำต่อจากเดิมได้เพราะมี cursor
FORCE_STOPPED_BY_OPERATORมีคนกดหยุดงานนั้นจาก Adminสั่งลองใหม่ที่ /admin/ingestion-history → retry
SNAPSHOT_STORAGE_NOT_CONFIGUREDpolicy ต้องการ replay snapshot แต่ไม่มี bucket/KMSตั้ง CORETH_SNAPSHOT_BUCKET + CORETH_SNAPSHOT_KMS_KEY_ID (บังคับใช้ SSE-KMS)

9.4 การตรวจทานและ schema

Error codeความหมายแก้
409 REVISION_CONFLICTcandidate เปลี่ยนไปแล้วระหว่างที่กำลังตรวจโหลด manifest ใหม่แล้วยืนยันอีกครั้ง
409 SCHEMA_BASE_CHANGEDมีข้อเสนออื่นถูกอนุมัติก่อน และ rebase อัตโนมัติไม่ปลอดภัยสั่ง regroup หรือให้ AI เสนอใหม่บน base ปัจจุบัน
409 SCHEMA_ALREADY_DECIDEDมีคนตัดสินข้อเสนอนี้ไปแล้วรีเฟรชหน้า
409 FEEDBACK_APPLY_PENDINGAI กำลังแก้ข้อเสนอตาม feedback อยู่ (REBUILDING)รอ worker ทำ ReviewJob(FEEDBACK_APPLY) จบ
IDENTITY_MIGRATION_REVIEW_REQUIREDการอนุมัติจะเปลี่ยน identity field ของแถวที่มีอยู่ตรวจแผน migration แล้วส่ง migration_digest ที่ตรงกันมายืนยัน
DESIGN_PLAN_DECISION_REQUIREDข้อเสนอนี้อยู่ภายใต้ SchemaDesignPlanตัดสินผ่านหน้า plan ไม่ใช่ตัดสินข้อเสนอเดี่ยว
HUMAN_UPLOAD_CONFIRMATION_REQUIREDไฟล์จาก OpenWebUI ต้องมีคนยืนยันเสมอไม่ใช่ข้อผิดพลาด — ให้ผู้ใช้เข้าไปกดยืนยัน
AUTO_NEW_DISABLED / AUTO_NEW_SOURCE_DISABLEDปิด auto-approve ที่ระดับ deployment หรือระดับ sourceตรวจ CORETH_AUTO_APPROVE_NEW_DATA, CORETH_REVIEW_PAUSE_APPLY และ structured_data.auto_approve_new_data
AUTO_NEW_SOURCE_CHANGED / ROW_APPROVAL_STALEsource config หรือ policy เปลี่ยนหลังออกใบอนุมัติแถวทำงานถูกต้องตามออกแบบ — ให้ worker สร้างการอนุมัติใหม่บนนโยบายปัจจุบัน
SUBMISSION_GRANT_REQUIRED / SOURCE_WRITE_SCOPE_REQUIREDService JWT ไม่มี scope หรือ grant หมดอายุmanage_tool_grant configure ใหม่ · ตรวจ expires_at และ claim source_id ใน token
HUMAN_SESSION_REQUIREDพยายามกดยืนยันด้วย Service JWTการยืนยันขั้นสุดท้ายต้องเป็น session ของคนเท่านั้น
LEGACY_CENTER_MAPPING_REQUIREDdataset เดิมถูกรับเป็น baseline โดยผูกกับศูนย์หนึ่ง แต่ข้อมูลใหม่มาจากศูนย์อื่นแยก dataset หรือย้าย identity ด้วยแผน migration
ไม่มีอะไรเผยแพร่เลยทั้งที่ยืนยันแล้วgrep CORETH_REVIEW_PAUSE_APPLY .envถ้าเป็น true คือเปิดโหมดหยุดทั้งหมดไว้ · run_review_jobs() return ทันที

9.5 ข้อมูลและฐานข้อมูล

อาการตรวจแก้
ข้อมูลเผยแพร่แล้วแต่ Context ยังไม่ขยับDoctor check DATA_OUTBOX_PROJECTIONต้องมี ContextProjectionDefinition ที่ active มิฉะนั้น event ถูกทำเป็น DELIVERED ทิ้ง · สร้างด้วย configure_context_projection
ตาราง app_output ไม่มีคอลัมน์ใหม่อนุมัติ SchemaImprovement แล้วหรือยังDDL สร้างตอน prepare_output() ซึ่งรันเมื่ออนุมัติ schema เท่านั้น
Supabase REST เห็นตารางภายในของระบบรัน SQL ตรวจ RLS ในหัวข้อ 7.5docker compose exec api python manage.py secure_database
แถวติด DataConflict ไม่เผยแพร่/admin/data-conflictsตัดสินทีละฟิลด์ หรือกำหนดลำดับความน่าเชื่อถือด้วย configure_source_authority
TransformRun ค้างสถานะ RUNNINGDoctor check TRANSFORM_RUNSrecover_interrupted_transforms — ไม่ลบข้อมูลที่ commit แล้ว
เปลี่ยนค่าใน .env แล้วไม่มีผลตัวแปรนั้นอยู่ในบล็อก &app-env ของ compose.yaml ไหมดูรายการในหัวข้อ 5.6 · ถ้าไม่อยู่ ต้องเพิ่มเองใน compose.yaml

10. Differences from Legacy Documentation#

เฉพาะข้อที่เปลี่ยนอย่างมีนัยสำคัญและพบระหว่างอ่านโค้ดจริงที่ commit 8d2a865 ไม่ใช่ changelog ทั้งหมด

เอกสารเดิมบอกว่าความจริงในโค้ดวันนี้
เอกสารสถาปัตยกรรม v1.1 (ลบแล้ว) อธิบาย streaming CSV pipeline (process_stream, StreamingIngestionCheckpoint) เป็นเส้นทางหลักของไฟล์ขนาดใหญ่ โค้ดยังอยู่ แต่ เข้าไม่ถึงเลย ด้วยค่าเริ่มต้น — sync.py เรียกเมื่อ not settings.CORETH_AGENTIC_ONLY ซึ่งค่าเริ่มต้นเป็น true
โหมด semantic processing มีหลายแบบ (DETERMINISTIC_ONLY, AI_OPTIONAL, AI_REQUIRED) และไฟล์ตัวอย่างใน examples/file-ingestion/ ใช้ DETERMINISTIC_ONLY เหลือ AI_REQUIRED ทางเดียวในทางปฏิบัติ · อย่างอื่นทำให้ signal fail ด้วย AGENTIC_REQUIRED · ไฟล์ตัวอย่างล้าสมัยแล้ว
ไม่มีการกล่าวถึง Submission / Review workflow มีครบเป็นแกนหลัก: Submission, CandidateGroup, CandidateRow, ReviewReceipt, RowApproval, ReviewJob, BatchReview พร้อม API และหน้า Admin
ไม่มี File Agent coreth_data/file_agent.py ใช้ OpenAI Agents SDK อ่าน schema เดิมแล้วเสนอ table/row · เป็นเส้นทางประมวลผลหลักของ FILE_READY และ OPENWEBUI_UPLOAD
ไม่มี OpenWebUI ในสถาปัตยกรรม เป็น source type เต็มรูปแบบ (OPENWEBUI_UPLOAD) มี Service JWT, ToolServiceGrant, OpenWebUIUploadReceipt, หน้าตรวจทานเฉพาะผู้ใช้ และการส่งไฟล์ดิบกลับไปเก็บที่ File Ingestion
ไม่มี Center / ขอบเขตการตรวจทาน Center + CenterMembership + SourceBinding เป็นตัวกำหนดว่าใครตรวจข้อมูลของศูนย์ไหนได้ · จัดการที่ /admin/centers
ไม่มี Schema Design ล่วงหน้า SchemaDesignPlan, SchemaDesignSource, API /api/v1/schema-design และวงจร feedback ให้ AI แก้ข้อเสนอ
เอกสารติดตั้งเดิมพาไปที่ /admin ผ่าน port หลัก Monitor เป็น service แยก (admin) ที่ 127.0.0.1:8081 · deploy/Caddyfile ปกติไม่ route /admin · เฉพาะ Caddyfile.cloudflare เท่านั้นที่ route ไป admin:8000
เริ่มทุกอย่างด้วย docker compose up -d worker อยู่ใน profile แยกโดยเจตนา ต้องสั่ง docker compose up -d worker ต่างหาก และห้ามใส่ไว้ใน COMPOSE_PROFILES
Readiness เป็นเรื่องของ API ContextDoctor รันอยู่ใน runworker · ถ้าไม่มี worker ระบบจะไม่มีวันขึ้น READY
.env.advanced.example เสนอให้ปรับ DJANGO_DEBUG, DATABASE_URL, CORETH_LOG_LEVEL, CORETH_BACKUP_DIRECTORY, DATABASE_CONN_MAX_AGE และชุด CORETH_*_VERSION ตัวแปรกลุ่มนี้ไม่ได้อยู่ในบล็อก &app-env ของ compose.yaml จึงไปไม่ถึง container · ดูหัวข้อ 5.6
เอกสารสถาปัตยกรรม v1.1 ระบุว่ามี “deliberate current limits” หลายข้อ หลายข้อถูกแทนที่ด้วย governance workflow แล้ว · อ่านข้อจำกัดจาก CORETH_AUTO_APPROVE_* และ CORETH_REVIEW_PAUSE_APPLY ในหัวข้อ 5.3 แทน

เอกสารที่เหลืออยู่ใน docs/

เอกสารที่เนื้อหาถูกยกมารวมไว้ในฉบับนี้แล้วถูกลบทิ้ง (24 ไฟล์ · ยังกู้คืนได้จาก git history) ที่เหลือคือเอกสารที่ฉบับนี้ไม่ได้ครอบคลุม

ไฟล์ใช้อ้างอิงเรื่องอะไร
docs/adr/0001–0003เหตุผลเบื้องหลังการตัดสินใจเชิงสถาปัตยกรรม · เป็นหลักฐานทางประวัติศาสตร์ ไม่แก้ย้อนหลัง และ scripts/rollback-preflight.sh ต้องอ้างหมายเลข ADR
docs/review-workflow/ · schema-review-feedback.md · review-validation-recovery.md · review-row-removal-and-text-numbers.mdรายละเอียดการตรวจทานและวงจร feedback ที่ลึกกว่า §2.4–2.5
docs/file-agent-workflow-th.mdพฤติกรรม File Agent ทีละขั้น ลึกกว่า §2.3
docs/openwebui-single-account-review-th.mdเคสใช้ OpenWebUI บัญชีเดียว
docs/file-ingestion-sales-validation-th.mdเกณฑ์ตรวจรับ Gmail CSV → Supabase สองฐาน
docs/cleanup-file-ingestion-th.mdคู่มือของ scripts/cleanup_file_ingestion_transactions.py
docs/stationrich-*.mdงานเฉพาะลูกค้า Station Rich
เอกสารฉบับนี้เป็นเอกสารหลักฉบับเดียวของสถาปัตยกรรมและการติดตั้ง ไฟล์ที่เหลือใน docs/ เป็นเอกสารประกอบเฉพาะเรื่อง เมื่อขัดกันให้ยึดฉบับนี้ และเมื่อฉบับนี้ขัดกับโค้ด ให้ยึดโค้ดแล้วแก้เอกสาร