diff --git a/.env b/.env index 1e1d979..ac8473c 100644 --- a/.env +++ b/.env @@ -77,26 +77,26 @@ GRAPH_ENTITY_EXTRACTION=false # └──────────────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┘ # ── Option 1 · LOW · CPU only · t3.2xlarge · 20 cores · 32 GB RAM ───────────── -OLLAMA_MAX_LOADED_MODELS=1 -OLLAMA_CONTEXT_LENGTH=8192 -CHAT_MODEL=phi4-mini -EMBEDDING_MODEL=bge-m3 -CODING_PLANNER_MODEL=phi4-mini -BA_MODEL=granite4.1:3b -PM_MODEL=granite4.1:3b -SA_MODEL=phi4-mini -TA_MODEL=phi4-mini -DESIGNER_MODEL=gemma4:e2b -FE_MODEL=qwen3:4b -MOBILE_MODEL=qwen3:4b -BE_MODEL=qwen3:4b -DBA_MODEL=qwen3:4b -DA_MODEL=granite4.1:3b -TL_MODEL=phi4-mini -TECH_LEAD_MODEL=qwen3:4b -TESTER_MODEL=gemma4:e2b -DEVSECOPS_MODEL=qwen3:4b -CLARIFIER_MODEL=phi4-mini +# OLLAMA_MAX_LOADED_MODELS=1 +# OLLAMA_CONTEXT_LENGTH=8192 +# CHAT_MODEL=phi4-mini +# EMBEDDING_MODEL=bge-m3 +# CODING_PLANNER_MODEL=phi4-mini +# BA_MODEL=granite4.1:3b +# PM_MODEL=granite4.1:3b +# SA_MODEL=phi4-mini +# TA_MODEL=phi4-mini +# DESIGNER_MODEL=gemma4:e2b +# FE_MODEL=qwen3:4b +# MOBILE_MODEL=qwen3:4b +# BE_MODEL=qwen3:4b +# DBA_MODEL=qwen3:4b +# DA_MODEL=granite4.1:3b +# TL_MODEL=phi4-mini +# TECH_LEAD_MODEL=qwen3:4b +# TESTER_MODEL=gemma4:e2b +# DEVSECOPS_MODEL=qwen3:4b +# CLARIFIER_MODEL=phi4-mini # ── Option 2 · MEDIUM · CPU heavy · m7i.12xlarge · 44 cores · 256 GB RAM · 3 GB VRAM ── # OLLAMA_MAX_LOADED_MODELS=4 @@ -121,26 +121,26 @@ CLARIFIER_MODEL=phi4-mini # CLARIFIER_MODEL=qwen3.6:35b # ── Option 3 · HIGH · GPU · g6e.2xlarge · 8 cores · 64 GB RAM · 48 GB VRAM ─── -# OLLAMA_MAX_LOADED_MODELS=2 -# OLLAMA_CONTEXT_LENGTH=32768 -# CHAT_MODEL=qwen3.6:35b -# EMBEDDING_MODEL=qwen3-embedding:8b -# CODING_PLANNER_MODEL=qwen3.6:35b -# BA_MODEL=qwen3.6:35b -# PM_MODEL=qwen3.6:35b -# SA_MODEL=qwen3.6:35b -# TA_MODEL=qwen3.6:35b -# DESIGNER_MODEL=gemma4:31b -# FE_MODEL=devstral-small-2:24b -# MOBILE_MODEL=devstral-small-2:24b -# BE_MODEL=devstral-small-2:24b -# DBA_MODEL=devstral-small-2:24b -# DA_MODEL=qwen3.6:35b -# TL_MODEL=devstral-small-2:24b -# TECH_LEAD_MODEL=devstral-small-2:24b -# TESTER_MODEL=magistral:24b -# DEVSECOPS_MODEL=devstral-small-2:24b -# CLARIFIER_MODEL=qwen3.6:35b +OLLAMA_MAX_LOADED_MODELS=2 +OLLAMA_CONTEXT_LENGTH=32768 +CHAT_MODEL=qwen3.6:35b +EMBEDDING_MODEL=qwen3-embedding:8b +CODING_PLANNER_MODEL=qwen3.6:35b +BA_MODEL=qwen3.6:35b +PM_MODEL=qwen3.6:35b +SA_MODEL=qwen3.6:35b +TA_MODEL=qwen3.6:35b +DESIGNER_MODEL=gemma4:31b +FE_MODEL=devstral-small-2:24b +MOBILE_MODEL=devstral-small-2:24b +BE_MODEL=devstral-small-2:24b +DBA_MODEL=devstral-small-2:24b +DA_MODEL=qwen3.6:35b +TL_MODEL=devstral-small-2:24b +TECH_LEAD_MODEL=devstral-small-2:24b +TESTER_MODEL=magistral:24b +DEVSECOPS_MODEL=devstral-small-2:24b +CLARIFIER_MODEL=qwen3.6:35b # ── Option 4 · ULTRA · GPU · g6e.12xlarge · 48 cores · 384 GB RAM · 192 GB VRAM (4× L40S) ── # OLLAMA_MAX_LOADED_MODELS=3 diff --git a/README.md b/README.md index 073ec78..d38c0f9 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,125 @@ # YAN Ollama Stack -Nền tảng AI cục bộ vận hành hoàn toàn offline: **Ollama** (suy luận LLM), **Qdrant** (vector DB), **Neo4j** (graph DB), **RAG API** (FastAPI), **Agent API** (điều phối SDLC), **Open WebUI** (chat UI), **Watchtower** (tự động cập nhật), và **Deunhealth** (giám sát healthcheck container). +Nền tảng AI cục bộ vận hành hoàn toàn **offline** — không cần cloud, không cần API key bên ngoài. Toàn bộ pipeline từ LLM inference, vector search, graph enrichment, RAG, đến SDLC automation chạy trong một cụm Docker Compose trên máy local hoặc private server. + +**Thành phần cốt lõi:** Ollama · Qdrant · Neo4j · RAG API · Agent API · Open WebUI · Watchtower · Deunhealth --- -## Stack Overview - -| Service | Image | Port (host) | Mô tả | -|--------------|------------------------------------|---------------|--------------------------------------------------------| -| `ollama` | `ollama/ollama:latest` | 11434 | LLM & embedding inference | -| `qdrant` | `qdrant/qdrant:latest` | 6333 / 6334 | Vector database (REST / gRPC) | -| `neo4j` | `neo4j:5-community` | 7474 / 7687 | Knowledge graph (Neo4j Browser / Bolt) | -| `rag-api` | build `./rag-api` | 8090 | FastAPI RAG service (ingest + ask) | -| `agent-api` | build `./agent-api` | 8091 | FastAPI SDLC Agent Orchestrator (15-step SDLC workflow) | -| `open-webui` | `ghcr.io/open-webui/open-webui` | 8085 | Chat UI kết nối Ollama & Qdrant | -| `watchtower` | `containrrr/watchtower` | — | Tự động pull & restart image mới nhất | -| `deunhealth` | `qmcgaw/deunhealth` | 9999 | Khởi động lại container khi healthcheck thất bại | - -**Model theo biến môi trường** (cấu hình trong `.env` → mục `SDLC Agent Models`): - -| Mục đích | Env var(s) | Giá trị | -|---------------------------------------------|-------------------------------------------------------------------|---------------------------| -| Embedding | `EMBEDDING_MODEL` | Lấy từ `.env` | -| Chat — RAG API `/ask` | `CHAT_MODEL` | Lấy từ `.env` | -| Reasoning agents (BA / PM / SA / TA / DA) | `BA_MODEL` `PM_MODEL` `SA_MODEL` `TA_MODEL` `DA_MODEL` | Lấy từ `.env` | -| Team Lead planning agent | `TL_MODEL` | Lấy từ `.env` | -| Coding agents (FE / Mobile / BE / DBA / Tech Lead / DevSecOps) | `FE_MODEL` `MOBILE_MODEL` `BE_MODEL` `DBA_MODEL` `TECH_LEAD_MODEL` `DEVSECOPS_MODEL` | Lấy từ `.env` | -| Creative agents (Tester / Designer) | `TESTER_MODEL` `DESIGNER_MODEL` | Lấy từ `.env` | -| Clarifier (cross-role gap analysis) | `CLARIFIER_MODEL` | Lấy từ `.env` | - -> **Lưu ý:** Đổi `EMBEDDING_MODEL` yêu cầu re-ingest toàn bộ documents (`POST /ingest {"reset": true}`). +## Mục lục + +1. [Tổng quan stack](#1-tổng-quan-stack) +2. [Kiến trúc hệ thống](#2-kiến-trúc-hệ-thống) +3. [SDLC Workflow — 15 bước](#3-sdlc-workflow--15-bước) +4. [Yêu cầu hệ thống](#4-yêu-cầu-hệ-thống) +5. [Cấu trúc thư mục](#5-cấu-trúc-thư-mục) +6. [Thiết lập từ đầu — hướng dẫn từng bước](#6-thiết-lập-từ-đầu--hướng-dẫn-từng-bước) +7. [Biến môi trường chi tiết](#7-biến-môi-trường-chi-tiết) +8. [Agent API — tài liệu đầy đủ](#8-agent-api--tài-liệu-đầy-đủ) +9. [RAG API — tài liệu đầy đủ](#9-rag-api--tài-liệu-đầy-đủ) +10. [Artifact System](#10-artifact-system) +11. [Clarifier Regen Loop](#11-clarifier-regen-loop) +12. [Episodic Memory Logging](#12-episodic-memory-logging) +13. [Open WebUI — Custom Tools](#13-open-webui--custom-tools) +14. [Neo4j — Graph Enrichment](#14-neo4j--graph-enrichment) +15. [Điều chỉnh model](#15-điều-chỉnh-model) +16. [Vận hành Docker](#16-vận-hành-docker) +17. [Troubleshooting](#17-troubleshooting) +18. [Danh mục URL dịch vụ](#18-danh-mục-url-dịch-vụ) --- -## Kiến trúc +## 1. Tổng quan stack -``` -User → Open WebUI (8085) - │ - ├── yan_knowledge_base.py → RAG API (8090) - │ ├── Qdrant (vector search) - │ └── Neo4j (graph enrichment) - │ └── Ollama (embedding + chat) - │ - └── yan_agent_workflow.py → Agent API (8091) - ├── LangGraph SDLC Workflow (15 bước) - │ BA → PM → SA → TA → Designer → Team Lead - │ → FE → Mobile → DBA → BE → DA - │ → Tech Lead → Tester → DevSecOps → Clarifier - └── Ollama (per-role models) -``` +| Service | Image | Port (host) | Mục đích | +|---|---|---|---| +| `ollama` | `ollama/ollama:latest` | 11434 | LLM inference & embedding — chạy tất cả model AI | +| `qdrant` | `qdrant/qdrant:latest` | 6333 / 6334 | Vector database — lưu và tìm kiếm embedding (REST / gRPC) | +| `neo4j` | `neo4j:5-community` | 7474 / 7687 | Graph database — lưu quan hệ thực thể, tăng cường RAG | +| `rag-api` | build `./rag-api` | 8090 | FastAPI RAG service — ingest tài liệu và hỏi đáp AI | +| `agent-api` | build `./agent-api` | 8091 | FastAPI SDLC Agent Orchestrator — 15 AI agents phân tích & sinh code | +| `open-webui` | `ghcr.io/open-webui/open-webui` | 8085 | Giao diện chat kết nối Ollama & Qdrant | +| `watchtower` | `containrrr/watchtower` | — | Tự động pull & restart image mới nhất | +| `deunhealth` | `qmcgaw/deunhealth` | 9999 | Khởi động lại container khi healthcheck thất bại liên tục | -### SDLC Workflow (15 bước) +### Phân nhóm model theo chức năng -| Bước | Role | Tên | Model | Phụ thuộc | -|------|-----------------|----------------------------------------------|-------------------|----------------------------------------| -| 1 | `ba` | BA — Phân tích nghiệp vụ | BA_MODEL | — | -| 2 | `pm` | PM — Quản lý dự án & Lập kế hoạch | PM_MODEL | ba | -| 3 | `sa` | SA — Kiến trúc giải pháp | SA_MODEL | ba, pm | -| 4 | `ta` | TA — Kiến trúc kỹ thuật | TA_MODEL | ba, sa | -| 5 | `designer` | Designer — Thiết kế UI/UX | DESIGNER_MODEL | ba, sa, ta | -| 6 | `tl` | Team Lead — Lập kế hoạch task kỹ thuật | TL_MODEL | ba, sa, ta, designer | -| 7 | `fe` | FE — Kỹ thuật Frontend | FE_MODEL | ba, sa, ta, designer, tl | -| 8 | `mobile` | Mobile — Phát triển Mobile | MOBILE_MODEL | ba, sa, ta, designer, tl | -| 9 | `dba` | DBA — Kiến trúc cơ sở dữ liệu | DBA_MODEL | ba, sa, ta, tl | -| 10 | `be` | BE — Triển khai Backend | BE_MODEL | ba, sa, ta, fe, mobile, dba, tl | -| 11 | `da` | DA — Phân tích & Báo cáo dữ liệu | DA_MODEL | ba, sa, dba | -| 12 | `tech_lead` | Tech Lead — Review code & Tiêu chuẩn | TECH_LEAD_MODEL | sa, fe, mobile, be, dba | -| 13 | `tester` | Tester — Kiểm thử & Đảm bảo chất lượng | TESTER_MODEL | be, fe, mobile, tech_lead, designer | -| 14 | `devsecops` | DevSecOps — Hạ tầng, CI/CD & Bảo mật | DEVSECOPS_MODEL | sa, ta, tech_lead, tester | -| 15 | `clarifier` | Clarifier — Kiểm tra Assumption & Gap | CLARIFIER_MODEL | ba, pm, sa, ta, designer, tl, fe, mobile, dba, be, da, tech_lead, tester, devsecops | +| Nhóm | Env vars | Vai trò | +|---|---|---| +| Embedding | `EMBEDDING_MODEL` | Chuyển đổi văn bản → vector cho Qdrant | +| Chat RAG | `CHAT_MODEL` | Sinh câu trả lời từ context RAG trong rag-api | +| Planner | `CODING_PLANNER_MODEL` | Lập kế hoạch danh sách file trước khi sinh code | +| Reasoning (phân tích) | `BA_MODEL` `PM_MODEL` `SA_MODEL` `TA_MODEL` `DA_MODEL` | Các agent phân tích nghiệp vụ và kiến trúc | +| Team Lead | `TL_MODEL` | Lập kế hoạch task kỹ thuật cho các engineer | +| Coding (sinh code) | `FE_MODEL` `MOBILE_MODEL` `BE_MODEL` `DBA_MODEL` `TECH_LEAD_MODEL` `DEVSECOPS_MODEL` | Các agent sinh code, schema, manifest | +| Creative / QA | `DESIGNER_MODEL` `TESTER_MODEL` | Thiết kế UI/UX và viết test | +| Clarifier | `CLARIFIER_MODEL` | Kiểm tra toàn bộ output — phát hiện gap, mâu thuẫn, assumption | -Mỗi bước nhận output đã rút gọn từ các bước phụ thuộc và có thể bổ sung context từ RAG knowledge base. +> **Lưu ý:** Đổi `EMBEDDING_MODEL` yêu cầu re-ingest toàn bộ tài liệu (`POST /ingest {"reset": true}`). Mọi model khác chỉ cần `docker compose restart agent-api` hoặc `rag-api`. -### Kiến trúc hybrid RAG +--- + +## 2. Kiến trúc hệ thống ``` -/ask request - ├── Qdrant → vector search (cosine similarity, top-k chunks) - └── Neo4j → graph enrichment (entity co-occurrence traversal) - └── merge → Ollama (chat model) → answer +Người dùng + │ + ├── Open WebUI (8085) ──────────────────────────────────────────────┐ + │ │ │ + │ ├── yan_knowledge_base.py ──► RAG API (8090) │ + │ │ ├── Qdrant (6333) │ + │ │ │ └── vector search │ + │ │ ├── Neo4j (7687) │ + │ │ │ └── graph enrichment │ + │ │ └── Ollama (11434) │ + │ │ ├── embed model │ + │ │ └── chat model │ + │ │ │ + │ └── yan_agent_workflow.py ──► Agent API (8091) ────────────┘ + │ └── LangGraph SDLC Workflow + │ ├── 15 agent nodes + │ ├── Ollama (per-role models) + │ └── RAG API (rag_query_hint) + │ + └── Trực tiếp qua curl / HTTP client + ├── Agent API (8091) + └── RAG API (8090) ``` -- Mỗi `/ask` trước tiên lấy top-k chunks từ Qdrant, sau đó Neo4j tìm thêm chunks liên quan qua entity co-occurrence. -- Nếu `GRAPH_ENTITY_EXTRACTION=true`, khi ingest LLM sẽ extract entities từ mỗi document rồi lưu vào Neo4j. +### Kiến trúc Hybrid RAG (rag-api) ---- +``` +POST /ask ─┬─► Qdrant vector search (cosine similarity, top-k chunks) + │ + └─► Neo4j graph search (entity co-occurrence traversal) + │ + └── merge & rank ──► Ollama (chat model) ──► trả lời +``` -## Tính năng mới +Mỗi request `/ask`: +1. Embed câu hỏi bằng embedding model +2. Tìm top-k chunks trong Qdrant theo cosine similarity +3. Neo4j tìm thêm chunks liên quan qua entity co-occurrence (nếu `GRAPH_ENABLED=true`) +4. Gộp và loại trùng kết quả +5. Đưa vào LLM sinh câu trả lời cuối cùng ### Module-scoped RAG -Khi tổ chức tài liệu theo thư mục con `data/raw/{project}/{module}/`, mỗi chunk sẽ được gán `module` tự động: +Khi tổ chức tài liệu theo thư mục con, mỗi chunk được gán `module` tự động: ``` -data/raw/yanlib/auth/auth-prd.md → module=auth -data/raw/yanlib/billing/schema.md → module=billing -data/raw/yanlib/spec.md → module=yanlib (flat file) +data/raw/{project}/{module}/file.md → module = tên thư mục con +data/raw/{project}/file.md → module = project (flat file) ``` -Sử dụng filter `module` trong `/ask` để giới hạn search: +Ví dụ: +``` +data/raw/yanlib/auth/auth-prd.md → project=yanlib, module=auth +data/raw/yanlib/billing/schema.md → project=yanlib, module=billing +data/raw/yanlib/spec.md → project=yanlib, module=yanlib +``` + +Dùng filter `module` trong `/ask` để giới hạn search phạm vi nhỏ hơn, tăng độ chính xác: ```bash curl -s -X POST http://localhost:8090/ask \ @@ -107,277 +127,457 @@ curl -s -X POST http://localhost:8090/ask \ -d '{"question": "JWT refresh token flow?", "project": "yanlib", "module": "auth"}' | jq ``` -Mỗi `SourceItem` trong response chứa thêm: `module`, `doc_type` (prd/schema/api/architecture/…), `chunk_type` (paragraph/table/code). - -### Role-specific RAG queries (rag_query_hint) - -Mỗi SDLC agent có `rag_query_hint` riêng để xây dựng câu truy vấn RAG chính xác hơn thay vì dùng nguyên `user_input`. Ví dụ: -- **PM**: tìm *"mục tiêu dự án, phạm vi, stakeholder, ràng buộc, rủi ro, timeline, OKR"* -- **BA**: tìm *"yêu cầu chức năng, user story, acceptance criteria, quy tắc nghiệp vụ"* -- **SA**: tìm *"kiến trúc hệ thống, API contracts, data model, tích hợp, pattern, bảo mật"* - -Thiết kế này cải thiện độ chính xác truy xuất cho từng vai trò SDLC mà không cần thay đổi `user_input`. +--- -### Episodic Memory Logging +## 3. SDLC Workflow — 15 bước -Mỗi lần workflow hoàn thành, agent-api tự động ghi log JSONL vào: +Agent API điều phối một pipeline 15 AI agents chạy tuần tự theo mô hình LangGraph StateGraph. Mỗi agent nhận output rút gọn của các agent phụ thuộc làm context, đồng thời có thể truy vấn RAG knowledge base với `rag_query_hint` riêng để tăng độ chính xác. ``` -data/memory/episodic/workflow_runs.jsonl +BA → PM → SA → TA → Designer → Team Lead +→ FE → Mobile → DBA → BE → DA +→ Tech Lead → Tester → DevSecOps → Clarifier ``` -Mỗi dòng là một JSON object: - -```json -{ - "workflow_id": "a1b2c3d4-...", - "project": "yanlib", - "user_input": "Xây dựng checkout flow...", - "completed_steps": ["ba","pm","sa","ta","designer","tl","fe","mobile","dba","be","da","tech_lead","tester","devsecops","clarifier"], - "steps_count": 15, - "status": "completed", - "error": null, - "duration_seconds": 842.5, - "timestamp": "2025-01-01T12:00:00+00:00" -} +| Bước | Role | Tên đầy đủ | Model | Phụ thuộc | Đầu ra chính | +|---|---|---|---|---|---| +| 1 | `ba` | Business Analysis | `BA_MODEL` | — | BRD, user stories, RTM, WBS | +| 2 | `pm` | Project Management | `PM_MODEL` | ba | Sprint plan, RAID log, milestone, roadmap | +| 3 | `sa` | Solution Architecture | `SA_MODEL` | ba, pm | C4 diagrams, API contracts, ADRs | +| 4 | `ta` | Technical Architecture | `TA_MODEL` | ba, sa | Tech stack, TDR, trade-off analysis | +| 5 | `designer` | UI/UX Design | `DESIGNER_MODEL` | ba, sa, ta | Wireframes, design system, component spec | +| 6 | `tl` | Team Lead / Task Planning | `TL_MODEL` | ba, sa, ta, designer | Task boards FE/Mobile/BE/DBA theo sprint | +| 7 | `fe` | Frontend Engineering | `FE_MODEL` | ba, sa, ta, designer, tl | React/Vue/Angular code + checklist | +| 8 | `mobile` | Mobile Engineering | `MOBILE_MODEL` | ba, sa, ta, designer, tl | Flutter/React Native code + checklist | +| 9 | `dba` | Database Architecture | `DBA_MODEL` | ba, sa, ta, tl | SQL schema / Mongoose models, indexes + checklist | +| 10 | `be` | Backend Implementation | `BE_MODEL` | ba, sa, ta, fe, mobile, dba, tl | NestJS/FastAPI/Express code + checklist | +| 11 | `da` | Data Analysis & Reporting | `DA_MODEL` | ba, sa, dba | KPI, dashboard spec, SQL/aggregation queries | +| 12 | `tech_lead` | Code Review & Standards | `TECH_LEAD_MODEL` | sa, fe, mobile, be, dba | ARCHITECTURE.md, security review, refactor plan | +| 13 | `tester` | Testing & QA | `TESTER_MODEL` | be, fe, mobile, tech_lead, designer | Test cases, UAT checklist, edge case matrix | +| 14 | `devsecops` | Infrastructure & CI/CD | `DEVSECOPS_MODEL` | sa, ta, tech_lead, tester | Dockerfile, K8s manifests, CI/CD pipeline YAML | +| 15 | `clarifier` | Cross-Role Gap Review | `CLARIFIER_MODEL` | tất cả 14 agent trước | Audit report, contradiction log, regen list | + +### Cơ chế sinh code (Artifact Roles) + +Các agent `fe`, `mobile`, `be`, `dba`, `da`, `tech_lead`, `devsecops` sử dụng quy trình sinh code **2 pha**: + +**Pha 1 — Lập kế hoạch file:** +- `CODING_PLANNER_MODEL` (model nhẹ, ví dụ `granite3.3:2b`) nhận danh sách task từ TL output +- Sinh JSON array các file cần tạo: `[{"filename": "src/auth/login.tsx", "description": "...", "language": "typescript"}]` +- Tối đa `MAX_FILES_PER_ROLE` file mỗi role + +**Pha 2 — Sinh từng file:** +- Mỗi file được sinh độc lập bằng coding model với context tập trung +- Nếu còn task TL chưa được cover → **pha follow-up**: lên kế hoạch thêm file và sinh tiếp + +**Task Completion Checklist:** +Cuối output của mỗi engineer agent luôn có bảng checklist đối chiếu từng task TL với file đã sinh: +``` +| # | Task | Priority | Status | File(s) / Section | +|---|------|----------|--------|-------------------| +| 1 | Setup project scaffolding | P0 | ✅ Done | `src/main.tsx`, `vite.config.ts` | +| 2 | Implement login form | P0 | ✅ Done | `src/pages/Login.tsx` | +| 3 | API integration layer | P1 | ⏳ Addressed in output | — | ``` -File này có thể dùng làm dataset cho fine-tune, phân tích hiệu suất, hoặc làm context lịch sử cho các workflow kế tiếp. - -### Input Sanitization +### Clarifier Regen Loop -`user_input` ở `/agent/{role}` và `/workflow/run` được tự động: -- Loại bỏ ký tự điều khiển (trừ newline/tab) -- Cắt ngắn tại 10 000 ký tự nếu vượt quá +Sau khi workflow hoàn tất, Clarifier phân tích §10 "Recommended Re-generation List" của chính nó. Nếu có agent nào cần re-generate: +1. Re-run từng agent được chỉ định (theo thứ tự WORKFLOW_STEPS) với output đã cập nhật +2. Re-run Clarifier lại để đánh giá kết quả +3. Lặp tối đa `CLARIFIER_REGEN_LOOPS` lần (mặc định = 1) -Giá trị sau sanitize được lưu vào `WorkflowRecord` và log. +Tắt tính năng: `CLARIFIER_REGEN_LOOPS=0` trong `.env`. +--- +## 4. Yêu cầu hệ thống -## Yêu cầu hệ thống +| Thành phần | Yêu cầu tối thiểu | Ghi chú | +|---|---|---| +| Docker Engine | ≥ 24.x | Docker Desktop (Windows/Mac) hoặc Docker Engine + Compose v2 (Linux) | +| RAM | ≥ 16 GB | Model 7B cần ~8 GB; model 35B cần ~24 GB với quantization | +| Dung lượng ổ cứng | ≥ 50 GB | Model + Docker images + data | +| GPU (tùy chọn) | NVIDIA với CUDA | Cải thiện tốc độ inference đáng kể; driver + NVIDIA Container Toolkit | +| CPU | ≥ 8 cores | Dùng khi không có GPU hoặc chạy song song nhiều model nhỏ | +**Định cỡ model theo tài nguyên:** -- Docker Desktop (Windows) hoặc Docker Engine + Compose plugin (Linux/Mac) -- Tài nguyên CPU/RAM/GPU phù hợp với model và mức tải bạn cấu hình trong `.env` -- Nếu bật GPU: cài driver và runtime container tương thích trên máy host +| RAM / VRAM | Khuyến nghị | +|---|---| +| 16 GB | Model 7B–8B (quantized Q4) cho tất cả roles | +| 32 GB | Model 14B–24B cho reasoning; model 7B cho coding | +| 64 GB+ | Model 35B+ cho reasoning; model 14B–32B cho coding | --- -## Cấu trúc thư mục +## 5. Cấu trúc thư mục ``` Ollama-Stack/ -├── .env # Single source of truth cho tất cả config -├── docker-compose.yml +├── .env # Nguồn cấu hình duy nhất (single source of truth) +├── docker-compose.yml # Định nghĩa toàn bộ stack +│ ├── data/ -│ ├── raw/ # Tài liệu RAG — tổ chức theo project (và module tùy chọn) -│ │ ├── yanlib/ # → collection: yan_raw_docs__yanlib -│ │ │ ├── auth/ # → module=auth (subdirectory = module) -│ │ │ ├── billing/ # → module=billing -│ │ │ ├── marketplace/ # → module=marketplace -│ │ │ └── *.md # → module=yanlib (flat file = dùng project làm module) -│ │ └── / # Thêm bất kỳ project nào -│ └── memory/ -│ └── episodic/ -│ └── workflow_runs.jsonl # Log tự động mỗi workflow run +│ ├── raw/ # Tài liệu nguồn cho RAG +│ │ ├── {project}/ → Qdrant collection: yan_raw_docs__{project} +│ │ │ ├── {module}/ → chunk.module = {module} +│ │ │ │ └── *.md / *.pdf ... → tài liệu được ingest +│ │ │ └── *.md → chunk.module = {project} (flat) +│ │ └── ... +│ ├── memory/ +│ │ └── episodic/ +│ │ └── workflow_runs.jsonl → Log tự động mỗi workflow run (JSONL) +│ └── artifacts/ +│ └── {workflow_id}/ +│ └── {role}/ +│ ├── _output.md → Toàn bộ markdown output của agent +│ ├── src/Login.tsx → File code được trích xuất +│ └── ... +│ ├── rag-api/ -│ ├── app.py # FastAPI endpoints (ingest, ask, projects, graph) -│ ├── ingest.py # Document ingestion: chunk → embed → Qdrant upsert -│ ├── graph.py # Neo4j GraphRAG layer +│ ├── app.py → FastAPI endpoints: /ingest /ask /projects /graph/* +│ ├── ingest.py → Pipeline: load → split → embed → upsert Qdrant + Neo4j +│ ├── graph.py → Neo4j GraphRAG: entity extraction & co-occurrence │ ├── requirements.txt │ └── Dockerfile +│ ├── agent-api/ -│ ├── app.py # FastAPI SDLC Agent Orchestrator API -│ ├── agents.py # 15 cấu hình agent (model, system prompt, deps, rag_query_hint) -│ ├── workflow.py # LangGraph StateGraph — 15-bước SDLC workflow +│ ├── app.py → FastAPI SDLC orchestrator: /workflow/run /agent/{role} /artifacts +│ ├── agents.py → 15 AgentConfig: model, system_prompt, depends_on, rag_query_hint +│ ├── workflow.py → LangGraph StateGraph: node factory, context builder, artifact generator +│ ├── artifacts.py → Trích xuất file code từ markdown output, lưu vào disk │ ├── requirements.txt -│ └── Dockerfile +│ ├── Dockerfile +│ └── static/ +│ └── workflow.html → SDLC Workflow UI (single-page app) +│ └── open-webui-tools/ - ├── yan_knowledge_base.py # Open WebUI tool: query RAG knowledge base - └── yan_agent_workflow.py # Open WebUI tool: run SDLC agent workflow + ├── yan_knowledge_base.py → Open WebUI tool: query RAG knowledge base + └── yan_agent_workflow.py → Open WebUI tool: chạy SDLC workflow hoặc agent đơn lẻ ``` -Mỗi subfolder trong `data/raw/` là một **project** độc lập → Qdrant collection riêng → tránh noise khi query. - -Cấu trúc thư mục con trong project: -- `data/raw/{project}/{module}/file.md` → chunk có `module=` -- `data/raw/{project}/file.md` (flat) → chunk có `module=` - -Nhờ đó `/ask` có thể lọc kết quả theo module để tăng độ chính xác: - -**File formats được hỗ trợ:** `.md`, `.txt`, `.pdf`, `.docx`, `.csv`, `.json`, `.jsonl`, `.yaml`, `.yml`, `.xml`, `.html`, `.py`, `.js`, `.ts`, `.go`, `.rs`, `.java`, `.cs`, `.sh`, `.sql`, `.log` +**Định dạng file được hỗ trợ khi ingest:** +`.md` `.txt` `.pdf` `.docx` `.csv` `.json` `.jsonl` `.yaml` `.yml` `.xml` `.html` `.py` `.js` `.ts` `.go` `.rs` `.java` `.cs` `.sh` `.sql` `.log` --- -## Thiết lập ban đầu +## 6. Thiết lập từ đầu — hướng dẫn từng bước -### 1. Chuẩn bị `.env` +### Bước 1 — Chuẩn bị file `.env` -```bash -cat .env -``` - -Các biến quan trọng cần kiểm tra: +Điền các biến bắt buộc vào `.env`: ```env -# ─── SDLC Agent Models ─────────────────────────────────────────────────────── -# Thay đổi model tại đây rồi restart service tương ứng (không cần rebuild) -EMBEDDING_MODEL= -CHAT_MODEL= -CODING_PLANNER_MODEL= - -# Reasoning agents (BA / PM / SA / TA / DA) -BA_MODEL= -PM_MODEL= -SA_MODEL= -TA_MODEL= -DA_MODEL= - -# Team Lead planning agent -TL_MODEL= - -# Coding agents (FE / Mobile / BE / DBA / Tech Lead / DevSecOps) -FE_MODEL= -MOBILE_MODEL= -BE_MODEL= -DBA_MODEL= -TECH_LEAD_MODEL= -DEVSECOPS_MODEL= -CLARIFIER_MODEL= - -# Creative/Test agents -DESIGNER_MODEL= -TESTER_MODEL= - -# ─── RAG API ───────────────────────────────────────────────────────────────── -RAG_TOP_K=4 - -# ─── Agent API ─────────────────────────────────────────────────────────────── -MEMORY_DIR=/data/memory # thư mục ghi episodic log (workflow_runs.jsonl) -ARTIFACT_DIR=/data/artifacts # thư mục lưu code artifacts do agent sinh ra - -# ─── Bảo mật — bắt buộc đổi trước khi deploy ───────────────────────────── +# ─── Ollama ────────────────────────────────────────────────────────────────── +OLLAMA_HOST=0.0.0.0 +OLLAMA_ORIGINS=* +OLLAMA_CONTEXT_LENGTH=32768 +OLLAMA_MAX_LOADED_MODELS=2 +OLLAMA_NUM_PARALLEL=2 +OLLAMA_KEEP_ALIVE=5m +OLLAMA_REQUEST_TIMEOUT=1200 + +# ─── Models ────────────────────────────────────────────────────────────────── +EMBEDDING_MODEL=nomic-embed-text +CHAT_MODEL=qwen2.5:7b +CODING_PLANNER_MODEL=granite3.3:2b + +BA_MODEL=qwen2.5:14b +PM_MODEL=qwen2.5:14b +SA_MODEL=qwen2.5:14b +TA_MODEL=qwen2.5:14b +DA_MODEL=qwen2.5:14b + +TL_MODEL=qwen2.5:7b + +FE_MODEL=qwen2.5-coder:7b +MOBILE_MODEL=qwen2.5-coder:7b +BE_MODEL=qwen2.5-coder:7b +DBA_MODEL=qwen2.5-coder:7b +TECH_LEAD_MODEL=qwen2.5-coder:7b +DEVSECOPS_MODEL=qwen2.5-coder:7b + +DESIGNER_MODEL=gemma2:9b +TESTER_MODEL=mistral:7b +CLARIFIER_MODEL=qwen2.5:14b + +# ─── RAG ───────────────────────────────────────────────────────────────────── +OLLAMA_BASE_URL=http://ollama:11434 +RAG_API_URL=http://rag-api:8090 +RAG_TOP_K=5 +RAG_TIMEOUT=600 +RAG_LOG_LEVEL=INFO +RAW_DATA_DIR=/data/raw +COLLECTION_NAME=yan_raw_docs +QDRANT_URL=http://qdrant:6333 +CHUNK_SIZE=1000 +CHUNK_OVERLAP=200 +UPSERT_BATCH_SIZE=64 +INGEST_EMBED_WORKERS=2 + +# ─── Neo4j ─────────────────────────────────────────────────────────────────── +NEO4J_URI=bolt://neo4j:7687 +NEO4J_USERNAME=neo4j NEO4J_PASSWORD=changeme_in_production -WATCHTOWER_HTTP_API_TOKEN=... +NEO4J_DATABASE=neo4j +GRAPH_ENABLED=true +GRAPH_ENTITY_EXTRACTION=false + +# ─── Open WebUI ────────────────────────────────────────────────────────────── +OPEN_WEBUI_PORT=8085 +WEBUI_AUTH=false +WEBUI_NAME=YAN AI +VECTOR_DB=qdrant +QDRANT_PREFER_GRPC=false +ENABLE_QDRANT_MULTITENANCY_MODE=false +QDRANT_COLLECTION_PREFIX=yan_raw_docs +RAG_EMBEDDING_ENGINE=ollama + +# ─── Watchtower ────────────────────────────────────────────────────────────── +WATCHTOWER_HTTP_API_TOKEN=your_secure_token_here +WATCHTOWER_SCHEDULE=0 * * * * * -# ─── Ollama performance ─────────────────────────────────────────────────────── -OLLAMA_MAX_LOADED_MODELS= # số model giữ đồng thời -OLLAMA_NUM_PARALLEL= # số request Ollama xử lý song song -OLLAMA_KEEP_ALIVE= # thời gian giữ model trong bộ nhớ -OLLAMA_CONTEXT_LENGTH= # context window cho mỗi LLM call trong agent-api -OLLAMA_REQUEST_TIMEOUT= # timeout cho Ollama call trong agent-api -MAX_FILES_PER_ROLE= # số file tối đa mỗi coding agent sinh trong 1 workflow run - -# Số batch embed/upsert chạy đồng thời khi ingest (asyncio pipeline). -# Nên điều chỉnh theo tài nguyên thực tế và mức song song mong muốn. -INGEST_EMBED_WORKERS= +# ─── Agent API ─────────────────────────────────────────────────────────────── +MAX_FILES_PER_ROLE=6 +CLARIFIER_REGEN_LOOPS=1 ``` -### 2. Khởi động stack +### Bước 2 — Khởi động stack ```bash docker compose up -d ``` -Lần khởi động đầu tiên sẽ pull toàn bộ image (dung lượng có thể vài GB). Theo dõi bằng: +Lần đầu sẽ pull image (~2–5 GB). Theo dõi tiến trình: ```bash docker compose logs -f ``` -### 3. Pull models Ollama +Chờ đến khi tất cả container ở trạng thái `healthy`: + +```bash +docker compose ps +``` + +### Bước 3 — Pull models Ollama + +Pull tất cả model đang khai báo trong `.env` (tự động loại trùng): ```bash -# Pull tất cả model đang khai báo trong .env (loại trùng tự động) grep -E '^(EMBEDDING_MODEL|CHAT_MODEL|CODING_PLANNER_MODEL|BA_MODEL|PM_MODEL|SA_MODEL|TA_MODEL|DA_MODEL|TL_MODEL|FE_MODEL|MOBILE_MODEL|BE_MODEL|DBA_MODEL|TECH_LEAD_MODEL|DEVSECOPS_MODEL|TESTER_MODEL|DESIGNER_MODEL|CLARIFIER_MODEL)=' .env \ | cut -d= -f2 \ | sort -u \ | xargs -I {} docker exec ollama ollama pull "{}" ``` -Xác nhận các model đã sẵn sàng: +Xác nhận: ```bash docker exec ollama ollama list ``` -### 4. Tổ chức tài liệu +### Bước 4 — Tổ chức tài liệu RAG -Đặt tài liệu vào thư mục con theo project (và tùy chọn theo module): +Đặt tài liệu vào thư mục con theo cấu trúc `data/raw/{project}/{module}/`: -``` -data/raw/yanlib/auth/auth-prd.md # module=auth -data/raw/yanlib/billing/billing-plans.md # module=billing -data/raw/yanlib/marketplace-schema.md # module=yanlib (flat) +```bash +mkdir -p data/raw/myproject/auth +mkdir -p data/raw/myproject/billing +mkdir -p data/raw/myproject/api + +cp docs/auth-prd.md data/raw/myproject/auth/ +cp docs/billing-schema.md data/raw/myproject/billing/ +cp docs/api-contracts.md data/raw/myproject/api/ +cp docs/overview.md data/raw/myproject/ ``` -Hoặc dùng cấu trúc phẳng nếu không cần lọc theo module: +Cấu trúc kết quả: ``` -data/raw/myproject/spec-v1.md -data/raw/myproject/architecture.md +data/raw/myproject/ +├── auth/ +│ └── auth-prd.md → module=auth +├── billing/ +│ └── billing-schema.md → module=billing +├── api/ +│ └── api-contracts.md → module=api +└── overview.md → module=myproject (flat) ``` -### 5. Ingest tài liệu vào Qdrant +### Bước 5 — Ingest tài liệu ```bash -# Ingest tất cả projects +# Ingest tất cả project curl -s -X POST http://localhost:8090/ingest | jq # Hoặc ingest từng project curl -s -X POST http://localhost:8090/ingest \ -H "Content-Type: application/json" \ - -d '{"project": "auth"}' | jq + -d '{"project": "myproject"}' | jq +``` + +Kết quả thành công: + +```json +{ + "status": "ok", + "projects": { + "myproject": { + "upserted": 142, + "skipped": 0, + "errors": 0, + "graph_chunks_saved": 142 + } + } +} ``` -### 6. Kiểm tra +### Bước 6 — Kiểm tra hệ thống ```bash -# RAG API health +# Kiểm tra RAG API curl -s http://localhost:8090/health | jq -# Agent API health +# Kiểm tra Agent API curl -s http://localhost:8091/health | jq -# Xem agents và models đang dùng +# Test RAG query +curl -s -X POST http://localhost:8090/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "Auth flow hoạt động như thế nào?", "project": "myproject"}' | jq .answer + +# Xem danh sách agent và model đang dùng curl -s http://localhost:8091/agents | jq +``` -# Test RAG -curl -s -X POST http://localhost:8090/ask \ +### Bước 7 — Chạy SDLC Workflow đầu tiên + +```bash +# Submit workflow +curl -s -X POST http://localhost:8091/workflow/run \ -H "Content-Type: application/json" \ - -d '{"question": "Auth flow hoạt động như thế nào?"}' | jq .answer + -d '{ + "user_input": "Xây dựng hệ thống đăng nhập và quản lý phiên cho SaaS platform", + "project": "myproject", + "rag_enabled": true, + "rag_top_k": 5, + "tech_stack": [ + "Backend: NestJS (Node.js, TypeScript)", + "Frontend: React + TypeScript + TanStack Query", + "Mobile: React Native", + "Database: PostgreSQL", + "Cache: Redis", + "Infra: Docker, Kubernetes" + ] + }' | jq ``` +```json +{ + "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "status": "pending", + "message": "Workflow đã được xếp hàng. Poll GET /workflow/a1b2c3d4-... để kiểm tra trạng thái." +} +``` + +```bash +# Poll trạng thái (workflow thường mất 15–60 phút tùy model) +watch -n 30 'curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890 | jq .status' +``` + +### Bước 8 — Thiết lập Open WebUI Tools (tùy chọn) + +1. Mở [http://localhost:8085](http://localhost:8085) +2. Vào **Admin Panel → Tools → "+"** +3. Dán nội dung `open-webui-tools/yan_knowledge_base.py` → **Save** +4. Lặp lại cho `open-webui-tools/yan_agent_workflow.py` +5. Bật tool trong **Model Settings → Tools** + --- -## Danh mục URL dịch vụ - -| Service | URL | Mô tả | -|----------------------|----------------------------------------|---------------------------------| -| **Open WebUI** | http://localhost:8085 | Chat UI | -| **RAG API** | http://localhost:8090 | FastAPI root | -| **RAG API Docs** | http://localhost:8090/docs | Swagger UI | -| **RAG API ReDoc** | http://localhost:8090/redoc | ReDoc | -| **Agent API** | http://localhost:8091 | SDLC Agent Orchestrator root | -| **Agent API UI** | http://localhost:8091/ui | SDLC Workflow UI | -| **Agent API Docs** | http://localhost:8091/docs | Swagger UI | -| **Ollama API** | http://localhost:11434 | LLM inference API | -| **Qdrant UI** | http://localhost:6333/dashboard | Vector DB dashboard | -| **Qdrant REST** | http://localhost:6333 | Qdrant REST API | -| **Qdrant gRPC** | localhost:6334 | Qdrant gRPC | -| **Neo4j Browser** | http://localhost:7474 | Graph DB browser UI | -| **Neo4j Bolt** | bolt://localhost:7687 | Neo4j Bolt (driver/tools) | -| **Deunhealth** | http://localhost:9999 | Health watchdog | -| **Watchtower** | http://localhost:8080 *(internal only)*| Metrics (nội bộ Docker) | +## 7. Biến môi trường chi tiết + +### Nhóm Ollama + +| Biến | Mặc định | Mô tả | +|---|---|---| +| `OLLAMA_HOST` | `0.0.0.0` | Interface Ollama lắng nghe | +| `OLLAMA_ORIGINS` | `*` | CORS origins cho Ollama API | +| `OLLAMA_CONTEXT_LENGTH` | `32768` | Context window (tokens) cho mọi LLM call trong agent-api | +| `OLLAMA_MAX_LOADED_MODELS` | `2` | Số model giữ đồng thời trong VRAM/RAM | +| `OLLAMA_NUM_PARALLEL` | `2` | Số request Ollama xử lý song song | +| `OLLAMA_KEEP_ALIVE` | `5m` | Thời gian giữ model trong bộ nhớ sau request cuối | +| `OLLAMA_REQUEST_TIMEOUT` | `1200` | Timeout (giây) cho mỗi lần gọi LLM trong agent-api | +| `OLLAMA_FLASH_ATTENTION` | `1` | Bật Flash Attention (tăng tốc trên GPU hỗ trợ) | + +### Nhóm Models + +| Biến | Mặc định docker-compose | Mô tả | +|---|---|---| +| `EMBEDDING_MODEL` | — | Model embedding cho Qdrant | +| `CHAT_MODEL` | — | Model chat cho rag-api `/ask` | +| `CODING_PLANNER_MODEL` | `granite3.3:2b` | Model nhẹ lập kế hoạch danh sách file | +| `BA_MODEL` | — | BA agent — phân tích nghiệp vụ | +| `PM_MODEL` | — | PM agent — quản lý dự án | +| `SA_MODEL` | — | SA agent — kiến trúc giải pháp | +| `TA_MODEL` | — | TA agent — kiến trúc kỹ thuật | +| `DA_MODEL` | — | DA agent — phân tích dữ liệu | +| `TL_MODEL` | `phi4-mini` | Team Lead agent — lập kế hoạch task | +| `FE_MODEL` | — | FE agent — frontend code | +| `MOBILE_MODEL` | — | Mobile agent — mobile code | +| `BE_MODEL` | — | BE agent — backend code | +| `DBA_MODEL` | — | DBA agent — database schema | +| `TECH_LEAD_MODEL` | — | Tech Lead agent — code review | +| `DEVSECOPS_MODEL` | — | DevSecOps agent — infra / CI-CD | +| `DESIGNER_MODEL` | — | Designer agent — UI/UX | +| `TESTER_MODEL` | — | Tester agent — test cases | +| `CLARIFIER_MODEL` | `qwen3.6:35b` | Clarifier agent — gap analysis | + +### Nhóm RAG API + +| Biến | Mặc định | Mô tả | +|---|---|---| +| `RAW_DATA_DIR` | `/data/raw` | Thư mục chứa tài liệu cần ingest | +| `QDRANT_URL` | `http://qdrant:6333` | URL Qdrant REST API | +| `COLLECTION_NAME` | `yan_raw_docs` | Tiền tố tên collection: `yan_raw_docs__{project}` | +| `CHUNK_SIZE` | `1000` | Kích thước chunk (ký tự) khi chia tài liệu | +| `CHUNK_OVERLAP` | `200` | Overlap giữa các chunk kề nhau | +| `UPSERT_BATCH_SIZE` | `64` | Số vector upsert vào Qdrant mỗi batch | +| `RAG_TOP_K` | `5` | Số chunk trả về mỗi query (default) | +| `INGEST_EMBED_WORKERS` | `1` | Số request embed asyncio đồng thời khi ingest | +| `GRAPH_ENABLED` | `false` | Bật Neo4j graph enrichment | +| `GRAPH_ENTITY_EXTRACTION` | `false` | LLM extract entity khi ingest (chậm hơn nhưng tăng độ phong phú graph) | + +### Nhóm Agent API + +| Biến | Mặc định | Mô tả | +|---|---|---| +| `RAG_TIMEOUT` | `600` | Timeout (giây) cho mỗi lần gọi RAG `/ask` từ agent | +| `MAX_FILES_PER_ROLE` | `6` | Số file tối đa mỗi coding agent sinh trong 1 workflow | +| `CLARIFIER_REGEN_LOOPS` | `1` | Số vòng lặp Clarifier re-generation (0 = tắt) | + +### Nhóm Neo4j + +| Biến | Mặc định | Mô tả | +|---|---|---| +| `NEO4J_URI` | `bolt://neo4j:7687` | Bolt URI kết nối Neo4j | +| `NEO4J_USERNAME` | `neo4j` | Username Neo4j | +| `NEO4J_PASSWORD` | — | Password Neo4j — **phải đổi trước khi deploy** | +| `NEO4J_DATABASE` | `neo4j` | Tên database | --- -## Agent API — Curl Reference +## 8. Agent API — tài liệu đầy đủ + +**Base URL:** `http://localhost:8091` + +### GET /health -### Health & Discovery +Kiểm tra trạng thái service và cấu hình runtime. ```bash -# Kiểm tra agent-api đang chạy curl -s http://localhost:8091/health | jq ``` @@ -391,106 +591,240 @@ curl -s http://localhost:8091/health | jq } ``` +### GET /agents + +Liệt kê cấu hình tất cả agent: step_id, name, model, depends_on. + ```bash -# Xem tất cả agents và models đang dùng curl -s http://localhost:8091/agents | jq ``` -### Chạy single-step agent +```json +{ + "ba": { "step_id": 1, "name": "BA Agent — Business Analysis", "model": "qwen2.5:14b", "depends_on": [] }, + "pm": { "step_id": 2, "name": "PM Agent — Project Management", "model": "qwen2.5:14b", "depends_on": ["ba"] } +} +``` + +### GET /ui + +Giao diện web để theo dõi và tương tác với SDLC workflow. + +``` +http://localhost:8091/ui +``` + +### POST /agent/{role} + +Chạy đồng bộ một agent đơn lẻ. Hữu ích để test từng role hoặc ghép nối thủ công. + +**Request body:** + +| Trường | Kiểu | Bắt buộc | Mô tả | +|---|---|---|---| +| `user_input` | string | ✅ | Mục tiêu hoặc context đầu vào | +| `project` | string\|null | | RAG project filter | +| `extra_context` | string\|null | | Context bổ sung thêm vào prompt | +| `prev_outputs` | object\|null | | Output của các agent trước `{"role": "text"}` | +| `tech_stack` | string[]\|null | | Danh sách tech stack bắt buộc | +| `rag_enabled` | bool | | Có query RAG không (mặc định: true) | +| `rag_top_k` | int | | Số kết quả RAG (mặc định: env `RAG_TOP_K`) | + +**Ví dụ — chạy BA agent:** ```bash -# Chạy riêng BA agent curl -s -X POST http://localhost:8091/agent/ba \ -H "Content-Type: application/json" \ -d '{ - "user_input": "Xây dựng hệ thống quản lý billing cho SaaS platform", - "project": "billing", + "user_input": "Xây dựng hệ thống thanh toán cho marketplace", + "project": "myproject", "rag_enabled": true, "rag_top_k": 5 }' | jq .output +``` -# Chạy PM agent với output BA đã có -curl -s -X POST http://localhost:8091/agent/pm \ +**Ví dụ — chạy FE agent với tech_stack cụ thể:** + +```bash +curl -s -X POST http://localhost:8091/agent/fe \ -H "Content-Type: application/json" \ -d '{ - "user_input": "Xây dựng hệ thống quản lý billing cho SaaS platform", - "project": "billing", - "prev_outputs": { - "ba": "" - } + "user_input": "Xây dựng trang quản lý đơn hàng cho admin", + "tech_stack": ["React", "TypeScript", "TanStack Query", "Zustand", "Tailwind CSS"], + "rag_enabled": false }' | jq .output ``` -### Chạy full SDLC Workflow (async) +**Ví dụ — chạy SA agent với output BA đã có:** + +```bash +BA_OUT=$(curl -s -X POST http://localhost:8091/agent/ba \ + -H "Content-Type: application/json" \ + -d '{"user_input": "Xây dựng hệ thống thanh toán", "rag_enabled": false}' | jq -r .output) + +curl -s -X POST http://localhost:8091/agent/sa \ + -H "Content-Type: application/json" \ + -d "{ + \"user_input\": \"Xây dựng hệ thống thanh toán\", + \"prev_outputs\": {\"ba\": $(echo \"$BA_OUT\" | jq -R -s .)}, + \"tech_stack\": [\"NestJS\", \"PostgreSQL\", \"Redis\", \"Kubernetes\"] + }" | jq .output +``` + +**Response:** + +```json +{ + "role": "fe", + "name": "FE Agent — Frontend Engineering", + "model": "qwen2.5-coder:7b", + "output": "## FE Agent\n\n### FILE: src/pages/OrderManagement.tsx\n```typescript\n..." +} +``` + +### POST /workflow/run + +Gửi SDLC workflow 15 bước chạy nền (async). Trả về `workflow_id` ngay lập tức. + +**Request body:** + +| Trường | Kiểu | Bắt buộc | Mô tả | +|---|---|---|---| +| `user_input` | string | ✅ | Mục tiêu kinh doanh / ý tưởng project | +| `project` | string\|null | | RAG project filter | +| `rag_enabled` | bool | | Có query RAG mỗi bước không (mặc định: true) | +| `rag_top_k` | int | | Số kết quả RAG mỗi agent | +| `tech_stack` | string[]\|null | | Danh sách tech stack bắt buộc — binding cho tất cả agents | + +**Ví dụ — workflow cơ bản:** ```bash -# 1a. Submit workflow cơ bản → nhận workflow_id curl -s -X POST http://localhost:8091/workflow/run \ -H "Content-Type: application/json" \ -d '{ - "user_input": "Xây dựng tính năng checkout và payment cho marketplace", - "project": "marketplace", + "user_input": "Xây dựng module quản lý tenant và billing cho SaaS platform đa khách hàng", + "project": "myproject", "rag_enabled": true, "rag_top_k": 5 }' | jq +``` + +**Ví dụ — workflow với tech_stack đầy đủ:** -# 1b. Submit workflow với tech_stack bắt buộc (optional) -# Mỗi agent sẽ nhận danh sách này trong context "## Công nghệ bắt buộc" +```bash curl -s -X POST http://localhost:8091/workflow/run \ -H "Content-Type: application/json" \ -d '{ - "user_input": "Xây dựng tính năng checkout và payment cho marketplace", + "user_input": "Xây dựng hệ thống checkout và thanh toán cho marketplace B2B", "project": "marketplace", "rag_enabled": true, "rag_top_k": 5, "tech_stack": [ - "Backend: NestJS (Node.js)", - "Frontend: React + TypeScript", - "Mobile: React Native", - "Database: PostgreSQL", - "Cache: Redis", + "Backend: NestJS (Node.js, TypeScript)", + "Frontend: React 18 + TypeScript + TanStack Query v5 + Zustand", + "Mobile: React Native (Expo)", + "Database: PostgreSQL 16", + "Cache: Redis 7", "Message Queue: RabbitMQ", - "Infra: Docker, Kubernetes, AWS ECS" + "Search: Elasticsearch", + "Infra: Docker, Kubernetes (AWS EKS)", + "CI/CD: GitHub Actions" ] }' | jq ``` +**Response:** + ```json { - "workflow_id": "a1b2c3d4-...", + "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "pending", "message": "Workflow đã được xếp hàng. Poll GET /workflow/a1b2c3d4-... để kiểm tra trạng thái." } ``` +### GET /workflow/{workflow_id} + +Kiểm tra trạng thái hoặc lấy kết quả đã hoàn thành. + ```bash -# 2. Poll trạng thái (lặp lại theo chu kỳ, khoảng 15-45 phút tùy model) -curl -s http://localhost:8091/workflow/a1b2c3d4-... | jq .status +# Xem status +curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890 | jq .status + +# Xem output của bước BA +curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890 | jq '.step_outputs.ba' -# 3. Khi status=completed, lấy output theo từng step -curl -s http://localhost:8091/workflow/a1b2c3d4-... | jq '.step_outputs.ba' -curl -s http://localhost:8091/workflow/a1b2c3d4-... | jq '.step_outputs.sa' +# Xem output của Clarifier (kết quả kiểm tra cuối) +curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890 | jq '.step_outputs.clarifier' -# 4. Xem danh sách workflow gần đây (tối đa 50 bản ghi) -curl -s http://localhost:8091/workflows | jq +# Xem danh sách bước đã hoàn thành +curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890 | jq '.completed_steps' + +# Kiểm tra lỗi nếu có +curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890 | jq '.error' ``` -**Workflow status values:** +**Trạng thái workflow:** + +| Status | Ý nghĩa | +|---|---| +| `pending` | Đã xếp hàng, chưa bắt đầu | +| `running` | LangGraph đang thực thi các bước (bao gồm cả Clarifier regen loop) | +| `completed` | Hoàn tất toàn bộ + Clarifier regen loop | +| `failed` | Lỗi không xử lý được — kiểm tra trường `error` | -| Status | Ý nghĩa | -|-------------|---------------------------------------------------| -| `pending` | Đã vào hàng đợi, chưa bắt đầu | -| `running` | LangGraph đang thực thi các bước | -| `completed` | Hoàn tất toàn bộ 15 bước | -| `failed` | Có lỗi không xử lý được; kiểm tra trường `error` | +> Nếu một bước gặp lỗi LLM (timeout, model chưa pull...), hệ thống ghi `[LỖI trong ]...` vào `step_outputs` và trường `error`, nhưng **workflow vẫn tiếp tục** các bước kế tiếp. -> **Lưu ý:** Nếu một bước gặp lỗi LLM (timeout, model chưa pull, v.v.), hệ thống sẽ ghi `[LỖI trong ] ...` vào `step_outputs` và trường `error`, nhưng workflow vẫn tiếp tục các bước kế tiếp (lỗi không dừng toàn pipeline). +### GET /workflows + +Liệt kê 50 workflow gần nhất. + +```bash +curl -s http://localhost:8091/workflows | jq '[.[] | {workflow_id, status, created_at, completed_at}]' +``` + +### GET /workflow/{workflow_id}/artifacts + +Liệt kê các file code đã được trích xuất và lưu vào disk. + +```bash +curl -s http://localhost:8091/workflow/a1b2c3d4-e5f6-7890-abcd-ef1234567890/artifacts | jq +``` + +```json +{ + "fe": [ + {"role": "fe", "path": "src/pages/Login.tsx", "filename": "Login.tsx", "size_bytes": 2341}, + {"role": "fe", "path": "src/hooks/useAuth.ts", "filename": "useAuth.ts", "size_bytes": 876} + ], + "be": [ + {"role": "be", "path": "src/auth/auth.controller.ts", "filename": "auth.controller.ts", "size_bytes": 3102} + ] +} +``` + +### GET /workflow/{workflow_id}/artifacts/{role}/{path} + +Đọc nội dung một file artifact cụ thể. + +```bash +# Đọc nội dung file +curl -s "http://localhost:8091/workflow/a1b2c3d4.../artifacts/fe/src/pages/Login.tsx" + +# Tải xuống dưới dạng binary +curl -O "http://localhost:8091/workflow/a1b2c3d4.../artifacts/fe/src/pages/Login.tsx?download=1" + +# Đọc toàn bộ markdown output của một agent +curl -s "http://localhost:8091/workflow/a1b2c3d4.../artifacts/be/_output.md" +``` --- -## RAG API — Curl Reference +## 9. RAG API — tài liệu đầy đủ + +**Base URL:** `http://localhost:8090` -### Health +### GET /health ```bash curl -s http://localhost:8090/health | jq @@ -502,9 +836,9 @@ curl -s http://localhost:8090/health | jq "ollama_base_url": "http://ollama:11434", "qdrant_url": "http://qdrant:6333", "collection_prefix": "yan_raw_docs", - "embedding_model": "", - "chat_model": "", - "rag_top_k": 4, + "embedding_model": "nomic-embed-text", + "chat_model": "qwen2.5:7b", + "rag_top_k": 5, "graph": { "enabled": true, "neo4j_uri": "bolt://neo4j:7687", @@ -514,7 +848,9 @@ curl -s http://localhost:8090/health | jq } ``` -### Xem danh sách projects & trạng thái index +### GET /projects + +Liệt kê tất cả project và trạng thái index. ```bash curl -s http://localhost:8090/projects | jq @@ -524,123 +860,170 @@ curl -s http://localhost:8090/projects | jq { "raw_data_dir": "/data/raw", "projects": { - "auth": { "collection": "yan_raw_docs__auth", "indexed": true, "points_count": 142 }, - "billing": { "collection": "yan_raw_docs__billing", "indexed": false, "points_count": null } + "myproject": { + "collection": "yan_raw_docs__myproject", + "indexed": true, + "points_count": 287 + }, + "marketplace": { + "collection": "yan_raw_docs__marketplace", + "indexed": false, + "points_count": null + } } } ``` -### Ingest +### POST /ingest + +Ingest tài liệu vào Qdrant (+ Neo4j nếu `GRAPH_ENABLED=true`). Idempotent — chạy lại không tạo bản sao. + +| Trường | Mô tả | +|---|---| +| `project` | Chỉ ingest một project cụ thể. Bỏ trống để ingest tất cả. | +| `reset` | `true` = xóa collection cũ trước rồi ingest lại. | ```bash # Ingest tất cả projects curl -s -X POST http://localhost:8090/ingest | jq -# Ingest 1 project cụ thể +# Ingest một project cụ thể curl -s -X POST http://localhost:8090/ingest \ -H "Content-Type: application/json" \ - -d '{"project": "marketplace"}' | jq + -d '{"project": "myproject"}' | jq -# Reset rồi ingest lại (xoá collection cũ trước) +# Reset và ingest lại một project curl -s -X POST http://localhost:8090/ingest \ -H "Content-Type: application/json" \ - -d '{"project": "auth", "reset": true}' | jq + -d '{"project": "myproject", "reset": true}' | jq +``` + +**Response:** + +```json +{ + "status": "ok", + "projects": { + "myproject": { + "upserted": 287, + "skipped": 0, + "errors": 0, + "graph_chunks_saved": 287 + } + } +} +``` + +### POST /reset-ingest + +Xóa tất cả collection rồi ingest lại toàn bộ từ đầu. -# Reset tất cả projects +```bash +# Reset và ingest lại toàn bộ curl -s -X POST http://localhost:8090/reset-ingest | jq -# Reset 1 project qua endpoint reset-ingest +# Reset một project cụ thể curl -s -X POST http://localhost:8090/reset-ingest \ -H "Content-Type: application/json" \ - -d '{"project": "billing"}' | jq + -d '{"project": "myproject"}' | jq ``` -### Ask +### POST /ask -**Request body** +Hỏi đáp RAG: embed câu hỏi → tìm kiếm Qdrant + Neo4j → sinh câu trả lời bằng LLM. -| Field | Type | Mô tả | -|------------|-----------------|--------------------------------------------------------------------| -| `question` | `string` (bắt buộc) | Câu hỏi | -| `project` | `string\|null` | Lọc theo project (null = search tất cả) | -| `module` | `string\|null` | Lọc theo module trong project (null = search toàn project) | -| `top_k` | `int\|null` | Số kết quả (null = dùng `RAG_TOP_K` env, mặc định 4) | +**Request body:** -**Các trường trong `sources[]`:** `score`, `project`, `module`, `doc_type`, `chunk_type`, `source_file`, `relative_path`, `file_type`, `chunk_index`, `preview` +| Trường | Kiểu | Bắt buộc | Mô tả | +|---|---|---|---| +| `question` | string | ✅ | Câu hỏi | +| `project` | string\|null | | Lọc theo project (null = search tất cả) | +| `module` | string\|null | | Lọc theo module trong project | +| `top_k` | int\|null | | Số chunk trả về (null = dùng env `RAG_TOP_K`) | ```bash -# Truy vấn toàn bộ (tìm trên tất cả collection và hợp nhất kết quả) +# Query toàn bộ (tất cả collections) curl -s -X POST http://localhost:8090/ask \ -H "Content-Type: application/json" \ -d '{"question": "Mô tả auth flow?"}' | jq -# Truy vấn trong một project cụ thể +# Query theo project curl -s -X POST http://localhost:8090/ask \ -H "Content-Type: application/json" \ - -d '{"question": "Schema billing như thế nào?", "project": "yanlib"}' | jq + -d '{"question": "Schema billing như thế nào?", "project": "myproject"}' | jq -# Truy vấn theo project và module (tăng độ chính xác) +# Query theo project + module (chính xác nhất) curl -s -X POST http://localhost:8090/ask \ -H "Content-Type: application/json" \ - -d '{"question": "Auth flow hoạt động như thế nào?", "project": "yanlib", "module": "auth"}' | jq + -d '{"question": "JWT refresh token flow?", "project": "myproject", "module": "auth"}' | jq -# Tùy chỉnh top_k +# Tùy chỉnh số chunk trả về curl -s -X POST http://localhost:8090/ask \ -H "Content-Type: application/json" \ - -d '{"question": "Marketplace pack architecture?", "project": "yanlib", "top_k": 8}' | jq + -d '{"question": "Kiến trúc marketplace?", "project": "myproject", "top_k": 10}' | jq -# Chỉ lấy trường answer +# Chỉ lấy phần câu trả lời curl -s -X POST http://localhost:8090/ask \ -H "Content-Type: application/json" \ - -d '{"question": "Partner platform là gì?"}' | jq .answer + -d '{"question": "Database schema là gì?", "project": "myproject", "module": "billing"}' \ + | jq .answer ``` -### Qdrant — kiểm tra collections +**Response:** + +```json +{ + "answer": "Schema billing bao gồm...", + "sources": [ + { + "score": 0.91, + "project": "myproject", + "module": "billing", + "doc_type": "schema", + "chunk_type": "table", + "source_file": "billing-schema.md", + "relative_path": "myproject/billing/billing-schema.md", + "file_type": "md", + "chunk_index": 3, + "preview": "| Field | Type | Description |..." + } + ] +} +``` + +**Metadata của `sources[]`:** + +| Trường | Mô tả | +|---|---| +| `score` | Cosine similarity (0–1) | +| `project` | Tên project | +| `module` | Tên module (thư mục con) | +| `doc_type` | Loại tài liệu: `prd`, `schema`, `api`, `architecture`, `spec`, `other` | +| `chunk_type` | Loại chunk: `paragraph`, `table`, `code`, `heading` | +| `source_file` | Tên file gốc | +| `relative_path` | Đường dẫn tương đối trong `data/raw/` | +| `chunk_index` | Số thứ tự chunk trong file | +| `preview` | 200 ký tự đầu của chunk | + +### Qdrant — kiểm tra trực tiếp ```bash # Liệt kê tất cả collections curl -s http://localhost:6333/collections | jq -# Chi tiết 1 collection -curl -s http://localhost:6333/collections/yan_raw_docs__auth | jq +# Chi tiết một collection +curl -s http://localhost:6333/collections/yan_raw_docs__myproject | jq -# Số points trong collection -curl -s http://localhost:6333/collections/yan_raw_docs__auth/points/count \ +# Đếm số points trong collection +curl -s -X POST http://localhost:6333/collections/yan_raw_docs__myproject/points/count \ -H "Content-Type: application/json" \ -d '{"exact": true}' | jq -# Xoá collection thủ công -curl -s -X DELETE http://localhost:6333/collections/yan_raw_docs__auth | jq +# Xóa collection thủ công +curl -s -X DELETE http://localhost:6333/collections/yan_raw_docs__myproject | jq ``` -### Graph (Neo4j) - -```bash -# Trạng thái Neo4j và thống kê số lượng node -curl -s http://localhost:8090/graph/status | jq -``` - -```json -{ - "enabled": true, - "connected": true, - "neo4j_uri": "bolt://neo4j:7687", - "entity_extraction": false, - "stats": { - "projects": 3, - "documents": 18, - "chunks": 742, - "entities": 0 - } -} -``` - -```bash -# Xem entities đã trích xuất trong một project (yêu cầu GRAPH_ENTITY_EXTRACTION=true) -curl -s http://localhost:8090/graph/projects/auth/entities | jq -``` - -### Ollama +### Ollama — kiểm tra trực tiếp ```bash # Danh sách model đã pull @@ -649,11 +1032,11 @@ curl -s http://localhost:11434/api/tags | jq .models[].name # Kiểm tra Ollama đang hoạt động curl -s http://localhost:11434/ -# Chat trực tiếp (không qua lớp RAG) +# Chat trực tiếp (không qua RAG) curl -s -X POST http://localhost:11434/api/chat \ -H "Content-Type: application/json" \ -d '{ - "model": "", + "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "Hello"}], "stream": false }' | jq .message.content @@ -661,214 +1044,483 @@ curl -s -X POST http://localhost:11434/api/chat \ --- -## Quy trình cập nhật tài liệu +## 10. Artifact System + +Sau khi mỗi coding agent hoàn thành, output markdown được tự động quét để trích xuất file code và lưu vào disk. + +**Roles có artifact:** `fe`, `mobile`, `be`, `dba`, `da`, `tech_lead`, `devsecops` + +**Layout thư mục artifact:** + +``` +/data/artifacts/{workflow_id}/{role}/ +├── _output.md ← Toàn bộ markdown output của agent (luôn có) +├── src/pages/Login.tsx ← File được trích xuất từ ### FILE: directive +├── src/hooks/useAuth.ts +├── schema.sql +└── Dockerfile +``` + +**Cơ chế nhận diện file trong markdown:** + +| Ưu tiên | Pattern nhận diện | Ví dụ | +|---|---|---| +| 1 (cao nhất) | `### FILE: path/to/file.ext` + code block ngay sau | `### FILE: src/auth/login.tsx` | +| 2 | Dòng comment đầu code block | `// filename: src/Login.tsx` | +| 3 | Bold/heading ngay trước code block | `**src/Login.tsx**` | +| 4 (fallback) | Đặt tên theo ngôn ngữ + index | `typescript-01.ts` | + +**Truy cập artifacts qua API:** ```bash -# 1. Thêm file vào đúng thư mục con -cp my-new-spec.md data/raw/marketplace/ +# Liệt kê tất cả artifacts của workflow +curl -s http://localhost:8091/workflow/{id}/artifacts | jq -# 2. Ingest lại project (idempotent: chunk đã tồn tại sẽ upsert, không tạo bản sao) -curl -s -X POST http://localhost:8090/ingest \ - -H "Content-Type: application/json" \ - -d '{"project": "marketplace"}' | jq .upserted +# Đọc nội dung file +curl -s "http://localhost:8091/workflow/{id}/artifacts/fe/src/pages/Login.tsx" -# 3. Kiểm tra lại truy vấn -curl -s -X POST http://localhost:8090/ask \ - -H "Content-Type: application/json" \ - -d '{"question": "...", "project": "marketplace"}' | jq .answer +# Tải xuống binary +curl -O "http://localhost:8091/workflow/{id}/artifacts/be/src/auth/auth.service.ts?download=1" + +# Đọc raw output markdown của agent +curl -s "http://localhost:8091/workflow/{id}/artifacts/be/_output.md" ``` -> **Idempotent:** Ingest cùng một file nhiều lần sẽ upsert vào cùng point ID (deterministic UUID từ path + content hash), không tạo bản ghi trùng lặp. +--- + +## 11. Clarifier Regen Loop + +### Cơ chế hoạt động + +1. **Workflow hoàn thành** → Clarifier agent sinh §10 "Recommended Re-generation List" +2. **Parse danh sách** → `_parse_clarifier_regen_list()` quét bảng §10, tìm tên role trong cột "Agent Role" +3. **Re-run từng agent** → mỗi role trong danh sách được chạy lại qua `run_single_step()` với `prev_outputs` đầy đủ nhất (bao gồm output mới của các agent đã re-gen trước đó trong cùng loop) +4. **Re-run Clarifier** → Clarifier được chạy lại với outputs đã cập nhật để đánh giá lại +5. **Lặp** → nếu Clarifier mới vẫn đề xuất re-gen → lặp tiếp, tối đa `CLARIFIER_REGEN_LOOPS` lần + +### Roles có thể được re-gen + +`fe`, `mobile`, `dba`, `be`, `da`, `tech_lead`, `devsecops` + +Các planning roles (`ba`, `pm`, `sa`, `ta`, `designer`, `tl`, `tester`) không nằm trong danh sách eligible để tránh thay đổi foundation của toàn pipeline. + +### Cấu hình + +```env +CLARIFIER_REGEN_LOOPS=1 # số vòng lặp tối đa +CLARIFIER_REGEN_LOOPS=0 # tắt hoàn toàn tính năng +CLARIFIER_REGEN_LOOPS=2 # tăng nếu muốn nhiều vòng tinh chỉnh hơn +``` + +### Theo dõi qua log + +```bash +docker compose logs -f agent-api | grep "Clarifier regen" +``` + +Output mẫu: +``` +Clarifier regen loop 1/1: re-generating roles=['be', 'dba'] +Clarifier regen loop 1: be re-generated (15432 chars) +Clarifier regen loop 1: dba re-generated (8921 chars) +Clarifier regen loop 1: clarifier re-run xong (12340 chars) +``` --- -## Open WebUI — Custom Tools +## 12. Episodic Memory Logging -### Thiết lập +Mỗi lần workflow hoàn thành (thành công hoặc thất bại), agent-api tự động ghi log vào: -1. Mở **Admin Panel → Tools → "+"** -2. Dán nội dung file tool, lưu, rồi bật trong phần model settings +``` +data/memory/episodic/workflow_runs.jsonl +``` -### Tool 1: `yan_knowledge_base.py` — RAG Query +Mỗi dòng là một JSON object: -Truy vấn trực tiếp knowledge base từ giao diện chat. +```json +{ + "workflow_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "project": "myproject", + "user_input": "Xây dựng hệ thống thanh toán...", + "completed_steps": ["ba","pm","sa","ta","designer","tl","fe","mobile","dba","be","da","tech_lead","tester","devsecops","clarifier"], + "steps_count": 15, + "status": "completed", + "error": null, + "duration_seconds": 1842.5, + "timestamp": "2026-06-06T10:30:00+00:00" +} +``` -**Valves:** +**Truy vấn log:** -| Valve | Default | Mô tả | -|-------------------|--------------------------|--------------------------------------------------| -| `rag_api_url` | `http://rag-api:8090` | URL rag-api (nội bộ Docker network) | -| `timeout` | `120` | Timeout giây | -| `top_k` | `null` | Số kết quả RAG (null = dùng `RAG_TOP_K` của API) | -| `default_project` | `null` | Project mặc định (null = search tất cả) | -| `default_module` | `null` | Module mặc định để lọc chunk (null = toàn project) | +```bash +# Xem 10 workflow gần nhất +tail -n 10 data/memory/episodic/workflow_runs.jsonl | jq . + +# Lọc workflow thất bại +cat data/memory/episodic/workflow_runs.jsonl | jq 'select(.status == "failed")' + +# Thống kê thời gian trung bình (giây) +cat data/memory/episodic/workflow_runs.jsonl | jq '[.duration_seconds] | add/length' + +# Lọc theo project +cat data/memory/episodic/workflow_runs.jsonl | jq 'select(.project == "myproject")' + +# Workflow nào chạy không đủ 15 bước +cat data/memory/episodic/workflow_runs.jsonl | jq 'select(.steps_count < 15)' +``` + +--- + +## 13. Open WebUI — Custom Tools + +### Cài đặt + +1. Mở [http://localhost:8085](http://localhost:8085) +2. **Admin Panel → Tools → "+" (Add Tool)** +3. Dán nội dung file tool, nhấn **Save** +4. Bật tool trong **Model Settings → Tools** cho model đang dùng + +### Tool 1: `yan_knowledge_base.py` — Hỏi đáp tài liệu nội bộ + +**Mục đích:** Query RAG API từ giao diện chat Open WebUI — tìm thông tin trong PRD, spec, architecture docs đã ingest. + +**Valves (cấu hình):** + +| Valve | Mặc định | Mô tả | +|---|---|---| +| `rag_api_url` | `http://rag-api:8090` | URL rag-api trong Docker network | +| `timeout` | `120` | Timeout request (giây) | +| `top_k` | `null` | Số chunk trả về (null = dùng env `RAG_TOP_K`) | +| `default_project` | `null` | Project mặc định (null = search tất cả) | +| `default_module` | `null` | Module mặc định (null = toàn project) | **Ví dụ sử dụng trong chat:** ``` -Tìm trong knowledge base: auth flow hoạt động như thế nào? +Tìm trong knowledge base: auth flow của yanlib hoạt động như thế nào? +``` +``` +Tìm thông tin về billing schema trong project yanlib, module billing ``` -### Tool 2: `yan_agent_workflow.py` — SDLC Workflow +### Tool 2: `yan_agent_workflow.py` — Chạy SDLC Workflow -Chạy toàn bộ SDLC workflow hoặc chạy từng agent độc lập. +**Mục đích:** Khởi chạy toàn bộ SDLC workflow hoặc từng agent đơn lẻ trực tiếp từ giao diện chat. -**Valves:** +**Valves (cấu hình):** -| Valve | Default | Mô tả | -|---------------------|---------------------------|----------------------------------------------------------| -| `agent_api_url` | `http://agent-api:8091` | URL agent-api (nội bộ Docker network) | -| `timeout` | `600` | Timeout cho single-step request (giây) | -| `poll_interval` | `15` | Khoảng cách poll workflow status (giây) | -| `poll_max_attempts` | `120` | Số poll tối đa (120 × 15s = 30 phút) | -| `rag_enabled` | `true` | Query RAG cho mỗi agent step | -| `rag_top_k` | `5` | Số kết quả RAG mỗi agent | -| `default_project` | `null` | Project mặc định | +| Valve | Mặc định | Mô tả | +|---|---|---| +| `agent_api_url` | `http://agent-api:8091` | URL agent-api trong Docker network | +| `timeout` | `600` | Timeout cho single-step request (giây) | +| `poll_interval` | `15` | Khoảng cách poll workflow status (giây) | +| `poll_max_attempts` | `120` | Số poll tối đa trước khi timeout (120 × 15s = 30 phút) | +| `rag_enabled` | `true` | Query RAG mỗi agent step | +| `rag_top_k` | `5` | Số kết quả RAG mỗi agent | +| `default_project` | `null` | Project mặc định | **Functions:** -| Function | Mô tả | -|-----------------------|------------------------------------------------------------| -| `run_sdlc_workflow` | Chạy đầy đủ 15 bước, poll đến khi hoàn tất, trả về summary | -| `run_agent_step` | Chạy 1 agent role đơn lẻ (sync) | -| `get_workflow_result` | Lấy kết quả workflow theo ID, filter theo role | -| `list_agent_roles` | Liệt kê tất cả roles, models, thứ tự chạy | +| Function | Mô tả | +|---|---| +| `run_sdlc_workflow(user_input, project, tech_stack)` | Chạy đầy đủ 15 bước, poll đến khi xong, trả về summary | +| `run_agent_step(role, user_input, project, tech_stack)` | Chạy 1 agent đơn lẻ (sync) | +| `get_workflow_result(workflow_id, role)` | Lấy output theo workflow ID, lọc theo role | +| `list_agent_roles()` | Liệt kê tất cả roles, models, thứ tự chạy | **Ví dụ sử dụng trong chat:** ``` -Chạy SDLC workflow cho: xây dựng tính năng payment gateway cho marketplace, project=marketplace +Chạy SDLC workflow: xây dựng module marketplace cho ứng dụng B2B, project=myproject +``` +``` +Chạy agent sa với yêu cầu: thiết kế kiến trúc hệ thống authentication đa tenant ``` --- -## Điều chỉnh model +## 14. Neo4j — Graph Enrichment + +### Kích hoạt Graph -Tất cả model được quản lý trong `.env` tại mục `SDLC Agent Models`. -Sau khi cập nhật `.env`, chỉ cần restart service, không cần rebuild image: +```env +GRAPH_ENABLED=true +GRAPH_ENTITY_EXTRACTION=false +``` ```bash -# Cập nhật model trong .env, sau đó: -docker compose restart agent-api # cho SDLC agent models -docker compose restart rag-api # cho CHAT_MODEL hoặc EMBEDDING_MODEL +docker compose restart rag-api +``` -# Nếu đổi EMBEDDING_MODEL: bắt buộc re-ingest toàn bộ tài liệu -curl -s -X POST http://localhost:8090/reset-ingest | jq -curl -s -X POST http://localhost:8090/ingest | jq +### Kiểm tra trạng thái + +```bash +curl -s http://localhost:8090/graph/status | jq ``` ---- +```json +{ + "enabled": true, + "connected": true, + "neo4j_uri": "bolt://neo4j:7687", + "entity_extraction": false, + "stats": { + "projects": 2, + "documents": 24, + "chunks": 892, + "entities": 0 + } +} +``` -## Rebuild sau khi cập nhật mã nguồn +### Xem entities (khi `GRAPH_ENTITY_EXTRACTION=true`) ```bash -# Rebuild và restart agent-api -docker compose up -d --build agent-api +curl -s http://localhost:8090/graph/projects/myproject/entities | jq +``` -# Rebuild và restart rag-api -docker compose up -d --build rag-api +### Neo4j Browser — Cypher Queries -# Rebuild cả hai -docker compose up -d --build agent-api rag-api +Mở [http://localhost:7474](http://localhost:7474) (user: `neo4j`, password: giá trị `NEO4J_PASSWORD` trong `.env`). + +```cypher +// Xem toàn bộ graph của một project +MATCH path = (:Project {name:"myproject"})-[:HAS_DOCUMENT]->(:Document)-[:HAS_CHUNK]->(:Chunk) +RETURN path LIMIT 50; + +// Thống kê node counts +MATCH (n) RETURN labels(n)[0] AS label, count(n) AS count ORDER BY count DESC; + +// Top entities được đề cập nhiều nhất (cần GRAPH_ENTITY_EXTRACTION=true) +MATCH (c:Chunk)-[:MENTIONS]->(e:Entity) +RETURN e.name, e.type, count(c) AS mentions +ORDER BY mentions DESC LIMIT 20; + +// Chunks đề cập một entity cụ thể +MATCH (c:Chunk)-[:MENTIONS]->(e:Entity {name:"BillingPlan"}) +RETURN c.source_file, c.chunk_index, c.text LIMIT 10; + +// Entities co-occur (xuất hiện cùng nhau trong chunks) +MATCH (e1:Entity)-[:CO_OCCURS_WITH]-(e2:Entity) +RETURN e1.name, e2.name LIMIT 30; + +// Xóa toàn bộ graph (reset thủ công) +MATCH (n) DETACH DELETE n; ``` --- -## Watchtower - Auto-update +## 15. Điều chỉnh model -Watchtower tự pull image mới và restart container theo lịch (`WATCHTOWER_SCHEDULE` trong `.env`, mặc định mỗi phút). +Tất cả model được quản lý trong `.env`. Sau khi đổi model, chỉ cần restart service — **không cần rebuild image**. ```bash -# Kích hoạt cập nhật thủ công qua HTTP API -curl -s -H "Authorization: Bearer ${WATCHTOWER_HTTP_API_TOKEN}" \ - http://localhost:8080/v1/update +# Đổi model reasoning — ví dụ nâng BA_MODEL +# 1. Sửa trong .env: BA_MODEL=qwen2.5:32b -# Xem metrics hiện tại -curl -s -H "Authorization: Bearer ${WATCHTOWER_HTTP_API_TOKEN}" \ - http://localhost:8080/v1/metrics +# 2. Pull model mới +docker exec ollama ollama pull qwen2.5:32b + +# 3. Restart agent-api +docker compose restart agent-api +``` + +```bash +# Đổi CHAT_MODEL (rag-api) +docker compose restart rag-api + +# Đổi EMBEDDING_MODEL — bắt buộc re-ingest toàn bộ +docker compose restart rag-api +curl -s -X POST http://localhost:8090/reset-ingest | jq +curl -s -X POST http://localhost:8090/ingest | jq ``` -> **Lưu ý:** `rag-api` và `agent-api` đang dùng local build với `watchtower.enable=false`, nên Watchtower không tự cập nhật. Sử dụng `docker compose up -d --build` khi cần cập nhật. +### Quản lý model Ollama + +```bash +# Pull model cụ thể +docker exec ollama ollama pull qwen2.5:14b + +# Xem tất cả model đã pull +docker exec ollama ollama list + +# Xóa model không cần thiết +docker exec ollama ollama rm old-model:tag +``` + +### Pull toàn bộ model từ `.env` + +```bash +grep -E '^(EMBEDDING_MODEL|CHAT_MODEL|CODING_PLANNER_MODEL|BA_MODEL|PM_MODEL|SA_MODEL|TA_MODEL|DA_MODEL|TL_MODEL|FE_MODEL|MOBILE_MODEL|BE_MODEL|DBA_MODEL|TECH_LEAD_MODEL|DEVSECOPS_MODEL|TESTER_MODEL|DESIGNER_MODEL|CLARIFIER_MODEL)=' .env \ + | cut -d= -f2 \ + | sort -u \ + | xargs -I {} docker exec ollama ollama pull "{}" +``` + +### Cấu hình cho phần cứng yếu (16 GB RAM) + +```env +BA_MODEL=qwen2.5:7b +PM_MODEL=qwen2.5:7b +SA_MODEL=qwen2.5:7b +TA_MODEL=qwen2.5:7b +DA_MODEL=qwen2.5:7b +TL_MODEL=qwen2.5:7b +FE_MODEL=qwen2.5-coder:7b +MOBILE_MODEL=qwen2.5-coder:7b +BE_MODEL=qwen2.5-coder:7b +DBA_MODEL=qwen2.5-coder:7b +TECH_LEAD_MODEL=qwen2.5-coder:7b +DEVSECOPS_MODEL=qwen2.5-coder:7b +DESIGNER_MODEL=qwen2.5:7b +TESTER_MODEL=qwen2.5:7b +CLARIFIER_MODEL=qwen2.5:7b +CODING_PLANNER_MODEL=qwen2.5:1.5b +OLLAMA_MAX_LOADED_MODELS=1 +MAX_FILES_PER_ROLE=3 +CLARIFIER_REGEN_LOOPS=0 +``` --- -## Vận hành Docker +## 16. Vận hành Docker + +### Khởi động & dừng ```bash -# Khởi động toàn bộ dịch vụ +# Khởi động toàn bộ stack docker compose up -d -# Dừng toàn bộ dịch vụ +# Dừng tất cả (giữ volumes) docker compose down -# Rebuild rag-api sau khi cập nhật mã nguồn +# Dừng và xóa volumes — MẤT TOÀN BỘ DỮ LIỆU Ollama + Qdrant + Neo4j +docker compose down -v +``` + +### Rebuild sau khi cập nhật mã nguồn + +```bash +# Rebuild agent-api +docker compose up -d --build agent-api + +# Rebuild rag-api docker compose up -d --build rag-api -# Theo dõi logs theo thời gian thực +# Rebuild cả hai +docker compose up -d --build agent-api rag-api +``` + +### Theo dõi log + +```bash +# Tất cả services +docker compose logs -f + +# Chỉ agent-api docker compose logs -f agent-api -docker compose logs -f rag-api -# Khởi động lại một service +# 100 dòng gần nhất +docker compose logs --tail=100 agent-api + +# Grep lỗi +docker compose logs agent-api | grep -i error +``` + +### Quản lý container + +```bash +# Restart một service docker compose restart agent-api -# Theo dõi mức sử dụng tài nguyên +# Xem trạng thái tất cả container +docker compose ps + +# Theo dõi tài nguyên CPU/RAM docker stats -# Xóa volumes (NGUY HIỂM: xóa toàn bộ dữ liệu Ollama + Qdrant + Neo4j) -docker compose down -v +# Vào shell container +docker exec -it agent-api bash +docker exec -it ollama bash ``` ---- +### Watchtower — Auto-update -## Troubleshooting +```bash +# Trigger update thủ công +curl -s -H "Authorization: Bearer ${WATCHTOWER_HTTP_API_TOKEN}" \ + http://localhost:8080/v1/update -| Triệu chứng | Nguyên nhân | Hướng xử lý | -|---|---|---| -| `rag-api` / `agent-api` crash khi start | Thiếu env var bắt buộc | `docker compose logs rag-api` hoặc `agent-api` | -| `/ask` trả về 404 "chưa được ingest" | Chưa chạy `/ingest` | `POST /ingest {"project": "..."}` | -| `/ingest` trả về `"status": "empty"` | Không có subfolder trong `data/raw/` | Tạo subfolder và đặt file vào đó | -| `/ask` với `module` không trả kết quả | Module chưa tồn tại trong collection | Kiểm tra tên module = tên thư mục con trong project | -| Ingest chậm hoặc mất nhiều thời gian | `INGEST_EMBED_WORKERS` quá thấp | Tăng `INGEST_EMBED_WORKERS` bằng `OLLAMA_NUM_PARALLEL`; restart `rag-api` | -| Embedding chậm / timeout | Model chưa được pull | Pull lại model đang cấu hình trong `.env` bằng lệnh ở mục **Pull models Ollama** | -| SDLC workflow `status=failed` | Xem field `error` | `curl .../workflow/{id} \| jq .error` | -| SDLC step output bắt đầu bằng `[LỖI` | LLM timeout hoặc model chưa pull | Kiểm tra model đã pull, tăng `OLLAMA_REQUEST_TIMEOUT` trong `.env` | -| Output chứa `...` thô | Model reasoning chưa được nhận diện | Kiểm tra tên model khớp rule nhận diện trong `agent-api/workflow.py` | -| `user_input` bị cắt ngắn trong workflow | Input > 10 000 ký tự | Input được sanitize tự động; chia nhỏ nếu cần | -| `workflow_runs.jsonl` không được tạo | `MEMORY_DIR` không mount | Kiểm tra volume `./data/memory:/data/memory` trong `docker-compose.yml` | -| Open WebUI không gọi được rag-api | URL sai (dùng `localhost`) | Valve `rag_api_url` = `http://rag-api:8090` | -| Open WebUI không gọi được agent-api | URL sai (dùng `localhost`) | Valve `agent_api_url` = `http://agent-api:8091` | -| Qdrant collection không tồn tại | Collection bị xoá hoặc chưa ingest | Chạy lại `POST /ingest` | -| Neo4j không kết nối được | Container chưa sẵn sàng | Đợi ~30s, kiểm tra `docker compose logs neo4j` | -| `/graph/status` → `connected: false` | Sai `NEO4J_PASSWORD` | `.env` phải khớp với lần đầu Neo4j khởi tạo | -| `graph_chunks_saved: 0` sau ingest | `GRAPH_ENABLED=false` trong `.env` | Đổi thành `true` + `docker compose up -d --build rag-api` | -| Workflow mất > 30 phút | Model nặng hoặc tài nguyên thiếu | Giảm mức song song, chọn model nhẹ hơn, hoặc tăng `poll_max_attempts` | -| `workflow_id` not found sau vài giờ | In-memory store bị evict (tối đa 50) | Tăng `_MAX_STORED_WORKFLOWS` trong `agent-api/app.py` hoặc lưu kết quả ngay sau khi hoàn tất | +# Xem metrics +curl -s -H "Authorization: Bearer ${WATCHTOWER_HTTP_API_TOKEN}" \ + http://localhost:8080/v1/metrics +``` ---- +> `rag-api` và `agent-api` dùng local build với `watchtower.enable=false` — Watchtower không tự cập nhật chúng. Dùng `docker compose up -d --build` khi cần cập nhật. + +### Cập nhật tài liệu RAG -## Neo4j — Cypher Queries +```bash +# 1. Thêm/sửa file +cp new-spec.md data/raw/myproject/auth/ -Mở Neo4j Browser tại [http://localhost:7474](http://localhost:7474) (login: `neo4j` / giá trị `NEO4J_PASSWORD` trong `.env`). +# 2. Ingest lại (idempotent — file cũ không bị duplicate) +curl -s -X POST http://localhost:8090/ingest \ + -H "Content-Type: application/json" \ + -d '{"project": "myproject"}' | jq .projects.myproject.upserted -```cypher -// Xem toàn bộ graph của 1 project -MATCH path = (:Project {name:"auth"})-[:HAS_DOCUMENT]->(:Document)-[:HAS_CHUNK]->(:Chunk) -RETURN path LIMIT 50; +# 3. Kiểm tra +curl -s -X POST http://localhost:8090/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "nội dung spec mới?", "project": "myproject", "module": "auth"}' | jq .answer +``` -// Thống kê node counts -MATCH (n) RETURN labels(n)[0] AS label, count(n) AS count ORDER BY count DESC; +--- -// Top entities được đề cập nhiều nhất (cần GRAPH_ENTITY_EXTRACTION=true) -MATCH (c:Chunk)-[:MENTIONS]->(e:Entity) -RETURN e.name, e.type, count(c) AS mentions -ORDER BY mentions DESC LIMIT 20; +## 17. Troubleshooting -// Chunks đề cập một entity cụ thể -MATCH (c:Chunk)-[:MENTIONS]->(e:Entity {name:"Billing Plan"}) -RETURN c.source_file, c.chunk_index, c.text LIMIT 10; +| Triệu chứng | Nguyên nhân | Cách xử lý | +|---|---|---| +| `rag-api` / `agent-api` crash khi start | Thiếu env var bắt buộc | `docker compose logs rag-api` — tìm `RuntimeError` | +| `/ask` trả về 404 "chưa được ingest" | Chưa chạy `/ingest` | `curl -X POST localhost:8090/ingest -d '{"project":"..."}'` | +| `/ingest` trả về `"status": "empty"` | Không có subfolder trong `data/raw/` | Tạo subfolder và đặt file vào đó | +| `/ask` với `module` không trả kết quả | Module sai tên hoặc chưa ingest | Tên module = tên thư mục con chính xác | +| Ingest chậm | `INGEST_EMBED_WORKERS` quá thấp | Tăng bằng giá trị `OLLAMA_NUM_PARALLEL`; restart `rag-api` | +| Embedding timeout | Model chưa được pull | Pull lại `EMBEDDING_MODEL` | +| SDLC workflow `status=failed` | Xem field `error` | `curl .../workflow/{id} | jq .error` | +| Step output bắt đầu bằng `[LỖI` | LLM timeout hoặc model chưa pull | Kiểm tra model; tăng `OLLAMA_REQUEST_TIMEOUT` | +| Output chứa `...` thô | Model reasoning chưa nhận diện | Tên model phải chứa: `phi4-mini-reasoning`, `phi4-reasoning`, `qwq`, `deepseek-r1`, hoặc `qwen3` | +| `user_input` bị cắt ngắn | Input > 10.000 ký tự | Chia nhỏ input | +| `workflow_runs.jsonl` không được tạo | Volume chưa mount | Kiểm tra `./data/memory:/data/memory` trong docker-compose | +| Artifacts không được lưu | Volume chưa mount | Kiểm tra `./data/artifacts:/data/artifacts` trong docker-compose | +| Open WebUI không gọi được rag-api | URL dùng `localhost` thay vì hostname nội bộ | Valve `rag_api_url` = `http://rag-api:8090` | +| Open WebUI không gọi được agent-api | URL dùng `localhost` | Valve `agent_api_url` = `http://agent-api:8091` | +| Neo4j không kết nối | Container chưa sẵn sàng | Đợi ~30s; `docker compose logs neo4j` | +| `/graph/status` → `connected: false` | Sai `NEO4J_PASSWORD` | `.env` phải khớp với lần đầu Neo4j init | +| `graph_chunks_saved: 0` sau ingest | `GRAPH_ENABLED=false` | Đổi thành `true` + rebuild `rag-api` | +| Workflow mất > 60 phút | Model nặng hoặc tài nguyên thiếu | Giảm song song; dùng model nhỏ hơn | +| `workflow_id` not found sau vài giờ | In-memory store bị evict (tối đa 50) | Lưu output ngay; tăng `_MAX_STORED_WORKFLOWS` trong `app.py` | +| Clarifier regen không hoạt động | `CLARIFIER_REGEN_LOOPS=0` | Kiểm tra env var; xem log `grep "Clarifier regen"` | +| Task Completion Checklist thiếu | TL output không có bảng task board | Kiểm tra TL agent đã chạy thành công và sinh §4/§5/§6/§7 | -// Entities co-occur (xuất hiện cùng nhau trong chunks) -MATCH (e1:Entity)-[:CO_OCCURS_WITH]-(e2:Entity) -RETURN e1.name, e2.name LIMIT 30; +--- -// Xoá toàn bộ graph (reset thủ công) -MATCH (n) DETACH DELETE n; -``` \ No newline at end of file +## 18. Danh mục URL dịch vụ + +| Service | URL | Mô tả | +|---|---|---| +| **Open WebUI** | http://localhost:8085 | Giao diện chat chính | +| **RAG API** | http://localhost:8090 | FastAPI root | +| **RAG API Swagger** | http://localhost:8090/docs | Swagger UI — thử API trực tiếp | +| **RAG API ReDoc** | http://localhost:8090/redoc | ReDoc documentation | +| **Agent API** | http://localhost:8091 | SDLC Orchestrator root | +| **Agent API UI** | http://localhost:8091/ui | SDLC Workflow web interface | +| **Agent API Swagger** | http://localhost:8091/docs | Swagger UI — thử API trực tiếp | +| **Ollama API** | http://localhost:11434 | LLM inference API | +| **Qdrant Dashboard** | http://localhost:6333/dashboard | Vector DB dashboard | +| **Qdrant REST** | http://localhost:6333 | Qdrant REST API | +| **Qdrant gRPC** | localhost:6334 | Qdrant gRPC endpoint | +| **Neo4j Browser** | http://localhost:7474 | Graph DB browser UI | +| **Neo4j Bolt** | bolt://localhost:7687 | Neo4j driver connection | +| **Deunhealth** | http://localhost:9999 | Health watchdog | +| **Watchtower** | http://localhost:8080 *(nội bộ)* | Auto-update metrics | diff --git a/agent-api/agents.py b/agent-api/agents.py index ddc4195..1932f80 100644 --- a/agent-api/agents.py +++ b/agent-api/agents.py @@ -1,51 +1,94 @@ """ -agents.py — Cấu hình cho 15 agent SDLC trong LangGraph workflow. - -Định nghĩa pipeline 15 agent: - - Bước Role Model Phụ thuộc - ----- -------------- ------------------ ---------------------------------- - 1 ba BA_MODEL -- - 2 pm PM_MODEL ba - 3 sa SA_MODEL ba, pm - 4 ta TA_MODEL ba, sa - 5 designer DESIGNER_MODEL ba, sa, ta - 6 tl TL_MODEL ba, sa, ta, designer - 7 fe FE_MODEL ba, sa, ta, designer, tl - 8 mobile MOBILE_MODEL ba, sa, ta, designer, tl - 9 dba DBA_MODEL ba, sa, ta, tl - 10 be BE_MODEL ba, sa, ta, fe, mobile, dba, tl - 11 da DA_MODEL ba, sa, dba - 12 tech_lead TECH_LEAD_MODEL sa, fe, mobile, be, dba - 13 tester TESTER_MODEL be, fe, mobile, tech_lead, designer - 14 devsecops DEVSECOPS_MODEL sa, ta, tech_lead, tester - 15 clarifier CLARIFIER_MODEL ba, pm, sa, ta, designer, tl, fe, mobile, dba, be, da, tech_lead, tester, devsecops - -Chọn model ----------- -Model của từng role được đọc từ biến môi trường lúc import, cho phép -thay đổi model chỉ cần sửa .env và restart container (không cần rebuild). -Giá trị mặc định được dùng khi biến môi trường vắng mặt. - -Thêm role mới -------------- -1. Thêm hằng số MODEL_XXX: MODEL_XXX = os.environ.get("XXX_MODEL", "") -2. Thêm AgentConfig vào AGENTS với step_id, depends_on và system_prompt đúng. -3. Chèn role vào WORKFLOW_STEPS đúng vị trí. -4. Thêm XXX_MODEL vào .env và vào khối environment của agent-api trong docker-compose.yml. +agents.py — Định nghĩa cấu hình 15 agent SDLC cho LangGraph workflow +===================================================================== + +Mô tả +----- +Module này là nguồn cấu hình duy nhất (single source of truth) cho toàn bộ +pipeline SDLC. Mỗi agent được định nghĩa bằng AgentConfig gồm: step_id (thứ tự +thực thi), role (định danh), name (tên hiển thị), model (LLM sử dụng), +system_prompt (hướng dẫn chi tiết cho LLM), depends_on (danh sách role cần +chạy trước) và rag_query_hint (gợi ý truy vấn RAG riêng để tăng độ chính xác +retrieval). + +Thứ tự thực thi và phụ thuộc +----------------------------- + Bước Role Model Phụ thuộc + ----- ------------- ------------------ ----------------------------------------- + 1 ba BA_MODEL — (không phụ thuộc) + 2 pm PM_MODEL ba + 3 sa SA_MODEL ba, pm + 4 ta TA_MODEL ba, sa + 5 designer DESIGNER_MODEL ba, sa, ta + 6 tl TL_MODEL ba, sa, ta, designer + 7 fe FE_MODEL ba, sa, ta, designer, tl + 8 mobile MOBILE_MODEL ba, sa, ta, designer, tl + 9 dba DBA_MODEL ba, sa, ta, tl + 10 be BE_MODEL ba, sa, ta, fe, mobile, dba, tl + 11 da DA_MODEL ba, sa, dba + 12 tech_lead TECH_LEAD_MODEL sa, fe, mobile, be, dba + 13 tester TESTER_MODEL be, fe, mobile, tech_lead, designer + 14 devsecops DEVSECOPS_MODEL sa, ta, tech_lead, tester + 15 clarifier CLARIFIER_MODEL ba, pm, sa, ta, designer, tl, fe, mobile, + dba, be, da, tech_lead, tester, devsecops + +Các nhóm agent theo chức năng +------------------------------ +- Nhóm phân tích nghiệp vụ: ba, pm, sa, ta, da + Sử dụng model reasoning mạnh (BA_MODEL, PM_MODEL, SA_MODEL, TA_MODEL, DA_MODEL). + Nhiệm vụ: phân tích yêu cầu, lập kế hoạch, thiết kế kiến trúc. + +- Nhóm lập kế hoạch kỹ thuật: tl + Sử dụng TL_MODEL. Nhiệm vụ: chia nhỏ công việc thành task board cho FE/Mobile/BE/DBA. + +- Nhóm sinh code: fe, mobile, be, dba, tech_lead, devsecops + Sử dụng coding model (FE_MODEL, MOBILE_MODEL, BE_MODEL, DBA_MODEL, TECH_LEAD_MODEL, + DEVSECOPS_MODEL). Chạy qua quy trình 2 pha: lập kế hoạch file → sinh từng file. + Mỗi agent kết thúc bằng Task Completion Checklist đối chiếu với TL task board. + +- Nhóm sáng tạo / kiểm thử: designer, tester + Sử dụng DESIGNER_MODEL, TESTER_MODEL. Nhiệm vụ: thiết kế UI/UX và viết test. + +- Clarifier: kiểm tra xuyên suốt toàn bộ 14 agent, phát hiện gap, mâu thuẫn, + assumption chưa được xác nhận. Kích hoạt Clarifier Regen Loop sau khi workflow + hoàn tất nếu §10 Recommended Re-generation List có nội dung. + +Quản lý model qua biến môi trường +---------------------------------- +Mỗi hằng số MODEL_XXX được đọc từ biến môi trường tương ứng lúc import. +Thay đổi model chỉ cần sửa .env rồi `docker compose restart agent-api` +— không cần rebuild image. Giá trị mặc định được dùng khi env var vắng mặt. + +Hằng số xuất khẩu +----------------- +- AGENTS: dict[str, AgentConfig] — tra cứu cấu hình theo role name +- WORKFLOW_STEPS: list[str] — thứ tự thực thi chuẩn của 15 role +- MAX_PREV_OUTPUT_CHARS: int — giới hạn ký tự mỗi dep output khi + xây dựng context (tránh overflow window) + +Hướng dẫn thêm role mới +----------------------- +1. Khai báo hằng model: MODEL_XXX = os.environ.get("XXX_MODEL", "") +2. Thêm AgentConfig vào AGENTS với step_id, role, name, model, depends_on, + rag_query_hint và system_prompt đầy đủ. +3. Chèn role vào WORKFLOW_STEPS đúng vị trí theo thứ tự phụ thuộc. +4. Thêm XXX_MODEL vào .env và vào khối environment của agent-api trong + docker-compose.yml. """ import os from dataclasses import dataclass, field -# ── Hằng số Model (lấy từ .env → khối environment trong docker-compose) ──── -# Agent suy luận +# ── Hằng số Model — đọc từ biến môi trường tương ứng lúc import ────────────── +# Nhóm agent phân tích nghiệp vụ: sử dụng model reasoning mạnh để phân tích +# yêu cầu, thiết kế kiến trúc và lập kế hoạch dự án. MODEL_BA: str = os.environ.get("BA_MODEL", "qwen3.6:35b") MODEL_PM: str = os.environ.get("PM_MODEL", "qwen3.6:35b") MODEL_SA: str = os.environ.get("SA_MODEL", "qwen3.6:35b") MODEL_TA: str = os.environ.get("TA_MODEL", "qwen3.6:35b") MODEL_DA: str = os.environ.get("DA_MODEL", "qwen3.6:35b") -# Agent lập trình +# Nhóm agent sinh code: sử dụng coding model tối ưu cho việc viết code +# và cấu hình hạ tầng. MODEL_FE: str = os.environ.get("FE_MODEL", "qwen3-coder-next") MODEL_MOBILE: str = os.environ.get("MOBILE_MODEL", "qwen3-coder-next") MODEL_BE: str = os.environ.get("BE_MODEL", "qwen3-coder-next") @@ -53,12 +96,14 @@ MODEL_TECH_LEAD: str = os.environ.get("TECH_LEAD_MODEL", "qwen3-coder-next") MODEL_DEVSECOPS: str = os.environ.get("DEVSECOPS_MODEL", "qwen3-coder-next") MODEL_TL: str = os.environ.get("TL_MODEL", "qwen3-coder-next") -# Agent sáng tạo / kiểm thử +# Nhóm agent sáng tạo và kiểm thử: Designer dùng model sáng tạo mạnh +# cho thiết kế UI/UX; Tester dùng model cân bằng giữa logic và ngôn ngữ tự nhiên. MODEL_TESTER: str = os.environ.get("TESTER_MODEL", "mistral-small3.2:24b") MODEL_DESIGNER: str = os.environ.get("DESIGNER_MODEL", "gemma4:31b") -# Clarifier — suy luận mạnh để phát hiện gap & assumption xuyên suốt toàn pipeline +# Clarifier — agent kiểm tra toàn bộ pipeline, cần model reasoning mạnh nhất +# để phát hiện gap, mâu thuẫn và assumption ẩn xuyên suốt 14 agent trước. +# LƯU Ý: EMBEDDING_MODEL được định nghĩa và dùng riêng trong rag-api/ingest.py. MODEL_CLARIFIER: str = os.environ.get("CLARIFIER_MODEL", "qwen3.6:35b") -# LƯU Ý: MODEL_EMBEDDING được định nghĩa trong rag-api/ingest.py, không dùng trong agent-api. @dataclass @@ -68,10 +113,11 @@ class AgentConfig: name: str model: str system_prompt: str - # Output của các bước trước cần chèn làm context (theo thứ tự phụ thuộc) + # Danh sách role phải chạy xong trước — output của chúng sẽ được rút gọn + # và chèn vào context trước khi gọi LLM của agent này. depends_on: list[str] = field(default_factory=list) - # Gợi ý truy vấn RAG riêng cho từng role (cải thiện độ chính xác retrieval) - rag_query_hint: str = "" + # Chuỗi gợi ý truy vấn RAG riêng cho từng role. Thay vì dùng nguyên user_input, + # rag-api sẽ nhận chuỗi này để lấy context chính xác hơn cho từng vai trò SDLC. # ────────────────────────────────────────────────────────────────────────────── @@ -85,7 +131,7 @@ class AgentConfig: name="BA Agent — Business Analysis", model=MODEL_BA, depends_on=[], - rag_query_hint="business requirement, user story, acceptance criteria, business rules, scope, gap analysis, WBS, RTM", + rag_query_hint="business requirement, user story, acceptance criteria, business rules, scope, gap analysis, WBS, RTM, platform architecture pattern, three-seam pattern, scope=platform, module naming convention, non-negotiable invariant, platform constraints", system_prompt="""\ You are the Business Analyst (BA) Agent for a software delivery team. Your responsibility is to analyze the business goal, product requirements, and source documents, @@ -162,7 +208,7 @@ class AgentConfig: name="SA Agent — Solution Architecture", model=MODEL_SA, depends_on=["ba", "pm"], - rag_query_hint="system architecture, service boundary, API contracts, data model, integration flow, NFR, security, deployment architecture", + rag_query_hint="system architecture, service boundary, API contracts, data model, integration flow, NFR, security, deployment architecture, three-seam pattern, scope=platform rows, internal endpoint, platform service naming, microservice port, Module Federation topology, codebase structure guide, Kafka topic naming convention", system_prompt="""\ You are the Solution Architect (SA) Agent. Design the complete technical solution based on the BA requirements and PM project plan. @@ -441,13 +487,22 @@ class AgentConfig: name="FE Agent — Frontend Engineering", model=MODEL_FE, depends_on=["ba", "sa", "ta", "designer", "tl"], - rag_query_hint="frontend architecture, React component, Next.js page, TypeScript interface, state management, API integration, form validation, responsive design, accessibility, third-party SDK", + rag_query_hint="frontend architecture, React component, TypeScript interface, state management, API integration, form validation, UI library, platform UI component library, Module Federation subapp, federated remote, host shell, shared singleton, TanStack Query, Zustand, useForm, shared compliance editor, shared workflow editor, subapp port, federation name, platform frontend conventions", system_prompt="""\ You are the Frontend Engineer (FE) Agent. Design the complete frontend architecture and implementation blueprint based on the BA requirements, SA architecture, TA tech stack decisions, and Designer wireframes. Your output is the implementation blueprint for FE development. +PLATFORM CONVENTIONS — MANDATORY (scan RAG context before writing any code): +- UI LIBRARY: If the RAG context specifies a platform UI library (e.g. `@blazeupai/blazeup-ui`), use it for ALL UI components, forms, and layouts. Do not use generic component libraries (MUI, Ant Design, shadcn, raw HTML) when a platform library is specified. +- FORMS: If the RAG context specifies a form abstraction (e.g. `useForm` from `@blazeupai/blazeup-ui`), use it everywhere. Do not use `react-hook-form` + `yup` if the platform has replaced them. +- SERVER STATE: If the RAG context specifies a server-state library (e.g. TanStack Query v5), use it for all API calls. Do not use raw `axios`/`fetch` hooks when a standardized pattern is documented. +- GLOBAL STATE: Use the platform-specified global state solution (e.g. Zustand). Do not mix Redux and Zustand. +- MODULE FEDERATION: If the RAG context documents a Module Federation topology (host shell + federated subapps with port numbers and federation names), the generated FE MUST be structured as the correct federated subapp. Include the correct `port`, federation `name`, exposed module path, and shared singletons (`react`, `react-dom`, the platform UI library). +- SHARED EDITORS: If the RAG context documents a shared editor component (e.g. `@blazeupai/compliance-editor`, `@blazeupai/workflow-editor`), use it instead of building a custom editor from scratch. +- SUBAPP NAMING: Use the exact subapp name, route path, and sidebar entry from the RAG context. Do not invent alternative names. + SYSTEM CONTEXT AWARENESS: The frontend does not exist in isolation. Before designing any component or page, map the full integration picture: (1) all backend services and endpoints this FE calls — not only the primary BE but also auth service, notification service, file/image storage, CDN; (2) all third-party client-side integrations (analytics SDK, payment widget, OAuth provider, maps, push notification, chat widget, feature flags, A/B testing tools); (3) all real-time channels (WebSocket, SSE, long-polling) that push data from backend to FE; (4) all shared global state (auth context, user session, feature flags, cart/basket shared across pages). Your §5 API Integration Map must cover ALL integration types — internal backend AND external/third-party — not just the primary REST calls. Any FE component that touches a service boundary must reference that boundary explicitly. @@ -471,6 +526,13 @@ class AgentConfig: 10. Accessibility Checklist (ARIA roles, keyboard navigation, color contrast, screen reader support) 11. FE Task Breakdown — REQUIRED before any code skeleton (table format: | # | Task | Category: Setup/Routing/Component/API Integration/Third-party/Testing | Estimate (days) | Priority: High/Med/Low | Depends On | Notes |; categories in this order: Setup → Routing → Core Components → Internal API Integration → Third-party Integration → Testing) 12. FE Code Skeleton (key pages and components with TypeScript structure stubs) + +13. Task Completion Checklist (MANDATORY FINAL SECTION) + Produce a "## Task Completion Checklist" section as the very last item in your output. + List EVERY task from the TL Agent's §4 FE Task Board. For each task, mark: + - ✅ Done — [Task name] → [section number or file where it was addressed] + - ⏳ Pending — [Task name] → [reason or dependency blocking it] + No task from the TL FE Task Board may be silently skipped. Every task must appear in this checklist. """, ), @@ -481,13 +543,18 @@ class AgentConfig: name="Mobile Agent — Mobile Engineering", model=MODEL_MOBILE, depends_on=["ba", "sa", "ta", "designer", "tl"], - rag_query_hint="mobile architecture, Flutter, React Native, navigation flow, screen component, API integration, offline cache, push notification, local storage, app state, mobile UX, permission, third-party SDK", + rag_query_hint="mobile architecture, Flutter, React Native, navigation flow, screen component, API integration, offline cache, push notification, local storage, app state, mobile UX, permission, third-party SDK, platform shared packages, platform mobile conventions, shared editor component mobile", system_prompt="""\ You are the Mobile Engineer Agent. Design the complete mobile architecture and implementation blueprint based on the BA requirements, SA architecture, TA tech stack decisions, and Designer wireframes. Your output is the implementation blueprint for mobile development (Flutter / React Native / native Android / iOS). +PLATFORM CONVENTIONS — MANDATORY (scan RAG context before writing any code): +- SHARED PACKAGES: If the RAG context documents platform-shared mobile packages or editor components (e.g. `@blazeupai/compliance-editor`, shared widget libraries), use them. Do not build custom implementations of functionality the platform already provides. +- API CONTRACTS: Use only the route paths, authentication headers, and request/response shapes documented in SA or the RAG context. Do not invent endpoint shapes. +- SERVICE NAMING: Use exact service and module names from RAG documents for logger `service` fields, analytics tags, and deep link host values. + SYSTEM CONTEXT AWARENESS: The mobile app does not exist in isolation. Before designing any screen or component, map the full integration picture: (1) all backend services and endpoints the app calls — primary BE, auth service, notification service, file/image storage; (2) all third-party SDKs and platform services (FCM/APNs push notifications, Google Maps, payment SDK, OAuth provider, camera/biometric, deep link routing, analytics, crash reporting); (3) all real-time channels (WebSocket, SSE, background sync) that push data to the app; (4) all offline/cache strategies and their sync contracts with the backend (what data is cached, TTL, conflict resolution). Your §4 API Integration Mapping must cover ALL integration types — internal backend AND external/third-party — not just the primary REST calls. @@ -513,6 +580,13 @@ class AgentConfig: 12. Loading / Empty / Error States (per screen: skeleton, spinner, empty illustration + CTA, error + retry) 13. Mobile Task Breakdown — REQUIRED before any code skeleton (table format: | # | Task | Category: Setup/Navigation/Screen/API Integration/Third-party SDK/Offline/Testing | Estimate (days) | Priority: High/Med/Low | Depends On | Notes |; categories in this order: Setup → Navigation → Core Screens → Internal API → Third-party SDKs → Offline/Cache → Testing) 14. Mobile Code Skeleton (key screens and widgets with Dart/TypeScript structure stubs) + +15. Task Completion Checklist (MANDATORY FINAL SECTION) + Produce a "## Task Completion Checklist" section as the very last item in your output. + List EVERY task from the TL Agent's §5 Mobile Task Board. For each task, mark: + - ✅ Done — [Task name] → [section number or file where it was addressed] + - ⏳ Pending — [Task name] → [reason or dependency blocking it] + No task from the TL Mobile Task Board may be silently skipped. Every task must appear in this checklist. """, ), @@ -523,13 +597,20 @@ class AgentConfig: name="DBA Agent — Database Architecture", model=MODEL_DBA, depends_on=["ba", "sa", "ta", "tl"], - rag_query_hint="ERD, SQL schema, NoSQL schema, database design, index, migration plan, query optimization, backup restore, data retention, task estimate", + rag_query_hint="ERD, SQL schema, NoSQL schema, database design, index, migration plan, query optimization, backup restore, data retention, task estimate, tenantId compound index invariant, multi-tenant data isolation, partition key, tenancy rule, platform schema convention, text index scope, tenant prefix index", system_prompt="""\ You are the Database Architect (DBA) Agent. First, check the Required Tech Stack from the TA Agent output: if a relational database is specified, produce SQL schema; if a document store or NoSQL database is specified, produce the appropriate document/collection schema. If both are present, cover both. Design the complete database schema, indexes, migration strategy, and query optimization plan based on the data model from SA and requirements from BA. +PLATFORM CONVENTIONS — MANDATORY (scan RAG context before writing any schema): +- TENANCY INVARIANT: If the RAG context documents a multi-tenancy indexing rule (e.g. "tenantId is field #1 in every compound index"), apply it to EVERY collection without exception. Do not create any compound index where tenantId is not the first field. +- TENANT FIELD: Every document collection that stores per-tenant data MUST have a `tenantId` field. If a collection is platform-scoped (e.g. scope=platform rows in a shared service), document that explicitly and note it is NOT tenant-partitioned. +- TEXT INDEXES: Any full-text search index MUST be prefixed with `{ tenantId: 1, ... }` to prevent cross-tenant data leakage. Unscoped text indexes are a critical security violation. +- SCHEMA CONVENTIONS: If the RAG context provides a canonical schema file (e.g. `compliance-schema-v2.0.md`, `tenants-billing-plans-schema-v1.0.md`), use the exact field names, types, and structure from that document. Do not invent alternative schemas. +- PLATFORM SERVICES: If the RAG context states that a data entity belongs to an existing service (e.g. "scope=platform rows inside ms-compliance"), reflect that in the schema — do NOT design a new standalone collection for data that is owned by an existing service. + SYSTEM CONTEXT AWARENESS: The database does not exist in isolation. Before designing any schema, identify: (1) which services WRITE to which tables/collections, with the triggering action and write frequency; (2) which services READ from which tables/collections, with query patterns and read frequency; (3) which data crosses service boundaries via API responses, events, or message queues; (4) which tables are exclusively owned by one service vs shared/read by multiple services (shared-mutable-state creates coupling and consistency risk). Your §11 Data Flow Map must document this full read/write ownership model so that every table's producer and consumer services are visible, not just the schema DDL. @@ -553,6 +634,13 @@ class AgentConfig: 11. Data Flow Map ASCII diagram or table: | Table/Collection | Written By (service + triggering action) | Write Frequency | Read By (service + query context) | Read Frequency | Data Crosses Service Boundary Via: API/event/queue/direct | Exclusive Owner | Notes | Goal: show which services produce vs consume each dataset, surface cross-service data dependencies and shared-mutable-state coupling, and identify tables that are read by services that do not own them (potential consistency and coupling risk). + +12. Task Completion Checklist (MANDATORY FINAL SECTION) + Produce a "## Task Completion Checklist" section as the very last item in your output. + List EVERY task from the TL Agent's §7 DBA Task Board. For each task, mark: + - ✅ Done — [Task name] → [section number or file where it was addressed] + - ⏳ Pending — [Task name] → [reason or dependency blocking it] + No task from the TL DBA Task Board may be silently skipped. Every task must appear in this checklist. """, ), @@ -563,7 +651,7 @@ class AgentConfig: name="BE Agent — Backend Implementation", model=MODEL_BE, depends_on=["ba", "sa", "ta", "fe", "mobile", "dba", "tl"], - rag_query_hint="backend API, business logic, service layer, DTO, validation, error handling, authentication, unit test, database access, external service integration, webhook, third-party API", + rag_query_hint="backend API, business logic, service layer, DTO, validation, error handling, authentication, unit test, database access, external service integration, webhook, third-party API, platform common library, shared guard decorator, AuthMethod decorator, TenantGuard, PlatformGuard, base repository, OutboxModule, AuditTrailModule, KafkaModule from common-lib, safeSearchRegex, platform service naming convention, internal endpoint route prefix, three-seam pattern, scope=platform", system_prompt="""\ You are the Backend Engineer Agent. Design and document backend service blueprints, API implementations, and code skeletons @@ -572,6 +660,15 @@ class AgentConfig: Produce implementation-ready blueprints and code skeletons - not full production code. For each code section, provide the structure, key logic, and inline notes for what the developer must implement. +PLATFORM CONVENTIONS — MANDATORY (scan RAG context before writing any code): +- COMMON LIBRARY: If the RAG context documents a shared platform library (e.g. `@blazeupai/blazeup-global-common`), use it for ALL auth guards, Kafka modules, cache services, audit trail modules, outbox modules, and base repositories. Do NOT reimplement these abstractions from scratch. +- GUARDS & DECORATORS: If the RAG context documents platform-specific guards or decorators (e.g. `@AuthMethod`, `TenantGuard`, `PlatformGuard`, `JwtAuthGuard`), use them in every controller. Do not leave any endpoint without the platform-specified auth decorator. +- ROUTE PREFIX: If the RAG context specifies a route prefix for a feature (e.g. `/internal/platform-compliance/*`, `/internal/platform-templates/*`), use that exact prefix. Do not invent a different controller path. +- TENANT SCOPE: Every service method that reads or writes tenant data MUST scope queries to the authenticated tenant (`tenantId` from the JWT/guard context). A `findOne()` or `find()` with no tenant filter is a critical cross-tenant data leakage bug. +- SAFE SEARCH: If the RAG context documents a `safeSearchRegex` or equivalent helper for user-supplied search inputs, use it for EVERY regex or `$regex` query. Never pass raw user input directly into `new RegExp()` or `$regex`. +- KAFKA TOPICS: If the RAG context lists canonical Kafka topic names, use ONLY those exact names. Do not invent topic names. +- SERVICE NAMING: Use exact service, module, and microservice names from the RAG context. Do not invent names. + SYSTEM CONTEXT AWARENESS: The backend does not exist in isolation. Before designing any endpoint or service, map the full integration picture: (1) UPSTREAM CALLERS — all clients that call INTO this BE (FE clients, Mobile clients, partner/webhook APIs, internal microservices, scheduled/cron jobs); (2) DOWNSTREAM CALLS — all services this BE calls OUT TO (external APIs, payment gateways, email/SMS services, file storage, other microservices, message queue publishes, databases); (3) SHARED INFRASTRUCTURE — auth/session service, cache layer, message broker, CDN/storage this BE uses; (4) EVENT TOPOLOGY — which events this BE emits and which events it subscribes to, and the consumer/producer chain. Your §12 Service Dependency Map must visualize this full integration graph, and your §3 API Registry must include every endpoint exposed to every consumer type. @@ -601,6 +698,13 @@ class AgentConfig: (1) Auth/authorization flow: HTTP request → auth middleware → token validation → service → repository → DB response → client. Show the token/claims payload at each step. (2) Core business transaction: HTTP request → input validation → service logic → DB write → event publish → external notification → client response. Show request/response shape at each step. (3) External service integration: BE → external API call (with auth header/payload) → success/failure response handling → DB update → event or client response. + +13. Task Completion Checklist (MANDATORY FINAL SECTION) + Produce a "## Task Completion Checklist" section as the very last item in your output. + List EVERY task from the TL Agent's §6 BE Task Board. For each task, mark: + - ✅ Done — [Task name] → [section number or file where it was addressed] + - ⏳ Pending — [Task name] → [reason or dependency blocking it] + No task from the TL BE Task Board may be silently skipped. Every task must appear in this checklist. """, ), @@ -652,7 +756,7 @@ class AgentConfig: name="Tech Lead Agent — Code Review & Standards", model=MODEL_TECH_LEAD, depends_on=["sa", "fe", "mobile", "be", "dba"], - rag_query_hint="code review, refactor, clean architecture, coding standard, performance optimization, technical debt, security review", + rag_query_hint="code review, refactor, clean architecture, coding standard, performance optimization, technical debt, security review, platform coding standards, tenantId index invariant, common-lib usage, AuthMethod decorator, safeSearchRegex, cross-tenant data isolation, Dockerfile security, pino logger, non-negotiable platform invariant", system_prompt="""\ You are the Tech Lead Agent. Review the FE, Mobile, and BE implementation for code quality, architecture compliance, @@ -661,6 +765,15 @@ class AgentConfig: IMPORTANT: If actual source code is not provided in the previous agent outputs, perform a Design Review only. Do not invent file names, line numbers, or PR comments — label your output as [Design Review] instead of [Code Review] in that case. +PLATFORM CONVENTIONS — CHECK AGAINST RAG: +Before issuing any finding, scan RAG context for platform-specific invariants. Required checks: +1. TENANCY: Are all database queries scoped to `tenantId`? Are all compound indexes prefixed with `{ tenantId: 1 }`? Flag any violation as OWASP A01 + platform tenancy invariant breach. +2. COMMON-LIB: Is the platform common library used for auth guards, Kafka, cache, audit trail, and base repos? Flag any reimplementation as technical debt. +3. SAFE SEARCH: Is every `RegExp` / `$regex` built from user input passed through the platform's safe search helper? Flag raw user input into regex as CRITICAL (ReDoS risk). +4. AUTH CHAIN: Does every controller endpoint have the platform-required auth decorator (e.g. `@AuthMethod`)? Flag missing decorators as CRITICAL. +5. DOCKERFILE: Is the container running as a non-root user? Is `.npmrc` absent from the runtime image? Flag violations per platform Dockerfile standards. +6. SERVICE NAMES: Are logger `service` fields, Kafka topic names, route prefixes, and module names consistent with the RAG-documented platform conventions? + SYSTEM CONTEXT AWARENESS: Code review does not stop at individual files. Beyond reviewing isolated components, assess: (1) INTEGRATION COMPLIANCE — does the FE/Mobile/BE implementation honor the SA service boundaries, API contracts, and event schemas? (2) CROSS-LAYER CONSISTENCY — do FE/Mobile TypeScript types match BE DTO shapes? Do BE query patterns match DBA schema and index designs? (3) INTEGRATION FAILURE HANDLING — does each layer correctly handle failures from downstream dependencies (timeouts, 4xx/5xx, event processing failures, cache misses)? (4) SHARED COMPONENT RISK — are auth service, cache, or queue being called in patterns that could cause cascading failures across services? Your §11 Integration Architecture Compliance Review must surface these cross-layer integration issues explicitly. @@ -749,7 +862,7 @@ class AgentConfig: name="DevSecOps Agent — Infrastructure, CI/CD & Deployment", model=MODEL_DEVSECOPS, depends_on=["sa", "ta", "tech_lead", "tester"], - rag_query_hint="Docker, Kubernetes, Helm, CI/CD pipeline, security gates, SAST, DAST, SCA, container security, secrets management, IAM, RBAC, network policy, monitoring, rollback, deployment plan, runbook", + rag_query_hint="Docker, Kubernetes, Helm, CI/CD pipeline, security gates, SAST, DAST, SCA, container security, secrets management, IAM, RBAC, network policy, monitoring, rollback, deployment plan, runbook, Dockerfile platform convention, pino logger config, service name convention, non-root user Dockerfile, npmrc bake risk, platform deployment standard", system_prompt="""\ You are the DevSecOps Agent. Your role spans infrastructure automation AND security hardening. @@ -758,13 +871,52 @@ class AgentConfig: All output must be executable or directly convertible to scripts/YAML/config. Mark any infrastructure config, pipeline stage, or security setting that has not been confirmed in the provided context as [Proposed] — do not present unconfirmed items as finalized. +═══════════════════════════════════════════════════════════════ +DEPLOYMENT TARGET DETECTION — READ BEFORE WRITING ANY CONFIG +═══════════════════════════════════════════════════════════════ +Step 1: Read the Required Tech Stack and TA §8 TDR carefully. + +IF the tech stack includes Kubernetes / K8s / EKS / GKE / AKS / Helm: + → PRIMARY DELIVERABLE = Kubernetes YAML manifests (Deployment, Service, Ingress, ConfigMap, Secret, HPA, PodDisruptionBudget). + → docker-compose is SECONDARY: provide only a dev/local docker-compose.yml scoped to §1 below. + → DO NOT provide a docker-compose.yml as a production deployment artifact. + → §2 Kubernetes Manifests is MANDATORY and must contain real, working YAML for every service. + +IF the tech stack specifies docker-compose only (no K8s): + → PRIMARY DELIVERABLE = docker-compose.yml (production-grade). + → Skip §2 K8s Manifests; skip §9 HPA/PDB. Mark those sections [N/A — docker-compose target]. + +IF the tech stack is ambiguous: + → Default to Kubernetes. State [Assumption: K8s target] and produce K8s manifests. + +═══════════════════════════════════════════════════════════════ +SECRET MANAGEMENT INVARIANT — APPLY TO EVERY SECTION +═══════════════════════════════════════════════════════════════ +NEVER place credentials, API keys, DB connection strings, JWT secrets, OAuth client secrets, +SMTP passwords, or any sensitive value as a plain `env:` entry in a Deployment spec or docker-compose `environment:` block. + +For Kubernetes: + - ALL sensitive env vars → K8s Secret objects → referenced via `secretKeyRef` in Deployment envFrom/env. + - Non-sensitive config (LOG_LEVEL, PORT, NODE_ENV, feature flags) → K8s ConfigMap → referenced via `configMapKeyRef` or `envFrom`. + - Every Secret object must include a comment: "# Inject value via: kubectl create secret generic ... --from-literal=KEY=VALUE" + - In CI/CD: secrets are injected from the secret store (Vault / AWS Secrets Manager / GitHub Actions secrets) into K8s Secrets at deploy time — they are never stored in Git. + +For docker-compose (dev only): + - Sensitive values → reference `.env` file entries → document all required keys in a `.env.example` committed to the repo with placeholder values only. + - Add a comment on every sensitive field: "# Set in .env — never commit real value". + SYSTEM CONTEXT AWARENESS: -Infrastructure and security must account for the full system topology. Before writing any config, identify: (1) all services, their ports, and inter-service communication paths (the complete network graph); (2) all external ingress points (public API, webhooks, OAuth callbacks, CDN, admin portals); (3) all external egress points (calls to payment gateways, email/SMS providers, maps APIs, AI/ML services, storage); (4) all data stores and their access patterns (which services connect to which DBs, caches, queues, and with what credentials/roles). Your §0 Network Topology Diagram must document this full topology as the foundation for §10 NetworkPolicy and §9 IAM/RBAC decisions — security controls are only as strong as the topology they enforce. +Infrastructure and security must account for the full system topology. Before writing any config, identify: (1) all services, their ports, and inter-service communication paths (the complete network graph); (2) all external ingress points (public API, webhooks, OAuth callbacks, CDN, admin portals); (3) all external egress points (calls to payment gateways, email/SMS providers, maps APIs, AI/ML services, storage); (4) all data stores and their access patterns (which services connect to which DBs, caches, queues, and with what credentials/roles). Your §0 Network Topology Diagram must document this full topology as the foundation for §11 NetworkPolicy and §10 IAM/RBAC decisions — security controls are only as strong as the topology they enforce. + +PLATFORM CONVENTIONS — MANDATORY (scan RAG context before writing any config): +- If the RAG context documents a platform Dockerfile standard (e.g. non-root user, no `.npmrc` in runtime image, base image choice), apply it to every Dockerfile. +- If the RAG context documents a logger configuration convention (e.g. pino with a specific `service` field naming rule), apply the correct service name — never hardcode a different service name. +- If the RAG context documents a Kubernetes namespace, Helm chart structure, or ingress annotation convention, follow it exactly. CROSS-REFERENCE REQUIREMENTS: - Every Dockerfile/K8s config must cite the TA infrastructure decision it implements (e.g., "TA §8 TDR-09 container runtime", "SA §8 Deployment Architecture"). - Every CI/CD security gate must cite the tech_lead security finding or tester quality gate that mandated it (e.g., "tech_lead §6 OWASP A03 finding", "tester §1 exit criteria: no Critical defects"). -- Every secret or credential reference must cite the SA security architecture decision (e.g., "SA §6 secrets management strategy"). +- Every Secret reference must cite the SA security architecture decision (e.g., "SA §6 secrets management strategy"). - Every monitoring alert must cite the BA NFR it enforces (e.g., "BA §4 NFR-01 p95 <200ms", "BA §4 NFR-03 99.9% availability SLA"). - Link your own sections using "→ see §N" notation. @@ -776,24 +928,165 @@ class AgentConfig: Zone 2 — Internal App Tier: backend services, internal APIs, job workers, event consumers Zone 3 — Data Tier: databases, cache, message broker (no direct public access) Zone 4 — External/SaaS: payment gateway, email/SMS, maps, AI/ML, OAuth provider, analytics - Label every inter-service connection: protocol + port + TLS enforced (Y/N). Mark all ingress paths (from internet) and egress paths (to external services). This diagram is the mandatory foundation for §10 Network Policy and §9 IAM & RBAC Review. -1. Dockerfile & docker-compose Security Review (base image vuln check, non-root user, read-only fs, no secrets baked in) -2. Kubernetes YAML Security (PodSecurityContext, RBAC, NetworkPolicy, ResourceLimits, Secret refs) -3. CI/CD Pipeline with Security Gates (stages: lint, SAST, SCA, test, build, image-scan, DAST, deploy) + Label every inter-service connection: protocol + port + TLS enforced (Y/N). Mark all ingress paths (from internet) and egress paths (to external services). This diagram is the mandatory foundation for §11 Network Policy and §10 IAM & RBAC Review. + +1. Dockerfile (per service) + For EACH service identified in SA/TA: produce a production-grade, security-hardened Dockerfile. + Required for every Dockerfile: + - Multi-stage build: builder stage (installs deps + builds) → runtime stage (copies dist only). + - Non-root user: `RUN addgroup -S appgroup && adduser -S appuser -G appgroup` + `USER appuser`. + - No secrets baked in: `.npmrc`, `.env`, credentials MUST NOT appear in any layer. + - Minimal base image: node:X-alpine or distroless. Document the choice and CVE rationale. + - Read-only filesystem where possible: `--read-only` flag noted in K8s `securityContext`. + - HEALTHCHECK instruction included. + Also produce a `docker-compose.yml` scoped to LOCAL DEV ONLY — label it clearly: + `# ⚠️ LOCAL DEV ONLY — NOT for production deployment. Production target: Kubernetes (see §2).` + All sensitive values in docker-compose must reference `.env` file entries — never inline. + Provide a `.env.example` listing every required key with placeholder values. + +2. Kubernetes Manifests (PRIMARY production deployment — skip if docker-compose-only target) + For EACH service, produce complete, working YAML for: + + a. Namespace + ```yaml + apiVersion: v1 + kind: Namespace + metadata: + name: + ``` + + b. ConfigMap (non-sensitive config only: NODE_ENV, LOG_LEVEL, PORT, feature flags, service discovery hostnames) + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: -config + namespace: + data: + NODE_ENV: "production" + PORT: "3000" + # ... all non-sensitive env vars + ``` + + c. Secret (ALL sensitive values — db password, JWT secret, API keys, OAuth secrets, SMTP password) + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: -secrets + namespace: + type: Opaque + data: + # Base64-encoded placeholders — inject real values via CI/CD secret store at deploy time + # NEVER commit real values to Git + DATABASE_URL: # kubectl create secret generic ... --from-literal=DATABASE_URL=... + JWT_SECRET: + # ... all sensitive env vars + ``` + List every sensitive env var for every service. If a var appears sensitive (password, secret, key, token, credential, dsn, url with auth), it goes in Secret. + + d. Deployment + ```yaml + apiVersion: apps/v1 + kind: Deployment + spec: + template: + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 2000 + containers: + - name: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + envFrom: + - configMapRef: + name: -config # non-sensitive config + - secretRef: + name: -secrets # sensitive values — never inline + resources: + requests: { cpu: "100m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } + livenessProbe: { httpGet: { path: /health, port: 3000 }, initialDelaySeconds: 10 } + readinessProbe: { httpGet: { path: /ready, port: 3000 }, initialDelaySeconds: 5 } + ``` + + e. Service (ClusterIP for internal; LoadBalancer/NodePort only if externally required) + + f. Ingress (with TLS, rate-limit annotations, auth annotations if applicable) + ```yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/rate-limit: "100" # requests/min + spec: + tls: + - hosts: [] + secretName: + ``` + + g. HorizontalPodAutoscaler (minReplicas, maxReplicas, CPU/memory targets) + + h. PodDisruptionBudget (minAvailable: 1 for stateless services) + + Produce a complete manifest for every service. Do NOT produce a single generic template — each service gets its own tailored manifests. + +3. CI/CD Pipeline with Security Gates (stages: lint, SAST, SCA, unit-test, build, image-scan, DAST, deploy) + Provide the full pipeline YAML (GitHub Actions / GitLab CI / Jenkins — match TA tech stack decision). + Required stages in order: + 1. lint + type-check + 2. unit tests (fail pipeline if coverage drops below threshold) + 3. SAST (Semgrep / ESLint-security / Bandit — fail on HIGH+) + 4. SCA / dependency scan (Trivy fs or Snyk — fail on CRITICAL+) + 5. Docker build + 6. Container image scan (Trivy image — fail on CRITICAL+) + 7. Push image to registry (only if all prior gates pass) + 8. DAST (OWASP ZAP baseline — run against staging only) + 9. Deploy to staging → smoke tests → manual approval gate → deploy to production + Secret injection step: show how secrets are pulled from the secret store and injected into K8s Secrets at deploy time: + ```yaml + - name: Inject secrets into K8s + run: | + kubectl create secret generic -secrets \ + --from-literal=DATABASE_URL=${{ secrets.DATABASE_URL }} \ + --from-literal=JWT_SECRET=${{ secrets.JWT_SECRET }} \ + --dry-run=client -o yaml | kubectl apply -f - + ``` + 4. SAST Checklist (Semgrep/Bandit/ESLint-security: rules configured, fail threshold, findings triage) 5. DAST Checklist (OWASP ZAP / Burp: auth, injection, XSS, CSRF, API fuzz plan) 6. SCA Dependency Scan (Trivy/Snyk/Dependabot: severity threshold, auto-PR for patches) 7. Container Image Scanning (Trivy/Grype in CI, base image selection, update cadence) -8. Secrets Management (Vault / K8s Secrets / AWS SM: rotation, injection, no plaintext in code) -9. IAM & RBAC Review (least-privilege principle, service account roles, network egress rules) -10. Network Policy (ingress/egress rules, service mesh consideration, TLS enforcement) -11. Environment & Config Checklist (env vars per env: dev/staging/prod, no secrets in env vars) -12. Monitoring & Alerting Plan (metrics, alert thresholds, security event alerting, dashboard links) -13. Deployment Plan (ordered steps, blue-green / canary notes, validation gates between steps) -14. Rollback Plan (trigger conditions, rollback commands, data migration rollback) -15. Post-deployment Health Checks & Smoke Tests (endpoints, expected responses, security headers check) -16. Incident Response Runbook (detect → contain → eradicate → recover → post-mortem template) -17. Security Hardening Checklist (OS hardening, container hardening, network hardening, compliance notes) +8. Secrets Management + Table: | Secret Name | Service(s) That Need It | Source: Vault/AWS SM/GitHub Actions | K8s Secret Name | K8s Key | Rotation Period | Notes | + Rules enforced: + - Zero plaintext secrets in Git (enforced by pre-commit `detect-secrets` hook or `gitleaks`). + - Zero plaintext secrets in Deployment YAML `env:` blocks — all via `secretKeyRef` or `secretRef`. + - Zero secrets in docker-compose `environment:` blocks — all via `.env` file reference. + - Zero secrets in CI/CD pipeline YAML — all via CI secret store variables. + - Rotation: every secret must have a rotation period. Secrets with no stated rotation = flag as [Open Question]. + +9. Environment Variable Classification Table + For EVERY env var used across ALL services: + Table: | Env Var Name | Service(s) | Sensitive: Yes/No | Classification | Dev Value Source | Staging Value Source | Prod Value Source | K8s Object: ConfigMap/Secret | + Classification values: DB_CREDENTIAL / API_KEY / AUTH_SECRET / OAUTH_SECRET / SMTP_CREDENTIAL / SERVICE_URL / CONFIG / FEATURE_FLAG + This table is the authoritative reference for §2c Secret and §2b ConfigMap generation. + +10. IAM & RBAC Review (least-privilege principle, service account roles, network egress rules) +11. Network Policy (ingress/egress rules per service, service mesh consideration, TLS enforcement) +12. Environment & Config Checklist (per env: dev/staging/prod — confirm no secrets in env vars, no debug flags in prod) +13. Monitoring & Alerting Plan (metrics, alert thresholds, security event alerting, dashboard links) +14. Deployment Plan (ordered steps, blue-green / canary notes, validation gates between steps) +15. Rollback Plan (trigger conditions, rollback commands, data migration rollback) +16. Post-deployment Health Checks & Smoke Tests (endpoints, expected responses, security headers check) +17. Incident Response Runbook (detect → contain → eradicate → recover → post-mortem template) +18. Security Hardening Checklist (OS hardening, container hardening, network hardening, compliance notes) """, ), diff --git a/agent-api/app.py b/agent-api/app.py index b4492a4..a5eddc9 100644 --- a/agent-api/app.py +++ b/agent-api/app.py @@ -1,32 +1,90 @@ """ -app.py — YAN SDLC Agent Orchestrator API (cổng 8091) - -Endpoints ---------- -GET /health Thông tin service: Ollama URL, RAG URL, số agent. -GET /agents Liệt kê cấu hình tất cả agent (step, model, depends_on). -POST /agent/{role} Gọi đồng bộ một bước agent đơn lẻ. - Body: AgentStepRequest → AgentStepResponse -POST /workflow/run Gửi workflow SDLC 15 agents để chạy nền. - Body: WorkflowRunRequest → {workflow_id, status} -GET /workflow/{workflow_id} Kiểm tra trạng thái hoặc lấy kết quả đã hoàn thành. - Response: WorkflowRecord -GET /workflows Liệt kê workflow gần đây (mới nhất trước). +app.py — YAN SDLC Agent Orchestrator API (cổng 8091) +===================================================== + +Mô tả +----- +FastAPI application điều phối toàn bộ quy trình SDLC 15 bước thông qua LangGraph. +Mỗi bước là một AI agent độc lập với model, system prompt và dependency riêng biệt. +Workflow chạy bất đồng bộ (BackgroundTasks) và cập nhật trạng thái real-time. + +Danh sách endpoint +------------------ +GET /health + Trả về trạng thái hoạt động của service, URL Ollama, URL RAG và số lượng agent. + Dùng bởi Docker healthcheck và giám sát bên ngoài. + +GET /agents + Liệt kê cấu hình đầy đủ của 15 agent: step_id, name, model, depends_on. + +GET /ui + Phục vụ giao diện web SDLC Workflow (file static/workflow.html). + +POST /agent/{role} + Chạy đồng bộ một agent đơn lẻ theo role chỉ định. + Nhận AgentStepRequest (user_input, project, prev_outputs, tech_stack, rag_enabled, rag_top_k). + Trả về AgentStepResponse (role, name, model, output). + Hữu ích để test từng agent riêng lẻ hoặc ghép pipeline thủ công. + +POST /workflow/run + Xếp hàng workflow SDLC 15 bước chạy nền, trả về workflow_id ngay lập tức. + Nhận WorkflowRunRequest (user_input, project, rag_enabled, rag_top_k, tech_stack). + Trạng thái chuyển đổi: pending → running → completed | failed. + +GET /workflow/{workflow_id} + Kiểm tra trạng thái hoặc lấy toàn bộ kết quả workflow đã hoàn thành. + Trả về WorkflowRecord bao gồm step_outputs, completed_steps, artifacts, error. + +GET /workflows + Liệt kê tối đa 50 workflow gần nhất, sắp xếp theo thời gian tạo mới nhất trước. + +GET /workflow/{workflow_id}/artifacts + Liệt kê metadata của tất cả file artifact đã được trích xuất và lưu vào disk. + Không trả về nội dung file — dùng endpoint bên dưới để đọc từng file. + +GET /workflow/{workflow_id}/artifacts/{role}/{path} + Đọc nội dung một file artifact cụ thể dưới dạng text. + Thêm query param ?download=1 để tải xuống dưới dạng binary. Vòng đời workflow ----------------- - POST /workflow/run → status=pending (record lưu, BG task xếp hàng) - └─> nền → status=running (LangGraph invoke bắt đầu) - └─> xong → status=completed | failed - -Ghi chú Concurrency -------------------- -- _store_lock bảo vệ workflow_store khỏi đọc/ghi đồng thời. -- get_workflow() dùng double-checked locking để đồ thị chỉ được biên dịch - một lần dù nhiều request đến lúc khởi động. -- FastAPI BackgroundTasks chạy workflow trong thread-pool thread; - _run_workflow_task cập nhật WorkflowRecord trực tiếp (không cần re-insert - vì dict là kiểu tham chiếu). + POST /workflow/run + → WorkflowRecord tạo với status=pending, lưu vào workflow_store + → BackgroundTasks.add_task(_run_workflow_task) + → status=running, LangGraph StateGraph.stream() bắt đầu + → Mỗi node hoàn thành: cập nhật step_outputs, completed_steps, artifacts + → Sau node cuối cùng (clarifier): chạy Clarifier Regen Loop nếu được bật + → status=completed (hoặc failed nếu có lỗi không xử lý được) + +Clarifier Regen Loop +-------------------- +Sau khi workflow hoàn tất thành công, _run_workflow_task phân tích §10 của +Clarifier output để tìm danh sách agent cần re-generate. Với mỗi vòng lặp +(tối đa CLARIFIER_REGEN_LOOPS lần): + 1. Re-run từng agent trong danh sách theo thứ tự WORKFLOW_STEPS + 2. Re-run Clarifier với outputs đã cập nhật + 3. Kiểm tra lại §10 — nếu rỗng thì dừng sớm + +Concurrency và thread-safety +----------------------------- +- workflow_store là dict in-memory, được bảo vệ bởi _store_lock (threading.Lock) + cho mọi thao tác đọc/ghi để tránh race condition. +- get_workflow() dùng double-checked locking: kiểm tra trước lock, kiểm tra lại + sau lock, đảm bảo LangGraph StateGraph chỉ được compile đúng một lần dù + nhiều request đến đồng thời lúc khởi động. +- _run_workflow_task chạy trong thread-pool của FastAPI BackgroundTasks. + WorkflowRecord được cập nhật trực tiếp (pass-by-reference) — không cần re-insert. +- Artifact extraction chạy ngay sau mỗi node hoàn thành (non-fatal: exception + trong extraction chỉ ghi warning log, không dừng workflow). + +Bộ nhớ và giới hạn +------------------ +- Tối đa _MAX_STORED_WORKFLOWS (50) workflow trong bộ nhớ. Entry cũ nhất bị xóa + khi đạt giới hạn. +- user_input được sanitize tại boundary: loại bỏ ký tự điều khiển, cắt tại + _MAX_INPUT_CHARS (10.000) ký tự. +- Mỗi workflow run được ghi vào data/memory/episodic/workflow_runs.jsonl để + làm seed dữ liệu lịch sử (episodic memory). """ import json @@ -53,11 +111,16 @@ SDLCState, get_workflow, run_single_step, + _parse_clarifier_regen_list, ) def _require_env(name: str) -> str: - """Trả về giá trị biến môi trường *name*, raise RuntimeError nếu không tồn tại.""" + """Trả về giá trị biến môi trường *name*. + + Raise RuntimeError nếu biến không tồn tại — đảm bảo service không khởi động + thiếu cấu hình bắt buộc. Các biến tùy chọn nên dùng os.environ.get() trực tiếp. + """ value = os.environ.get(name) if value is None: raise RuntimeError( @@ -79,7 +142,7 @@ def _require_env(name: str) -> str: _STATIC_DIR = Path(__file__).parent / "static" -# ── Mô hình Request / Response ───────────────────────────────────────────── +# ── Models Pydantic cho Request và Response của các endpoint ──────────────────────────────────────── class AgentStepRequest(BaseModel): user_input: str = Field(..., min_length=1, @@ -144,26 +207,33 @@ class WorkflowRecord(BaseModel): completed_at: str | None = None -# ── Lưu trữ workflow trong bộ nhớ (khóa theo workflow_id) ────────────────────── -# Dùng cho local stack; môi trường production nên dùng Redis hoặc database. -# _store_lock đồng bộ hóa mọi thao tác đọc/ghi để tránh race condition. +# ── In-memory workflow store — đồng bộ hóa bằng _store_lock ─────────────────── +# workflow_store lưu kết quả của tất cả workflow trong RAM. Giới hạn +# _MAX_STORED_WORKFLOWS entry — entry cũ nhất bị xóa khi đạt giới hạn. +# Môi trường production cần Redis hoặc database để persistence. workflow_store: dict[str, WorkflowRecord] = {} _store_lock = threading.Lock() -_MAX_STORED_WORKFLOWS = 50 # xóa entry cũ nhất khi đạt giới hạn +_MAX_STORED_WORKFLOWS = 50 +# Số vòng lặp Clarifier re-generation tối đa. 0 = tắt toàn bộ tính năng. +# Tăng lên 2 nếu muốn nhiều vòng tinh chỉnh hơn (tốn thêm thời gian). +_CLARIFIER_REGEN_LOOPS: int = int(os.environ.get("CLARIFIER_REGEN_LOOPS", "1")) -# ── Tài nguyên bộ nhớ sự kiện ──────────────────────────────────────────────────── +# ── Đường dẫn bộ nhớ episodic và giới hạn input ──────────────────────────── MEMORY_DIR: str = os.environ.get("MEMORY_DIR", "/data/memory") -_MAX_INPUT_CHARS: int = 10_000 # Giới hạn input để phòng context overflow +_MAX_INPUT_CHARS: int = 10_000 # Giới hạn 10.000 ký tự để phòng chống context overflow và DoS. def _sanitize_input(text: str) -> str: - """ - Kiểm tra và làm sạch input người dùng tại system boundary. - Loại bỏ ký tự điều khiển (ngoại trừ newline/tab) và cắt ngắn nếu quá dài. - Theo nguyên tắc SecOps: validate input trước khi đưa vào LLM context. + """Sanitize input người dùng tại system boundary trước khi đưa vào LLM context. + + Hai bước xử lý: + 1. Loại bỏ ký tự điều khiển (control characters) ngoại trừ newline, carriage return + và tab — ngăn chặn injection qua ANSI escape hay null bytes. + 2. Cắt ngắn tại _MAX_INPUT_CHARS (10.000) nếu input vượt quá — tránh overflow + context window của model và chống DoS qua input khổng lồ. """ sanitized = "".join(ch for ch in text if ch >= " " or ch in "\n\r\t") if len(sanitized) > _MAX_INPUT_CHARS: @@ -183,9 +253,11 @@ def _log_workflow_run( error: str | None, duration_seconds: float, ) -> None: - """ - Ghi thông tin workflow run ra file JSONL làm seed dữ liệu episodic memory. - Non-fatal: lỗi ghi file không làm gián đoạn workflow. + """Ghi thông tin workflow run ra file JSONL để làm dữ liệu lịch sử (episodic memory). + + Mỗi dòng JSONL lưu: workflow_id, project, user_input (500 ký tự đầu), + danh sách bước đã chạy, số bước, trạng thái, lỗi, thời gian chạy và timestamp. + Non-fatal: lỗi ghi file chỉ ghi warning log, không làm gán đoạn workflow. """ try: log_dir = Path(MEMORY_DIR) / "episodic" @@ -210,7 +282,10 @@ def _log_workflow_run( def _store_workflow(record: WorkflowRecord) -> None: - """Lưu *record* vào store, xóa entry cũ nhất khi store đầy.""" + """Lưu *record* vào workflow_store, tự động xóa entry cũ nhất khi store đạt giới hạn. + + Thread-safe: dùng _store_lock để đồng bộ hóa ghi vào dict in-memory. + """ with _store_lock: if len(workflow_store) >= _MAX_STORED_WORKFLOWS: oldest_key = next(iter(workflow_store)) @@ -218,16 +293,18 @@ def _store_workflow(record: WorkflowRecord) -> None: workflow_store[record.workflow_id] = record -# ── Chạy workflow nền ─────────────────────────────────────────────────────────────── +# ── Task nền chạy toàn bộ SDLC workflow ──────────────────────────────────────────────────── def _run_workflow_task(workflow_id: str, req: WorkflowRunRequest) -> None: - """ - Task nền: chạy toàn bộ SDLC LangGraph workflow và cập nhật - WorkflowRecord trực tiếp. Được gọi bởi FastAPI BackgroundTasks. + """Task nền thực thi toàn bộ SDLC LangGraph workflow và cập nhật WorkflowRecord. + + Được gọi bởi FastAPI BackgroundTasks — chạy trong thread-pool thread riêng biệt. - Việc cập nhật *record* an toàn vì: - - Chỉ có duy nhất một background task cho mỗi workflow_id. - - Các endpoint đọc chỉ đọc các trường được gán nguyên tử (status, timestamps). + Thread-safety: + - Chỉ một background task tồn tại cho mỗi workflow_id. + - WorkflowRecord được cập nhật trực tiếp (pass-by-reference) vì dict + là kiểu tham chiếu — không cần re-insert vào store. + - Các endpoint đọc (GET /workflow/{id}) chỉ đọc các trường được ghi nguyên tử. """ record = workflow_store.get(workflow_id) if record is None: @@ -236,7 +313,8 @@ def _run_workflow_task(workflow_id: str, req: WorkflowRunRequest) -> None: record.status = WorkflowStatus.running logger.info("Workflow %s started", workflow_id) - # user_input đã được sanitize khi enqueue — dùng lại từ record để tránh xử lý hai lần. + # user_input đã được sanitize lúc enqueue và lưu vào record — tái sử dụng trực tiếp + # thay vì sanitize lại từ req.user_input để đảm bảo nhất quán. sanitized_input = record.user_input initial_state: SDLCState = { @@ -256,13 +334,16 @@ def _run_workflow_task(workflow_id: str, req: WorkflowRunRequest) -> None: try: final_state: SDLCState = {} # type: ignore[assignment] for chunk in get_workflow().stream(initial_state, stream_mode="updates"): - # chunk = {node_name: partial_state_dict} (stream_mode="updates") + # chunk = {ten_node: partial_state_dict} với stream_mode="updates". + # Mỗi chunk cập nhật được ngay lập tức vào record để GET /workflow/{id} + # phản ánh tiến trình real-time. for node_output in chunk.values(): if isinstance(node_output, dict): # Cập nhật real-time để GET /workflow/{id} phản ánh tiến trình if "step_outputs" in node_output: record.step_outputs.update(node_output["step_outputs"]) - # Trích xuất artifact cho các role có file code (non-fatal) + # Trích xuất và lưu artifact ngay sau khi mỗi coding node hoàn thành. + # Non-fatal: lỗi extraction chỉ ghi log, không dừng workflow. for _role, _out in node_output["step_outputs"].items(): if ( _role in _ARTIFACT_ROLES @@ -282,6 +363,94 @@ def _run_workflow_task(workflow_id: str, req: WorkflowRunRequest) -> None: record.error = node_output["error"] final_state.update(node_output) record.status = WorkflowStatus.failed if record.error else WorkflowStatus.completed + + # ── Clarifier Regen Loop ─────────────────────────────────────────────────────────────────── + # Chỉ chạy khi workflow hoàn thành thành công và CLARIFIER_REGEN_LOOPS > 0. + # Clarifier phân tích §10 để lấy danh sách agent cần re-gen, re-run từng agent, + # sau đó re-run Clarifier để đánh giá lại — lặp tối đa CLARIFIER_REGEN_LOOPS lần. + if record.status == WorkflowStatus.completed and _CLARIFIER_REGEN_LOOPS > 0: + _live_outputs: dict[str, str] = dict(record.step_outputs) + for _loop_idx in range(_CLARIFIER_REGEN_LOOPS): + regen_roles = _parse_clarifier_regen_list( + _live_outputs.get("clarifier", "") + ) + if not regen_roles: + logger.info( + "Clarifier regen loop %d/%d: không có agent nào cần re-gen — dừng.", + _loop_idx + 1, _CLARIFIER_REGEN_LOOPS, + ) + break + + logger.info( + "Clarifier regen loop %d/%d: re-generating roles=%s", + _loop_idx + 1, _CLARIFIER_REGEN_LOOPS, regen_roles, + ) + record.status = WorkflowStatus.running + + # Re-run từng agent được đề xuất theo thứ tự WORKFLOW_STEPS (dependency order). + for _role in regen_roles: + try: + _new_output = run_single_step( + role=_role, + user_input=sanitized_input, + project=req.project, + prev_outputs=_live_outputs, + tech_stack=req.tech_stack, + rag_enabled=req.rag_enabled, + rag_top_k=req.rag_top_k, + ollama_base_url=OLLAMA_BASE_URL, + rag_api_url=RAG_API_URL, + ) + _live_outputs[_role] = _new_output + record.step_outputs[_role] = _new_output + # Re-extract artifact nếu role này có file code — cập nhật artifacts sau re-gen. + if ( + _role in _ARTIFACT_ROLES + and _new_output + and not _new_output.startswith("[LỖI") + ): + _arts = _extract_artifacts(_role, _new_output, workflow_id) + if _arts: + record.artifacts[_role] = _arts + logger.info( + "Clarifier regen loop %d: %s re-generated (%d chars)", + _loop_idx + 1, _role, len(_new_output), + ) + except Exception as _regen_exc: + logger.warning( + "Clarifier regen loop %d: re-gen '%s' thất bại (non-fatal): %s", + _loop_idx + 1, _role, _regen_exc, + ) + + # Re-run clarifier với outputs đã cập nhật + try: + _new_clarifier = run_single_step( + role="clarifier", + user_input=sanitized_input, + project=req.project, + prev_outputs=_live_outputs, + tech_stack=req.tech_stack, + rag_enabled=req.rag_enabled, + rag_top_k=req.rag_top_k, + ollama_base_url=OLLAMA_BASE_URL, + rag_api_url=RAG_API_URL, + ) + _live_outputs["clarifier"] = _new_clarifier + record.step_outputs["clarifier"] = _new_clarifier + logger.info( + "Clarifier regen loop %d: clarifier re-run xong (%d chars)", + _loop_idx + 1, len(_new_clarifier), + ) + except Exception as _clarifier_exc: + logger.warning( + "Clarifier regen loop %d: clarifier re-run thất bại: %s — dừng loop.", + _loop_idx + 1, _clarifier_exc, + ) + break + + record.status = WorkflowStatus.failed if record.error else WorkflowStatus.completed + # ── Kết thúc Clarifier Regen Loop ───────────────────────────────────────── + except Exception as exc: logger.exception("Workflow %s thất bại", workflow_id) record.status = WorkflowStatus.failed diff --git a/agent-api/artifacts.py b/agent-api/artifacts.py index 0e63baf..f0dd989 100644 --- a/agent-api/artifacts.py +++ b/agent-api/artifacts.py @@ -1,24 +1,77 @@ """ -artifacts.py — Trích xuất và lưu trữ file code từ output của SDLC agents. - -Sau khi mỗi bước workflow hoàn thành, output markdown được quét để tìm: - 1. Explicit ### FILE: path/to/file.ext (ưu tiên — định dạng có cấu trúc) - 2. Code block có dòng comment đầu chứa tên file (// filename: ...) - 3. Bold/heading text ngay trước code block (**src/Login.tsx**) - 4. Fallback: tự đặt tên theo ngôn ngữ + index (lang-01.ts) - -Full raw output luôn được lưu dưới tên _output.md. - -Layout thư mục: - /data/artifacts/{workflow_id}/{role}/ - _output.md ← luôn có: toàn bộ markdown output của agent - src/Login.tsx ← file code được trích xuất - schema.sql ← v.v. +artifacts.py — Trích xuất và lưu trữ file code từ output markdown của SDLC agents +================================================================================== + +Mô tả +----- +Sau khi mỗi coding agent hoàn thành (fe, mobile, be, dba, da, tech_lead, +devsecops), toàn bộ output markdown được lưu và quét để tách riêng từng +file code, sau đó ghi ra disk theo layout chuẩn. + +Layout thư mục artifact +----------------------- + /data/artifacts/{workflow_id}/{role}/ + _output.md — Toàn bộ markdown output gốc của agent (luôn có) + src/pages/Login.tsx — File được trích xuất từ directive ### FILE: + src/hooks/useAuth.ts + schema.sql + Dockerfile + ... + +Thứ tự ưu tiên nhận diện tên file trong markdown +------------------------------------------------- +1. Directive có cấu trúc (ưu tiên cao nhất): + ### FILE: src/components/Button.tsx + ```typescript + ... + ``` + Agent được yêu cầu dùng định dạng này trong system prompt. + +2. Dòng comment đầu tiên của code block: + ```typescript + // filename: src/components/Button.tsx + ... + ``` + Hỗ trợ các prefix: //, #, *, --, /* và các biến thể như "file:", "filename:". + +3. Bold hoặc heading ngay trước code block: + **src/components/Button.tsx** + ```typescript + ... + ``` + +4. Fallback — đặt tên tự động: + Khi không tìm được tên file từ bất kỳ pattern nào, file được đặt tên + theo ngôn ngữ và index: typescript-01.ts, python-02.py, yaml-03.yml... + Dockerfile được xử lý đặc biệt (không có extension). + +Bảo mật đường dẫn +----------------- +_sanitize_relpath() chuẩn hóa và làm sạch mọi đường dẫn do LLM sinh ra: +- Chuyển backslash Windows thành forward slash +- Loại bỏ path traversal: /../, /./ +- Chỉ cho phép ký tự an toàn: chữ cái, chữ số, dấu chấm, dấu gạch ngang, + dấu gạch dưới và dấu slash +- Đường dẫn rỗng sau làm sạch được thay bằng "file.txt" + +Hằng số xuất khẩu +----------------- +- ARTIFACT_ROLES: frozenset[str] — các role có artifact được trích xuất +- extract_and_save(role, output, workflow_id) → list[dict] — hàm chính +- list_artifacts(workflow_id) → dict[str, list[dict]] — liệt kê artifacts +- read_artifact(workflow_id, role, path) → str | None — đọc nội dung file + +Lưu ý +----- +Mọi exception trong quá trình trích xuất đều được bắt và ghi log warning +— không làm dừng workflow chính. _output.md luôn được ghi trước, đảm bảo +output thô không bao giờ bị mất dù trích xuất file code thất bại. """ from __future__ import annotations import logging import os +import posixpath import re from pathlib import Path @@ -26,12 +79,14 @@ ARTIFACT_BASE: str = os.environ.get("ARTIFACT_DIR", "/data/artifacts") -# Chỉ trích xuất artifact cho các role này +# Chỉ trích xuất và lưu artifact cho các role coding này. +# Planning roles (ba, pm, sa, ta...) và tester không sinh code file nên không cần trích xuất. ARTIFACT_ROLES: frozenset[str] = frozenset( {"fe", "mobile", "be", "dba", "da", "tech_lead", "devsecops"} ) -# language identifier → file extension (không có dấu chấm) +# Bảng ánh xạ từ tên ngôn ngữ code fence sang phần mở rộng file (không có dấu chấm). +# __dockerfile__ là sentinel đặc biệt — được xử lý riêng để tạo file "Dockerfile" không extension. _LANG_EXT: dict[str, str] = { "typescript": "ts", "ts": "ts", "tsx": "tsx", @@ -55,30 +110,46 @@ "env": "env", } -# Khớp: ### FILE: some/path/file.ext rồi đến ```lang\ncontent\n``` +# Pattern ưu tiên 1: ### FILE: path/to/file.ext rồi đến code block ngay sau. +# Đây là định dạng có cấu trúc cao nhất — agent được yêu cầu dùng pattern này. _FILE_HDR_RE = re.compile( r"###\s+FILE:\s*([^\n]+?)\s*\n```(\w*)\n(.*?)```", re.DOTALL, ) -# Khớp bất kỳ code block nào +# Pattern dự phòng: khớp bất kỳ code block nào trong markdown. _BLOCK_RE = re.compile(r"```(\w+)?\n(.*?)```", re.DOTALL) -# Tên file từ dòng comment đầu tiên của code block +# Pattern nhận diện tên file từ dòng comment đầu tiên bên trong code block. +# Hỗ trợ các prefix comment: //, #, *, -- và biến thể "file:", "filename:". _CMT_FNAME_RE = re.compile( r"^[/#*\-]+\s*(?:file(?:name)?:\s*)?([a-zA-Z0-9_.][^\s]*\.[a-zA-Z0-9]{1,10})\s*$", re.IGNORECASE, ) -# Bold / heading ngay trước code block — ví dụ: **src/Login.tsx** hoặc ### src/Login.tsx +# Pattern nhận diện tên file từ bold/heading Markdown ngay trước code block. +# Ví dụ: **src/Login.tsx** hoặc ### src/Login.tsx ngay trước ```typescript. _PRE_FNAME_RE = re.compile( r"(?:\*{1,2}|`|#{1,4}\s+)([a-zA-Z0-9_.][^\s*`\n]*\.[a-zA-Z0-9]{1,10})`?\*{0,2}\s*:?\s*\n?\s*$" ) def _sanitize_relpath(raw: str) -> str: - """Loại bỏ path-traversal và ký tự không hợp lệ.""" - p = raw.replace("..", "").lstrip("/").strip() + """Chuẩn hóa và làm sạch đường dẫn tương đối do LLM sinh ra. + + Bảo vệ chống path traversal và ký tự không hợp lệ: + 1. Chuyển backslash Windows thành forward slash. + 2. Loại bỏ /../ và /./ bằng posixpath.normpath. + 3. Xóa các thành phần .. còn lại và dấu / đầu tiên. + 4. Chỉ cho phép: chữ cái, chữ số, dấu chấm, gạch ngang, gạch dưới, slash. + 5. Trả về "file.txt" nếu kết quả rỗng sau làm sạch. + """ + # Chuyển backslash Windows → forward slash, sau đó resolve path traversal. + normalized = posixpath.normpath(raw.replace("\\", "/")) + # Loại bỏ .. còn lại và dấu / đầu tiên sau normpath. + parts = [p for p in normalized.split("/") if p and p != ".."] + p = "/".join(parts) + # Chỉ cho phép ký tự an toàn: word chars, dấu chấm, gạch ngang, gạch dưới, slash. p = re.sub(r"[^\w.\-/]", "_", p) return p or "file.txt" @@ -99,9 +170,11 @@ def _default_name(lang: str, idx: int) -> str: # ───────────────────────────────────────────────────────────────────────────── def extract_and_save(role: str, output: str, workflow_id: str) -> list[dict]: - """ - Trích xuất code block có tên từ *output*, ghi file, trả về danh sách metadata. - Non-fatal: lỗi được log, trả về list rỗng (hoặc một phần) nếu thất bại. + """Trích xuất các code block có tên file từ *output*, ghi ra disk, trả về danh sách metadata. + + Bao bọc _do_extract() bằng try/except để không làm crash workflow chính. + Nếu role không nằm trong ARTIFACT_ROLES hoặc output rỗng, trả về [] ngay lập tức. + Non-fatal: lỗi trích xuất chỉ ghi warning log, không ảnh hưởng đến workflow. """ if role not in ARTIFACT_ROLES: return [] @@ -117,11 +190,12 @@ def extract_and_save(role: str, output: str, workflow_id: str) -> list[dict]: def _do_extract(role: str, output: str, workflow_id: str) -> list[dict]: base = Path(ARTIFACT_BASE) / workflow_id / role base.mkdir(parents=True, exist_ok=True) + base_resolved = base.resolve() # compute once; passed to every _save call artifacts: list[dict] = [] seen: set[str] = set() - # ── Pass 1: explicit ### FILE: headers ────────────────────────── + # ── Pass 1: tìm và lưu các file theo directive ### FILE: (ưu tiên cao nhất) ─────────────── structured_spans: list[tuple[int, int]] = [] for m in _FILE_HDR_RE.finditer(output): raw_path = m.group(1) @@ -130,13 +204,13 @@ def _do_extract(role: str, output: str, workflow_id: str) -> list[dict]: if not content.strip(): continue rel = _sanitize_relpath(raw_path) - _save(base, role, rel, content, lang, artifacts, seen) + _save(base, base_resolved, role, rel, content, lang, artifacts, seen) structured_spans.append((m.start(), m.end())) def _in_structured(start: int) -> bool: return any(s <= start <= e for s, e in structured_spans) - # ── Pass 2: free code blocks (fallback) ───────────────────────── + # ── Pass 2: code block tự do (dự phòng khi không có ### FILE: directive) ───────────── counter = [0] for m in _BLOCK_RE.finditer(output): if _in_structured(m.start()): @@ -149,14 +223,14 @@ def _in_structured(start: int) -> bool: filename = None save_content = content - # Thử tên file từ dòng comment đầu + # Thử tên file từ dòng comment đầu tiên bên trong code block. if lines: m2 = _CMT_FNAME_RE.match(lines[0].strip()) if m2: filename = m2.group(1).strip() save_content = "\n".join(lines[1:]) - # Thử tên file từ text ngay trước code block + # Thử tên file từ bold/heading Markdown ngay trước code block. if not filename: pre = output[: m.start()].rstrip() m3 = _PRE_FNAME_RE.search(pre) @@ -168,9 +242,10 @@ def _in_structured(start: int) -> bool: filename = _default_name(lang, counter[0]) rel = _sanitize_relpath(filename) - _save(base, role, rel, save_content, lang, artifacts, seen) + _save(base, base_resolved, role, rel, save_content, lang, artifacts, seen) - # ── Luôn lưu toàn bộ output raw dưới dạng _output.md ─────────── + # ── Luôn lưu toàn bộ output thô dưới dạng _output.md ────────────────────────────── + # Đây là bản gốc đầy đủ không qua xử lý — đảm bảo output không bao giờ bị mất. md_path = base / "_output.md" md_path.write_text(output, encoding="utf-8") artifacts.insert(0, { @@ -188,6 +263,7 @@ def _in_structured(start: int) -> bool: def _save( base: Path, + base_resolved: Path, role: str, rel: str, content: str, @@ -195,14 +271,21 @@ def _save( artifacts: list, seen: set, ) -> None: + """Ghi nội dung file vào disk tại đường dẫn *base/rel*. + + Kiểm tra path confinement trước khi ghi để đảm bảo file luôn nằm trong + thư mục *base* — ngăn chặn path traversal qua workflow_id hoặc role giả mạo. + Bỏ qua file trùng tên (rel đã có trong seen) để tránh ghi đè artifact. + Non-fatal: lỗi ghi file chỉ ghi warning log, không dừng extraction. + """ if rel in seen: return seen.add(rel) dest = base / rel - # Defense-in-depth: confirm the resolved path stays within base (guards against - # any edge case that bypasses _sanitize_relpath). + # Defense-in-depth: xác nhận đường dẫn đã resolve vẫn nằm trong base. + # Bảo vệ khỏi các edge case không bị _sanitize_relpath chặn được. try: - dest.resolve().relative_to(base.resolve()) + dest.resolve().relative_to(base_resolved) except ValueError: logger.warning("Path confinement blocked write outside base: %s", dest) return @@ -222,7 +305,11 @@ def _save( # ───────────────────────────────────────────────────────────────────────────── def list_artifacts(workflow_id: str) -> dict[str, list[dict]]: - """Quét thư mục artifact đã lưu, trả về {role: [file_meta]}.""" + """Quét thư mục artifact của *workflow_id*, trả về dict {role: [file_metadata]}. + + Mỗi phần tử metadata bao gồm: path, filename, language, size (bytes). + Trả về {} nếu workflow chưa có artifact hoặc thư mục không tồn tại. + """ wf_dir = Path(ARTIFACT_BASE) / workflow_id if not wf_dir.exists(): return {} @@ -248,15 +335,22 @@ def list_artifacts(workflow_id: str) -> dict[str, list[dict]]: def read_artifact(workflow_id: str, role: str, rel_path: str) -> tuple[str, str] | None: - """ - Trả về (content, language) hoặc None nếu không tìm thấy. - *rel_path* là đường dẫn tương đối từ thư mục role. + """Đọc và trả về nội dung của một file artifact cụ thể. + + Tham số: + workflow_id: ID của workflow đã chạy. + role: Tên role agent (ví dụ: "fe", "be"). + rel_path: Đường dẫn tương đối từ thư mục role (ví dụ: "src/Login.tsx"). + + Trả về: + (content, language): nội dung file và tên ngôn ngữ. + None: nếu file không tồn tại hoặc path bị chặn do bảo mật. """ safe = _sanitize_relpath(rel_path) base = Path(ARTIFACT_BASE) path = base / workflow_id / role / safe - # Ensure the resolved path stays within ARTIFACT_BASE to block traversal via - # crafted workflow_id or role parameters. + # Đảm bảo đường dẫn đã resolve nằm trong ARTIFACT_BASE để chặn traversal + # qua workflow_id hoặc role parameter giả mạo từ HTTP request. try: path.resolve().relative_to(base.resolve()) except ValueError: diff --git a/agent-api/workflow.py b/agent-api/workflow.py index d91a1fa..9ac6307 100644 --- a/agent-api/workflow.py +++ b/agent-api/workflow.py @@ -1,34 +1,103 @@ """ -workflow.py — Bộ điều phối SDLC workflow dùng LangGraph. +workflow.py — Bộ điều phối SDLC workflow 15 bước dùng LangGraph +================================================================ + +Mô tả +----- +Module trung tâm xây dựng và vận hành LangGraph StateGraph cho toàn bộ pipeline +SDLC. Mỗi bước (node) trong đồ thị tương ứng một AgentConfig từ agents.py. Cấu trúc đồ thị (tuần tự nghiêm ngặt) -------------------------------------- - ba -> pm -> sa -> ta -> designer -> tl -> fe -> mobile -> dba -> be -> da -> tech_lead -> tester -> devsecops -> clarifier -> END - -Mỗi node thực thi ba giai đoạn: - 1. Tổng hợp context — chèn output (đã cắt ngắn) từ các bước phụ thuộc. - 2. Bổ sung RAG — gọi HTTP tùy chọn đến rag-api /ask để lấy kiến thức ngữ cảnh. - 3. Gọi LLM — gọi ChatOllama với system prompt của agent + context đã tổng hợp. - -Quản lý State (LangGraph reducers) ------------------------------------ - step_outputs : Annotated[dict, operator.or_] — mỗi node gộp slice của mình - completed_steps: Annotated[list, operator.add] — mỗi node nối thêm vai trò vào list - Các trường còn lại là TypedDict thông thường (ghi đè lần cuối thắng). + ba → pm → sa → ta → designer → tl → fe → mobile → dba → be + → da → tech_lead → tester → devsecops → clarifier → END + +Không có nhánh song song. Mỗi node đợi tất cả node phụ thuộc hoàn thành +(thứ tự được đảm bảo bởi chuỗi cạnh tuần tự trong StateGraph). + +Vòng đời mỗi node — 3 giai đoạn +--------------------------------- + 1. Tổng hợp context (bắt buộc) + _build_context_parts() thu thập: + - user_input (mục tiêu kinh doanh gốc từ người dùng) + - tech_stack bắt buộc nếu được cung cấp + - output rút gọn từ các agent phụ thuộc (dep_max_chars per dep) + - gợi ý ngày hiện tại cho planning roles (ba, pm, sa, ta) + - gợi ý kiểu DB (nosql/sql/both) cho dba và da + - extra_context tùy chọn + + 2. Bổ sung RAG (tùy chọn, khi rag_enabled=True) + _query_rag() gọi POST /ask đến rag-api với rag_query_hint của agent. + Kết quả được cắt ngắn và thêm vào context. Lỗi RAG không dừng workflow. + + 3. Gọi LLM (bắt buộc) + Artifact roles (fe, mobile, be, dba, da, tech_lead, devsecops): + _generate_artifacts_multi_turn() — 2 pha sinh code: + Pha 1: CODING_PLANNER_MODEL lập kế hoạch danh sách file, + tham chiếu TL task board để không bỏ sót task. + Pha 2: coding model sinh từng file độc lập với context tập trung. + Nếu còn TL task chưa cover → follow-up pass sinh thêm file. + Kết thúc: _build_task_checklist() tạo bảng ✅/⏳ đối chiếu TL tasks. + Các role còn lại: + _call_agent() — gọi LLM một lần với toàn bộ context. + Nếu output < 30 ký tự → retry với context rút gọn 1.500 ký tự cuối. + +Quản lý State (LangGraph TypedDict + reducers) +---------------------------------------------- + step_outputs : Annotated[dict, operator.or_] + Mỗi node trả về {role: output_text}. LangGraph gộp bằng operator.or_. + + completed_steps: Annotated[list, operator.add] + Mỗi node nối [role] vào list. LangGraph dùng operator.add. + + Các trường còn lại (project, user_input, tech_stack, error): + TypedDict thông thường — ghi đè lần cuối thắng. Giới hạn context window ------------------------ - Mỗi output phụ thuộc được cắt tối đa MAX_PREV_OUTPUT_CHARS (3 000 ký tự). - Kết quả RAG cũng được cắt theo cùng giới hạn. - Tổng context mỗi bước ≈ (|deps| + 1) × 3 000 + system_prompt + user_input. - Ollama context length được đặt là 32 768 tokens (OLLAMA_CONTEXT_LENGTH). - -Concurrency ------------ - _workflow_lock dùng double-checked locking để đồ thị được biên dịch đúng một lần - dù nhiều request đến đồng thời. + MAX_PREV_OUTPUT_CHARS (3.000): giới hạn mặc định per dep output. + _PER_DEP_CHARS: ghi đè per-role — tech_lead: 800, devsecops: 1.000, + tester: 3.000, clarifier: 1.500. + OLLAMA_NUM_CTX: context window Ollama, mặc định 32.768 token. + +Xử lý reasoning models +----------------------- + _REASONING_MODELS: frozenset khớp chuỗi con tên model reasoning + (phi4-mini-reasoning, phi4-reasoning, qwq, deepseek-r1, qwen3). + _strip_thinking(): loại bỏ ... khỏi output trước khi + lưu vào step_outputs — chỉ giữ lại phần nội dung thực sự. + +Chống vòng lặp output +---------------------- + _deloop(): cắt các dòng lặp liên tiếp quá giới hạn. Phòng ngừa + coding model nhỏ có xu hướng lặp lại boilerplate vô tận. + +TL Task Tracking +---------------- + _extract_tl_tasks(role, context) → list[dict] + _find_uncovered_tasks(tl_tasks, file_infos) → list[dict] + _build_task_checklist(tl_tasks, file_infos) → str (markdown table) + +Clarifier Regen Loop +-------------------- + _parse_clarifier_regen_list(clarifier_output) → list[str] + Parse §10 Recommended Re-generation List, chỉ trả về roles thuộc + _REGEN_ELIGIBLE_ROLES. Logic vòng lặp thực thi trong app.py. + +Singleton workflow (thread-safe) +--------------------------------- + _workflow_lock + double-checked locking: StateGraph chỉ compile + đúng một lần dù nhiều request đến đồng thời lúc khởi động. + Sau khi compile lần đầu, _workflow được cache module-level. + +Hàm xuất khẩu công khai +----------------------- + get_workflow() → CompiledGraph đã compile + run_single_step(role, ...) → str → chạy một agent độc lập + _parse_clarifier_regen_list(text) → list → parse §10 cho regen loop """ +import functools import json as _json import logging import os @@ -50,23 +119,29 @@ OLLAMA_BASE_URL: str = os.environ.get("OLLAMA_BASE_URL", "http://ollama:11434") RAG_API_URL: str = os.environ.get("RAG_API_URL", "http://rag-api:8090") RAG_TOP_K: int = int(os.environ.get("RAG_TOP_K", "5")) -# Context window Ollama (tokens). Tăng nếu model hỗ trợ window lớn hơn. +# Kích thước context window (token) gửi cho Ollama mỗi lần gọi LLM. +# Tăng giá trị này nếu model hỗ trợ window lớn hơn (ví dụ: 65536). OLLAMA_NUM_CTX: int = int(os.environ.get("OLLAMA_CONTEXT_LENGTH", "32768")) -# Timeout (giây) cho mỗi lần gọi RAG /ask. +# Timeout (giây) chờ rag-api trả về kết quả mỗi lần gọi POST /ask. RAG_TIMEOUT: int = int(os.environ.get("RAG_TIMEOUT", "600")) -# Timeout (giây) cho mỗi lần gọi Ollama LLM. +# Timeout (giây) chờ Ollama trả về response cho mỗi lần gọi LLM. +# Nên đặt cao hơn RAG_TIMEOUT vì LLM generation tốn thời gian hơn embed. OLLAMA_REQUEST_TIMEOUT: int = int(os.environ.get("OLLAMA_REQUEST_TIMEOUT", "1200")) -# Model dùng để lập kế hoạch danh sách file (chat model, không phải code-completion model). +# Model nhẹ chuyên lập kế hoạch danh sách file cần sinh — dùng chat model, +# không phải coding model, vì nhiệm vụ là phân tích task rồi xuất JSON array. CODING_PLANNER_MODEL: str = os.environ.get( "CODING_PLANNER_MODEL", os.environ.get("BA_MODEL", "granite3.3:2b"), ) -# Số file tối đa mỗi role trong vòng lặp per-file. +# Giới hạn số file tối đa mỗi coding agent được phép sinh ra trong một workflow. +# Tăng giá trị này nếu project phức tạp cần nhiều file hơn. _MAX_FILES_PER_ROLE: int = int(os.environ.get("MAX_FILES_PER_ROLE", "6")) -# ── Quy tắc chung được chèn vào system prompt của mọi agent ───────────────── +# Bộ quy tắc bắt buộc được chèn vào đầu system prompt của mọi agent. +# Đảm bảo hành vi nhất quán: không bịa thông tin, phân loại phát biểu, +# trích dẫn xuyên agent, ưu tiên chi tiết thay vì tóm tắt chung chung. COMMON_AGENT_RULES: str = """\ Critical rules — apply to every response: 1. Use ONLY the provided User Input, Previous Agent Outputs, Required Tech Stack, and RAG Knowledge Base Context. Do not invent information from outside these sources. @@ -87,6 +162,7 @@ 12. CROSS-AGENT CITATIONS: Whenever a decision, design choice, or data element traces back to a prior agent's output, cite it explicitly using the format "Agent §Section" (e.g., "per BA §3 FR-01", "per SA §3 /api/auth/login", "per TL §4 FE Task #3", "per Designer §5 Screen S-02"). Do NOT silently consume upstream information without citation. 13. INTRA-DOCUMENT LINKAGE: For every section in your output, add a brief note stating which other sections within this document it connects to, using "→ see §N" notation (e.g., "→ see §5 API Integration Map", "→ see §3 Component Breakdown"). This makes the dependency graph within your output explicit. 14. DEPTH OVER SUMMARY: Every section must contain complete, actionable, implementation-ready detail — not a summary or placeholder. Tables must have real data rows derived from the provided context. Bullet points must be specific (names, values, IDs), not generic descriptions. If you find yourself writing a generic statement like "handle errors appropriately", replace it with the exact error codes, HTTP statuses, and recovery actions required. +15. PLATFORM CONVENTIONS ARE BINDING: If the RAG Knowledge Base Context contains project-specific coding conventions, library names, naming rules, architectural patterns (e.g. three-seam pattern, scope=platform rows, Module Federation topology, tenantId-first index invariant), platform-provided shared modules (common-lib, guard decorators, base repositories), or Kafka topic names — treat ALL of these as HARD CONSTRAINTS that OVERRIDE generic best practices. Do not invent a new abstraction when the platform already provides one. Do not use a generic library when the platform mandates a specific one. Do not invent service names, Kafka topic names, route prefixes, port numbers, or decorator names — use only what the RAG context documents. If RAG documents a naming invariant (e.g. "tenantId is field #1 in every compound index"), apply it everywhere without exception. """ @@ -95,22 +171,26 @@ class SDLCState(TypedDict): """Trạng thái dùng chung được truyền qua các LangGraph node.""" - project: str | None # Bộ lọc RAG collection (tùy chọn) - user_input: str # Mục tiêu kinh doanh / yêu cầu gốc - rag_enabled: bool # Có truy vấn RAG mỗi bước không - rag_top_k: int # Số kết quả RAG mỗi truy vấn - rag_api_url: str # URL cơ sở của rag-api - ollama_base_url: str # URL cơ sở của Ollama + project: str | None # Tên project để lọc RAG collection (ví dụ: "yanlib"). Null = tìm tất cả. + user_input: str # Mục tiêu kinh doanh hoặc yêu cầu gốc từ người dùng — được truyền qua toàn pipeline. + rag_enabled: bool # True = mỗi agent gọi RAG API để lấy context từ knowledge base. + rag_top_k: int # Số chunk trả về mỗi lần gọi POST /ask của rag-api. + rag_api_url: str # URL của rag-api service (ví dụ: http://rag-api:8090). + ollama_base_url: str # URL của Ollama server (ví dụ: http://ollama:11434). - # operator.or_ → gộp dict: {**hiện_tại, **node_return} + # LangGraph reducer operator.or_: mỗi node trả về {role: output}, + # LangGraph gộp vào dict tổng bằng cách cập nhật (không ghi đè toàn bộ). step_outputs: Annotated[dict[str, str], operator.or_] - # operator.add → nối list: hiện_tại + node_return + # LangGraph reducer operator.add: mỗi node nối thêm [role] vào list. + # Kết quả cuối cùng là danh sách tất cả role đã chạy xong theo thứ tự. completed_steps: Annotated[list[str], operator.add] - tech_stack: list[str] | None # danh sách công nghệ bắt buộc (ngôn ngữ, framework, DB, infra) + tech_stack: list[str] | None # Danh sách công nghệ bắt buộc. Tất cả agent đều nhận danh sách + # này và phải bám sát — không đề xuất công nghệ thay thế. - error: str | None # Thông báo lỗi đầu tiên; None nếu không có lỗi + error: str | None # Thông báo lỗi đầu tiên gặp phải trong pipeline. None = không có lỗi. + # Workflow tiếp tục chạy dù có lỗi; lỗi chỉ ghi lại không dừng hẳn. # ── Hàm tiện ích ─────────────────────────────────────────────────────────────────── @@ -137,53 +217,70 @@ def _query_rag(rag_api_url: str, question: str, project: str | None, top_k: int) def _truncate(text: str, max_chars: int = MAX_PREV_OUTPUT_CHARS) -> str: - """Cắt ngắn text im lặng — không thêm marker để tránh LLM tái tạo marker đó.""" + """Cắt ngắn text về tối đa max_chars ký tự. + + Không thêm dấu "..." hay marker cắt để tránh LLM đọc marker đó + rồi tái tạo lại phần đã bị cắt trong output tiếp theo. + """ if len(text) <= max_chars: return text return text[:max_chars] -# Models that emit ... chain-of-thought blocks — stripped from output. -# Values are substrings matched against model names (e.g. "qwen3" matches "qwen3.6:35b"). +# Frozenset các chuỗi con khớp tên model reasoning. Khi tên model chứa +# một trong các chuỗi này, _strip_thinking() sẽ loại bỏ ... +# khỏi output trước khi lưu vào step_outputs. _REASONING_MODELS: frozenset[str] = frozenset({"phi4-mini-reasoning", "phi4-reasoning", "qwq", "deepseek-r1", "qwen3"}) def _strip_thinking(text: str) -> str: - """Remove ... blocks emitted by reasoning models.""" + """Loại bỏ tất cả block ... từ output của reasoning model. + + Một số model (qwen3, deepseek-r1, qwq...) phát ra chain-of-thought nội bộ + trong tag . Người dùng không cần thấy phần này — chỉ giữ lại + phần nội dung thực sự sau khi quá trình suy luận kết thúc. + """ text = re.sub(r".*?", "", text, flags=re.DOTALL | re.IGNORECASE) return text.strip() def _get_num_ctx(model: str) -> int: - """Trả về num_ctx phù hợp với model. + """Trả về context window (token) phù hợp với model cụ thể. - Một số model nhỏ được clamp ở 4 096 để giảm loop/hallucination - và timeout trong bối cảnh pipeline nhiều bước. + codegemma:2b bị clamp ở 4.096 token để tránh hallucination và vòng lặp + vô tận — model nhỏ không xử lý tốt context quá dài trong pipeline nhiều bước. + Các model khác dùng OLLAMA_NUM_CTX toàn cục (mặc định 32.768). """ if "codegemma:2b" in model.lower(): return min(4096, OLLAMA_NUM_CTX) return OLLAMA_NUM_CTX -# Giới hạn chars tối đa MỖI dep output theo role (để tránh overflow context window) +# Giới hạn ký tự tối đa lấy từ mỗi dep output theo role, để tránh overflow +# context window khi role có nhiều phụ thuộc. Tính toán ước lượng: +# tech_lead: 5 deps × 800 = 4.000 chars ≈ 1.000 tokens +# devsecops: 4 deps × 1000 = 4.000 chars ≈ 1.000 tokens +# tester: 5 deps × 3000 = 15.000 chars ≈ 3.700 tokens +# clarifier: 14 deps × 1500 = 21.000 chars ≈ 5.250 tokens _PER_DEP_CHARS: dict[str, int] = { - "tech_lead": 800, # 5 deps × 800 = 4 000 chars ≈ 1 000 tokens - "devsecops": 1_000, # 4 deps × 1 000 = 4 000 chars ≈ 1 000 tokens - "tester": 3_000, # 5 deps × 3 000 = 15 000 chars ≈ 3 700 tokens - "clarifier": 1_500, # 14 deps × 1 500 = 21 000 chars ≈ 5 250 tokens + "tech_lead": 800, + "devsecops": 1_000, + "tester": 3_000, + "clarifier": 1_500, } -# Roles sinh code/config theo từng file riêng biệt (loop-per-file approach). -# Không inject _FILE_OUTPUT_INSTRUCTION nữa vì một số model có thể lặp lại template verbatim. -# NOTE: "da" is intentionally absent — DA runs via _call_agent (produces a full analysis -# report in one shot). artifacts.py ARTIFACT_ROLES *does* include "da" so any code blocks -# in the DA output are still extracted to disk after the fact. +# Tập hợp các role sử dụng quy trình sinh code 2 pha (lập kế hoạch → sinh từng file). +# "da" được loại trừ khỏi đây vì DA agent chạy qua _call_agent (sinh báo cáo một lần), +# không phải vòng lặp per-file. Tuy nhiên artifacts.py vẫn trích xuất code block +# từ output của DA và lưu vào disk sau khi node hoàn thành. _ARTIFACT_ROLES: frozenset[str] = frozenset( {"fe", "mobile", "be", "dba", "devsecops", "tech_lead"} ) -# Template prompt cho bước lập kế hoạch file +# Template prompt gửi cho CODING_PLANNER_MODEL để lập kế hoạch danh sách file. +# Model nhận task, tech stack và context tóm tắt, rồi trả về JSON array +# mô tả các file cần sinh: [{filename, description, language}]. _PLAN_PROMPT_TMPL = """\ You are a {role_name}. Based on the task and tech stack, list the source code / config files you will create. Return ONLY a JSON array (no prose, no markdown wrapper): @@ -192,6 +289,7 @@ def _get_num_ctx(model: str) -> int: - Produce ONLY essential, non-trivial files. Minimum 2, maximum {max_files}. Do NOT pad to the limit — only list files that are truly needed. - Use relative paths from project root. - "language" must be a valid code fence name (typescript, python, yaml, sql, dockerfile, etc). +{tl_tasks_section} {extra} Task: {user_input} Tech stack: {tech_stack} @@ -199,7 +297,9 @@ def _get_num_ctx(model: str) -> int: {context_summary} """ -# Template prompt cho từng file +# Template prompt gửi cho coding model để sinh nội dung một file cụ thể. +# Mỗi file được sinh độc lập với context tập trung (filename + description) +# để tránh model bị phân tán bởi context quá lớn. _FILE_GEN_PROMPT_TMPL = """\ Write the complete {language} source code for: {filename} Purpose: {description} @@ -214,8 +314,226 @@ def _get_num_ctx(model: str) -> int: """ +# ── Trích xuất TL task board và tạo checklist hoàn thành cho engineer agents ──── + +# Ánh xạ từ role engineer sang các từ khóa tiêu đề section trong TL output. +# Dùng để định vị bảng task board tương ứng trong context khi parse. +_TL_ROLE_SECTIONS: dict[str, list[str]] = { + "fe": ["FE Task Board", "4. FE Task", "§4 FE", "4. FE"], + "mobile": ["Mobile Task Board", "5. Mobile Task", "§5 Mobile", "5. Mobile"], + "be": ["BE Task Board", "6. BE Task", "§6 BE", "6. BE"], + "dba": ["DBA Task Board", "7. DBA Task", "§7 DBA", "7. DBA"], +} + + +def _extract_tl_tasks(role: str, context: str) -> list[dict]: + """Phân tích bảng TL task board cho *role* từ chuỗi context tổng hợp. + + Tìm section TL tương ứng với role (dựa trên _TL_ROLE_SECTIONS), + sau đó quét từng dòng của bảng Markdown để lấy thông tin task. + + Trả về: + list[dict]: Danh sách task, mỗi phần tử có {id, task, priority}. + Trả về [] nếu không tìm thấy section hoặc bảng không thể parse. + """ + patterns = _TL_ROLE_SECTIONS.get(role) + if not patterns: + return [] + + section_start = -1 + for pat in patterns: + idx = context.find(pat) + if idx != -1 and (section_start == -1 or idx < section_start): + section_start = idx + if section_start == -1: + return [] + + section_text = context[section_start:] + # Cắt section text tại heading số tiếp theo (ví dụ: "5. Mobile", "8. Sprint") + # để tránh đọc nhầm task của section khác. + stop_match = re.search(r"\n(?:[5-9]|[1-9][0-9])\.\s+[A-Z]", section_text[50:]) + if stop_match: + section_text = section_text[: 50 + stop_match.start()] + + tasks: list[dict] = [] + for line in section_text.split("\n"): + if "|" not in line: + continue + cells = [c.strip() for c in line.split("|")] + cells = [c for c in cells if c] + if len(cells) < 2: + continue + # Bỏ qua dòng kẻ phân cách (---) và dòng tiêu đề bảng Markdown. + if all(set(c.replace("-", "").replace(":", "").replace(" ", "")) <= {"-", ":", "|"} for c in cells): + continue + first = cells[0] + if first.lower() in ("#", "task", "id", "no") or first.startswith("-"): + continue + # Xác định cột nào chứa tên task: nếu cột đầu là số thứ tự thì tên task ở cột 2. + task_idx = 1 if (first.lstrip("#").strip().isdigit() or first.strip() == "#") else 0 + if task_idx >= len(cells): + continue + task_text = cells[task_idx].strip() + if not task_text or len(task_text) < 4 or task_text.lower() in ("task", "description"): + continue + entry: dict = { + "id": cells[0].lstrip("#").strip() if task_idx == 1 else str(len(tasks) + 1), + "task": task_text, + "priority": "—", + } + # Lấy mức độ ưu tiên: quét các cột sau tên task để tìm P0/P1/P2/HIGH/MED/LOW. + for offset in (3, 4, 5): + if task_idx + offset < len(cells): + val = cells[task_idx + offset].upper() + if val in ("P0", "P1", "P2", "HIGH", "MED", "LOW", "H", "M", "L"): + entry["priority"] = val + break + tasks.append(entry) + return tasks + + +def _find_uncovered_tasks(tl_tasks: list[dict], file_infos: list[dict]) -> list[dict]: + """Trả về danh sách TL task chưa được cover bởi bất kỳ file nào trong *file_infos*. + + Heuristic xác định coverage: một task được coi là "covered" khi ít nhất + max(1, len(từ_khóa) // 2) từ khóa dài >= 4 ký tự của task xuất hiện + trong chuỗi ghép (filename + description) của ít nhất một file đã lên kế hoạch. + + Các task không được cover sẽ được đưa vào follow-up pass để sinh thêm file. + """ + uncovered: list[dict] = [] + for task in tl_tasks: + task_text = task.get("task", "").lower() + keywords = [w for w in re.split(r"\W+", task_text) if len(w) >= 4] + if not keywords: + continue + threshold = max(1, len(keywords) // 2) + matched = any( + sum( + 1 for kw in keywords + if kw in (f.get("filename", "") + " " + f.get("description", "")).lower() + ) >= threshold + for f in file_infos + ) + if not matched: + uncovered.append(task) + return uncovered + + +def _build_task_checklist(tl_tasks: list[dict], file_infos: list[dict]) -> str: + """Xây dựng bảng Markdown Task Completion Checklist để thêm vào cuối output engineer. + + Mỗi dòng đối chiếu một TL task với kết quả sinh code: + - ✅ Done: task được cover bởi ít nhất một file đã sinh. + - ⏳ Addressed in output: task chưa có file khớp nhưng đã được đề cập. + + Trả về chuỗi rỗng nếu tl_tasks trống (không có TL task board). + """ + if not tl_tasks: + return "" + lines: list[str] = [ + "\n---", + "## ✅ Task Completion Checklist (TL Task Board → Implementation)", + "", + "| # | Task | Priority | Status | File(s) / Section |", + "|---|------|----------|--------|-------------------|", + ] + for task in tl_tasks: + task_text = task.get("task", "") + priority = task.get("priority", "—") + tid = task.get("id", "—") + task_kws = [w for w in re.split(r"\W+", task_text.lower()) if len(w) >= 4] + threshold = max(1, len(task_kws) // 2) if task_kws else 1 + matched_files = [ + f.get("filename", "") + for f in file_infos + if task_kws and sum( + 1 for kw in task_kws + if kw in (f.get("filename", "") + " " + f.get("description", "")).lower() + ) >= threshold + ] + if matched_files: + status = "✅ Done" + files_label = ", ".join(f"`{fn}`" for fn in matched_files[:2]) + else: + status = "⏳ Addressed in output" + files_label = "—" + task_display = (task_text[:60] + "…") if len(task_text) > 60 else task_text + lines.append(f"| {tid} | {task_display} | {priority} | {status} | {files_label} |") + return "\n".join(lines) + + +# Tập hợp role được phép re-generate trong Clarifier Regen Loop. +# Loại trừ planning roles (ba, pm, sa, ta, designer, tl, tester) để tránh +# thay đổi foundation phân tích nghiệp vụ, và loại clarifier để ngăn vòng lặp vô tận. +_REGEN_ELIGIBLE_ROLES: frozenset[str] = frozenset( + {"fe", "mobile", "dba", "be", "da", "tech_lead", "devsecops"} +) + + +def _parse_clarifier_regen_list(clarifier_output: str) -> list[str]: + """Parse §10 Recommended Re-generation List từ Clarifier output. + + Tìm bảng trong §10, quét cột "Agent Role" để lấy tên role hợp lệ. + Chỉ trả về các role thuộc _REGEN_ELIGIBLE_ROLES (loại trừ planning roles + và clarifier để ngăn infinite loop). + Kết quả được sắp xếp theo thứ tự WORKFLOW_STEPS. + Trả về list rỗng nếu §10 không có trong output hoặc không có gì để re-gen. + """ + # Tìm vị trí bắt đầu của §10 trong output Clarifier bằng nhiều pattern khác nhau + # để xử lý cả trường hợp model viết "10.", "§10", "## 10. Recommended Re-generation". + section_match = re.search( + r"(?:10\.|§\s*10\b|#+\s*10\.?\s+Recommended\s+Re.?generation|" + r"Recommended\s+Re.?generation\s+List)", + clarifier_output, + re.IGNORECASE, + ) + if not section_match: + return [] + + section_text = clarifier_output[section_match.start():] + # Cắt nội dung tại section tiếp theo (11. hoặc cao hơn) để tránh + # đọc nhầm tên role từ các section khác trong Clarifier output. + stop = re.search(r"\n(?:1[1-9]|[2-9][0-9])\.\s+[A-Z]", section_text[30:]) + if stop: + section_text = section_text[:30 + stop.start()] + + seen: set[str] = set() + roles_ordered: list[str] = [] + + for line in section_text.split("\n"): + if "|" not in line: + continue + cells = [c.strip().lower() for c in line.split("|") if c.strip()] + if not cells: + continue + # Bỏ qua dòng kẻ phân cách Markdown (---) và dòng tiêu đề bảng. + if all(set(c.replace("-", "").replace(":", "").replace(" ", "")) <= {"-", ":", "|"} for c in cells): + continue + # Quét từng cell để tìm tên role hợp lệ bằng word-boundary regex, + # tránh khớp nhầm tên role con trong tên role khác (ví dụ: "be" trong "devsecops"). + for cell in cells: + for role in _REGEN_ELIGIBLE_ROLES: + if role not in seen and re.search(rf"\b{re.escape(role)}\b", cell): + seen.add(role) + break + + # Trả về theo thứ tự WORKFLOW_STEPS để đảm bảo dependency được tôn trọng: + # agent phụ thuộc vào agent khác sẽ được re-gen sau agent đó. + return [r for r in WORKFLOW_STEPS if r in seen] + + def _detect_db_type(tech_stack: list[str] | None) -> str: - """Detect DB paradigm. Returns 'nosql', 'sql', 'both', or 'unknown'.""" + """Nhận diện loại cơ sở dữ liệu từ danh sách tech_stack. + + Trả về: + 'nosql' — chỉ có NoSQL (MongoDB, DynamoDB, Cassandra, Redis...) + 'sql' — chỉ có SQL (PostgreSQL, MySQL, SQL Server, SQLite...) + 'both' — có cả SQL và NoSQL + 'unknown' — không nhận diện được hoặc tech_stack trống + + Kết quả được dùng để chọn đúng extra_instruction cho DBA và DA agent. + """ if not tech_stack: return "unknown" _nosql_kw = {"mongodb", "mongo", "nosql", "dynamodb", "cassandra", @@ -234,18 +552,38 @@ def _detect_db_type(tech_stack: list[str] | None) -> str: return "unknown" -def _strip_code_fence(content: str, language: str) -> str: - """Remove outer code-fence markers so nested fences don't break markdown/regex.""" - # Try to extract content between ```lang ... ``` - patterns = [ +@functools.lru_cache(maxsize=32) +def _fence_pattern(language: str) -> re.Pattern: + """Compile và cache pattern regex để extract content trong code fence ngôn ngữ chỉ định. + + Dùng @lru_cache để tránh compile lại regex cho cùng ngôn ngữ nhiều lần. + Pattern khớp cả dạng: ```typescript ... ``` và ```TypeScript ... ```. + """ + return re.compile( rf"```(?:{re.escape(language)}|{re.escape(language.lower())})\s*\n(.*?)(?:\n```\s*$|\n```\s*\Z)", - r"```\w*\s*\n(.*?)(?:\n```\s*$|\n```\s*\Z)", - ] - for pat in patterns: - m = re.search(pat, content, re.DOTALL | re.IGNORECASE | re.MULTILINE) + re.DOTALL | re.IGNORECASE | re.MULTILINE, + ) + + +_FENCE_FALLBACK_RE = re.compile( + r"```\w*\s*\n(.*?)(?:\n```\s*$|\n```\s*\Z)", + re.DOTALL | re.IGNORECASE | re.MULTILINE, +) + + +def _strip_code_fence(content: str, language: str) -> str: + """Loại bỏ code fence bao ngoài (```lang...```) từ content LLM trả về. + + Cần thiết vì một số model bọc toàn bộ output trong code fence, trong khi + output đã chứa các fence nội bộ riêng. Loại bỏ fence ngoài để tránh + markdown bị lồng nhau không hợp lệ khi lưu artifact. + """ + # Thử extract content giữa ```lang ... ``` bằng pattern đã cache cho ngôn ngữ này. + for pat in (_fence_pattern(language), _FENCE_FALLBACK_RE): + m = pat.search(content) if m: return m.group(1).rstrip() - # Fallback: strip leading/trailing fence lines + # Fallback thủ công: xóa dòng đầu (```lang) và dòng cuối (```) nếu regex không khớp. lines = content.strip().splitlines() if lines and lines[0].strip().startswith("```"): lines = lines[1:] @@ -255,7 +593,12 @@ def _strip_code_fence(content: str, language: str) -> str: def _deloop(text: str, max_repeats: int = 8) -> str: - """Truncate infinitely-repeated identical lines to mitigate model loop bugs.""" + """Cắt ngắn các đoạn dòng lặp lại liên tiếp để giảm thiểu lỗi vòng lặp của model. + + Một số coding model nhỏ có xu hướng lặp lại cùng một dòng code (thường là + import hoặc boilerplate) hàng trăm lần. Hàm này giới hạn tối đa max_repeats + lần lặp liên tiếp rồi thêm dấu hiệu truncation. + """ lines = text.split("\n") out: list[str] = [] prev: str | None = None @@ -276,7 +619,12 @@ def _deloop(text: str, max_repeats: int = 8) -> str: def _compute_extra_instruction(role: str, tech_stack: list[str] | None) -> str: - """Role-specific instructions for artifact generation.""" + """Sinh câu lệnh bổ sung (extra instruction) đặc thù theo role và tech stack. + + Câu lệnh này được chèn vào planning prompt và file-gen prompt để hướng dẫn + coding model dùng đúng framework, ngôn ngữ và pattern phù hợp với tech stack. + Ví dụ: DBA + NoSQL → dùng Mongoose, không dùng SQL DDL. + """ if not tech_stack: if role == "tech_lead": return ( @@ -313,7 +661,7 @@ def _compute_extra_instruction(role: str, tech_stack: list[str] | None) -> str: return "Write real Angular component with TypeScript, decorators, and template. No license headers." if "svelte" in combined: return "Write real Svelte component with TypeScript and reactive syntax. No license headers." - # Default: React / Next.js + # Mặc định nếu không khớp Vue/Angular/Svelte: dùng React/TypeScript (phổ biến nhất). return ( "Write real React/TypeScript component with JSX markup, hooks, and props interface. " "No license headers." @@ -368,8 +716,10 @@ def _compute_extra_instruction(role: str, tech_stack: list[str] | None) -> str: if role == "devsecops": return ( - "Write real Dockerfile, docker-compose.yml, or CI/CD YAML with working config. " - "No license headers." + "Write real Kubernetes manifests (Deployment, Service, Ingress, ConfigMap, Secret), " + "Dockerfile, and CI/CD pipeline YAML with working config. " + "All sensitive env vars must go in K8s Secret objects referenced via secretKeyRef — " + "never in plain env: blocks. No license headers." ) if role == "tech_lead": return ( @@ -387,8 +737,14 @@ def _plan_code_files( tech_stack: list[str] | None, context_summary: str, extra_instruction: str = "", + tl_tasks: list[dict] | None = None, ) -> list[dict]: - """Dùng CODING_PLANNER_MODEL để lấy danh sách file cần tạo.""" + """Gọi CODING_PLANNER_MODEL để lập kế hoạch danh sách file cần sinh. + + Nhận task, tech stack, context tóm tắt và danh sách TL task (nếu có), + rồi trả về JSON array mô tả các file: [{filename, description, language}]. + Nếu parse JSON thất bại hoặc model không trả về kết quả, trả về []. + """ planner = ChatOllama( model=CODING_PLANNER_MODEL, base_url=ollama_base_url, @@ -397,12 +753,23 @@ def _plan_code_files( request_timeout=float(OLLAMA_REQUEST_TIMEOUT), ) stack_str = ", ".join(tech_stack) if tech_stack else "not specified" + # Nếu có TL task board, chèn danh sách task vào planning prompt + # để model biết cần tạo file nào để cover hết task (tối đa 15 task đầu tiên). + if tl_tasks: + task_lines = "\n".join(f" - {t['task']}" for t in tl_tasks[:15]) + tl_tasks_section = ( + "IMPORTANT — Team Lead Task Board: your file list MUST cover all of these tasks:\n" + + task_lines + ) + else: + tl_tasks_section = "" prompt = _PLAN_PROMPT_TMPL.format( role_name=agent.name, max_files=_MAX_FILES_PER_ROLE, user_input=user_input[:500], tech_stack=stack_str, context_summary=context_summary[:1000], + tl_tasks_section=tl_tasks_section, extra=f"Constraint: {extra_instruction}" if extra_instruction else "", ) try: @@ -430,7 +797,15 @@ def _generate_one_file( extra_instruction: str = "", context_snippet: str = "", ) -> str: - """Tạo nội dung một file bằng coding model, strip code fences, remove loops.""" + """Sinh nội dung đầy đủ của một file bằng coding model. + + Gọi LLM với prompt tập trung vào filename + description + tech stack. + Sau khi nhận response, áp dụng _strip_code_fence() và _deloop() + để làm sạch output trước khi lưu. + + Trả về chuỗi nội dung file (không có code fence bao ngoài). + Nếu LLM call thất bại, trả về comment lỗi để không làm crash workflow. + """ llm = ChatOllama( model=agent.model, base_url=ollama_base_url, @@ -476,20 +851,42 @@ def _generate_artifacts_multi_turn( tech_stack: list[str] | None, extra_instruction: str = "", ) -> str: - """ - Sinh code/artifact theo 2 pha: - 1. Lập kế hoạch — lấy danh sách file dùng CODING_PLANNER_MODEL (granite3.3:2b). - 2. Loop — tạo từng file riêng bằng coding model với context nhỏ và tập trung. - Trả về markdown tổng hợp với ### FILE: sections. + """Sinh toàn bộ artifact cho một coding role theo quy trình 2 pha. + + Pha 1 — Lập kế hoạch: + CODING_PLANNER_MODEL nhận task, tech stack, context tóm tắt và TL task board, + trả về JSON array danh sách file cần sinh. + + Pha 2 — Sinh từng file: + Coding model sinh nội dung từng file độc lập với context tập trung. + Nếu còn TL task chưa được cover sau pha 2 → chạy follow-up pass để lên + kế hoạch thêm file mới và sinh chúng (bỏ qua file trùng tên). + + Kết thúc: + Append bảng Task Completion Checklist đối chiếu mọi TL task với kết quả. + + Trả về: + Chuỗi markdown tổng hợp với ### FILE: sections và checklist cuối. """ context_summary = context[-1200:] + + # Trích xuất danh sách TL task từ context để tracking coverage và lập kế hoạch. + tl_tasks = _extract_tl_tasks(role, context) + if tl_tasks: + logger.info("Role=%s: found %d TL tasks to cover", role, len(tl_tasks)) + + # Pha 1: Gọi CODING_PLANNER_MODEL để lập kế hoạch danh sách file. files = _plan_code_files( - role, agent, ollama_base_url, user_input, tech_stack, context_summary, extra_instruction + role, agent, ollama_base_url, user_input, tech_stack, + context_summary, extra_instruction, tl_tasks=tl_tasks or None, ) if not files: logger.warning("No files planned for %s, fallback to single call", role) - return _call_agent(agent, ollama_base_url, context[-2000:]) + output = _call_agent(agent, ollama_base_url, context[-2000:]) + if tl_tasks: + output += _build_task_checklist(tl_tasks, []) + return output logger.info( "Planned %d files for %s: %s", @@ -500,6 +897,10 @@ def _generate_artifacts_multi_turn( # Review roles benefit from seeing previous agent outputs _review_roles = {"tech_lead", "devsecops"} ctx_snippet = context[-1500:] if role in _review_roles else "" + + # Theo dõi tất cả file trên cả hai pha để dedup và tạo checklist chính xác. + all_files: list[dict] = list(files) + for file_info in files: filename = file_info.get("filename", "unknown") description = file_info.get("description", "") @@ -511,6 +912,46 @@ def _generate_artifacts_multi_turn( ) parts.append(f"\n### FILE: {filename}\n```{language}\n{file_content}\n```\n") + # Pha 2 (follow-up): nếu còn TL task chưa được cover sau pha 1, + # lên kế hoạch thêm file mới và sinh chúng để đảm bảo coverage đầy đủ. + if tl_tasks: + uncovered = _find_uncovered_tasks(tl_tasks, all_files) + if uncovered: + logger.info( + "Role=%s: %d TL tasks uncovered after initial pass — running follow-up loop", + role, len(uncovered), + ) + followup_input = ( + user_input + + "\n\nFOCUS: The following Team Lead tasks were not yet addressed — " + "implement them now:\n" + + "\n".join(f"- {t['task']}" for t in uncovered) + ) + followup_files = _plan_code_files( + role, agent, ollama_base_url, followup_input, tech_stack, + context_summary, extra_instruction, tl_tasks=uncovered, + ) + for file_info in followup_files: + fname = file_info.get("filename", "") + # Bỏ qua file đã sinh trong pha 1 để tránh ghi đè. + if any(f.get("filename") == fname for f in all_files): + logger.info("Follow-up skip duplicate file %s for role=%s", fname, role) + continue + description = file_info.get("description", "") + language = file_info.get("language", "text") + logger.info("Follow-up: generating file %s for role=%s", fname, role) + file_content = _generate_one_file( + fname, description, language, agent, ollama_base_url, tech_stack, + extra_instruction, ctx_snippet, + ) + parts.append(f"\n### FILE: {fname}\n```{language}\n{file_content}\n```\n") + all_files.append(file_info) + + # Thêm bảng Task Completion Checklist vào cuối output để đối chiếu + # mọi TL task với file đã sinh — ✅ Done hoặc ⏳ Addressed in output. + if tl_tasks: + parts.append(_build_task_checklist(tl_tasks, all_files)) + return "\n".join(parts) @@ -519,11 +960,13 @@ def _call_agent( ollama_base_url: str, context: str, ) -> str: - """Gọi LLM với system prompt của agent + context đã tổng hợp. + """Gọi LLM một lần với system prompt đầy đủ của agent + context đã tổng hợp. + + Dùng cho tất cả non-artifact roles (ba, pm, sa, ta, designer, tl, da, + tester, clarifier) — những role sinh báo cáo một lần thay vì per-file loop. - Nếu model trả về output rỗng hoặc quá ngắn (< 30 ký tự), thực hiện - retry với context rút gọn (1 500 ký tự cuối) để xử lý các model nhỏ - có context window hạn chế. + Nếu model trả về output < 30 ký tự (thường do model nhỏ bị overflow context), + thực hiện retry với 1.500 ký tự cuối của context để giảm tải. """ llm = ChatOllama( model=agent.model, @@ -532,14 +975,12 @@ def _call_agent( num_ctx=_get_num_ctx(agent.model), request_timeout=float(OLLAMA_REQUEST_TIMEOUT), ) - # Compute once — reused in both the primary call and the retry branch. + # Kiểm tra một lần — dùng lại cho cả lần gọi chính và lần retry. is_reasoning = any(m in agent.model.lower() for m in _REASONING_MODELS) - messages = [ - SystemMessage(content=COMMON_AGENT_RULES + "\n\n---\n\n" + agent.system_prompt), - HumanMessage(content=context), - ] - response = llm.invoke(messages) + # Build system message once — reused in primary call and retry to avoid duplication. + sys_msg = SystemMessage(content=COMMON_AGENT_RULES + "\n\n---\n\n" + agent.system_prompt) + response = llm.invoke([sys_msg, HumanMessage(content=context)]) result = str(response.content) # Strip chain-of-thought blocks from reasoning models. @@ -552,10 +993,7 @@ def _call_agent( agent.name, agent.model, len(result.strip()), ) trimmed = context[-1500:] if len(context) > 1500 else context - response = llm.invoke([ - SystemMessage(content=COMMON_AGENT_RULES + "\n\n---\n\n" + agent.system_prompt), - HumanMessage(content=trimmed), - ]) + response = llm.invoke([sys_msg, HumanMessage(content=trimmed)]) result = str(response.content) if is_reasoning: result = _strip_thinking(result) @@ -563,16 +1001,122 @@ def _call_agent( return result +# ── Context builder — shared by _build_node and run_single_step ────────────────── + + +def _build_context_parts( + role: str, + agent: AgentConfig, + user_input: str, + step_outputs: dict[str, str], + tech_stack: list[str] | None, + dep_max_chars: int, + rag_enabled: bool = False, + rag_api_url: str = "", + project: str | None = None, + rag_top_k: int = RAG_TOP_K, + extra_context: str | None = None, +) -> list[str]: + """Lắp ráp danh sách context-parts cho bất kỳ lần gọi agent nào. + + Đây là hàm trung tâm tập trung logic build context, dùng chung cho cả + _build_node (workflow chính) và run_single_step (single-step endpoint). + Đảm bảo hai luồng code luôn tạo context nhất quán — bao gồm: + - Gợi ý ngày hiện tại cho planning roles (ba, pm, sa, ta) tránh dùng ngày quá khứ. + - Gợi ý kiểu DB (nosql/sql/both) cho dba/da để chọn đúng dialect schema. + - Context bổ sung tùy chọn từ caller. + """ + parts: list[str] = [ + f"## Mục tiêu kinh doanh / Yêu cầu đầu vào\n{user_input}" + ] + + # Chèn ngày hiện tại cho planning roles để tránh sinh timeline có ngày quá khứ. + # Chỉ áp dụng cho ba, pm, sa, ta — các role sinh timeline và sprint plan. + if role in {"pm", "ba", "sa", "ta"}: + now = datetime.now() + parts.append( + f"\n## Ngày hiện tại\n" + f"{now.strftime('%d/%m/%Y')} (tháng {now.month} năm {now.year}). " + f"Mọi timeline, sprint, milestone PHẢI bắt đầu từ ngày này trở về sau. " + f"Không dùng bất kỳ ngày nào trong quá khứ." + ) + + if tech_stack: + stack_lines = "\n".join(f"- {item}" for item in tech_stack) + parts.append( + f"\n## Công nghệ bắt buộc\n" + f"Các công nghệ sau BẮT BUỘC phải sử dụng. Không đề xuất lựa chọn khác " + f"trừ khi được yêu cầu rõ ràng.\n{stack_lines}" + ) + + for dep in agent.depends_on: + if dep in step_outputs: + dep_name = AGENTS[dep].name + parts.append( + f"\n## Kết quả {dep_name}\n{_truncate(step_outputs[dep], dep_max_chars)}" + ) + + if extra_context: + parts.append(f"\n## Context bổ sung\n{extra_context}") + + if rag_enabled and rag_api_url: + _hint = agent.rag_query_hint or agent.name + rag_text = _query_rag(rag_api_url, f"{_hint}: {user_input}", project, rag_top_k) + if rag_text: + parts.append(f"\n## Context từ Knowledge Base\n{_truncate(rag_text)}") + + # DB-type hints steer DBA/DA to the correct schema dialect. + if role in {"dba", "da"} and tech_stack: + db_type = _detect_db_type(tech_stack) + if role == "dba": + if db_type == "nosql": + parts.append( + "\n## DB Schema: NoSQL Only\n" + "Tech stack chỉ dùng NoSQL (MongoDB). Tạo Mongoose schema files (.ts). " + "KHÔNG tạo SQL DDL hay CREATE TABLE. " + "Dùng embedded documents và arrays thay cho JOINs/FK." + ) + elif db_type == "sql": + parts.append( + "\n## DB Schema: SQL Relational\n" + "Tech stack dùng relational DB. Tạo SQL DDL với CREATE TABLE, " + "FOREIGN KEY relationships, và CREATE INDEX." + ) + elif db_type == "both": + parts.append( + "\n## DB Schema: Mixed (SQL + NoSQL)\n" + "Tech stack dùng cả SQL và NoSQL. Tạo SQL DDL schema VÀ " + "Mongoose models riêng cho từng data store." + ) + elif role == "da": + if db_type == "nosql": + parts.append( + "\n## Data Analysis: NoSQL Only\n" + "Tech stack chỉ dùng MongoDB NoSQL. Sử dụng aggregation pipeline " + "($match, $group, $project, $lookup). " + "Nếu cần cross-collection analysis: BE export CSV trước, DA đọc CSV bằng Python/pandas. " + "KHÔNG dùng SQL SELECT/FROM/WHERE." + ) + elif db_type == "sql": + parts.append( + "\n## Data Analysis: SQL\n" + "Tech stack dùng relational DB. Phân tích bằng SQL queries với " + "GROUP BY, window functions, aggregates, CTEs." + ) + + return parts + + # ── Factory tạo node ─────────────────────────────────────────────────────────────── def _build_node(role: str): - """ - Trả về hàm LangGraph node cho vai trò SDLC đã cho. + """Factory function tạo LangGraph node cho một role SDLC. + + Sử dụng closure để mỗi node nắm giữ AgentConfig riêng của nó mà không cần + tra cứu global dict lúc chạy — an toàn hơn và nhanh hơn. - Hàm trả về dùng closure để nắm giữ *agent*, mỗi node có config riêng - mà không cần tra cứu global lúc chạy. - __name__ được đặt tường minh để LangGraph hiển thị tên vai trò - thay vì tên hàm chung 'node_fn'. + __name__ được set tường minh thành "{role}_node" để LangGraph hiển thị + đúng tên role trong trace/debug thay vì tên hàm chung "node_fn". """ agent = AGENTS[role] @@ -582,95 +1126,25 @@ def node_fn(state: SDLCState) -> dict: step_outputs: dict[str, str] = state.get("step_outputs", {}) tech_stack = state.get("tech_stack") - # Giới hạn chars mỗi dep cho các role có nhiều dependencies + # Lấy giới hạn ký tự per-dep cho role này; dùng MAX_PREV_OUTPUT_CHARS nếu không có override. dep_max_chars = _PER_DEP_CHARS.get(role, MAX_PREV_OUTPUT_CHARS) # 1. Tổng hợp context - context_parts: list[str] = [ - f"## Mục tiêu kinh doanh / Yêu cầu đầu vào\n{state['user_input']}" - ] - - # Ngày hiện tại — quan trọng cho PM/BA để tạo timeline không bị lùi về quá khứ - if role in {"pm", "ba", "sa", "ta"}: - now = datetime.now() - context_parts.append( - f"\n## Ngày hiện tại\n" - f"{now.strftime('%d/%m/%Y')} (tháng {now.month} năm {now.year}). " - f"Mọi timeline, sprint, milestone PHẢI bắt đầu từ ngày này trở về sau. " - f"Không dùng bất kỳ ngày nào trong quá khứ." - ) - - # Chèn tech stack ngay sau user input nếu có - if tech_stack: - stack_lines = "\n".join(f"- {item}" for item in tech_stack) - context_parts.append( - f"\n## Công nghệ bắt buộc\n" - f"Các công nghệ sau BẮT BUỘC phải sử dụng. Không đề xuất lựa chọn khác " - f"trừ khi được yêu cầu rõ ràng.\n{stack_lines}" - ) - - for dep in agent.depends_on: - if dep in step_outputs: - dep_name = AGENTS[dep].name - context_parts.append( - f"\n## Kết quả {dep_name}\n{_truncate(step_outputs[dep], dep_max_chars)}" - ) - - # 2. Bổ sung RAG (tùy chọn) - if state.get("rag_enabled") and state.get("rag_api_url"): - _hint = agent.rag_query_hint or agent.name - rag_question = f"{_hint}: {state['user_input']}" - rag_text = _query_rag( - state["rag_api_url"], - rag_question, - state.get("project"), - state.get("rag_top_k", RAG_TOP_K), - ) - if rag_text: - context_parts.append( - f"\n## Context từ Knowledge Base\n{_truncate(rag_text)}" - ) + context_parts = _build_context_parts( + role=role, + agent=agent, + user_input=state["user_input"], + step_outputs=step_outputs, + tech_stack=tech_stack, + dep_max_chars=dep_max_chars, + rag_enabled=state.get("rag_enabled", False), + rag_api_url=state.get("rag_api_url", ""), + project=state.get("project"), + rag_top_k=state.get("rag_top_k", RAG_TOP_K), + ) - # DB-type awareness cho DBA và DA roles - if role in {"dba", "da"} and tech_stack: - db_type = _detect_db_type(tech_stack) - if role == "dba": - if db_type == "nosql": - context_parts.append( - "\n## DB Schema: NoSQL Only\n" - "Tech stack chỉ dùng NoSQL (MongoDB). Tạo Mongoose schema files (.ts). " - "KHÔNG tạo SQL DDL hay CREATE TABLE. " - "Dùng embedded documents và arrays thay cho JOINs/FK." - ) - elif db_type == "sql": - context_parts.append( - "\n## DB Schema: SQL Relational\n" - "Tech stack dùng relational DB. Tạo SQL DDL với CREATE TABLE, " - "FOREIGN KEY relationships, và CREATE INDEX." - ) - elif db_type == "both": - context_parts.append( - "\n## DB Schema: Mixed (SQL + NoSQL)\n" - "Tech stack dùng cả SQL và NoSQL. Tạo SQL DDL schema VÀ " - "Mongoose models riêng cho từng data store." - ) - elif role == "da": - if db_type == "nosql": - context_parts.append( - "\n## Data Analysis: NoSQL Only\n" - "Tech stack chỉ dùng MongoDB NoSQL. Sử dụng aggregation pipeline " - "($match, $group, $project, $lookup). " - "Nếu cần cross-collection analysis: BE export CSV trước, DA đọc CSV bằng Python/pandas. " - "KHÔNG dùng SQL SELECT/FROM/WHERE." - ) - elif db_type == "sql": - context_parts.append( - "\n## Data Analysis: SQL\n" - "Tech stack dùng relational DB. Phân tích bằng SQL queries với " - "GROUP BY, window functions, aggregates, CTEs." - ) - - # Tính extra_instruction cho _generate_artifacts_multi_turn + # Tính extra instruction đặc thù theo role và tech stack, + # chèn vào planning prompt và file-gen prompt. extra_instruction = _compute_extra_instruction(role, tech_stack) context = "\n".join(context_parts) @@ -680,7 +1154,7 @@ def node_fn(state: SDLCState) -> dict: error_msg: str | None = None try: if role in _ARTIFACT_ROLES: - # Sinh code theo từng file riêng biệt (planner + loop) + # Artifact roles: dùng quy trình 2 pha (planner + per-file loop). output = _generate_artifacts_multi_turn( role, agent, ollama_url, context, state["user_input"], tech_stack, extra_instruction, @@ -710,12 +1184,13 @@ def node_fn(state: SDLCState) -> dict: # ── Xây dựng đồ thị ─────────────────────────────────────────────────────────────── def build_workflow(): - """ - Biên dịch SDLC StateGraph. + """Biên dịch và trả về SDLC LangGraph StateGraph. - Node được thêm theo thứ tự WORKFLOW_STEPS; cạnh được thêm tuần tự - để đồ thị phản ánh luồng SDLC theo mô hình Waterfall/Agile truyền thống. - Trả về LangGraph runnable đã biên dịch. + Thêm node theo thứ tự WORKFLOW_STEPS, thêm cạnh tuần tự giữa các node + liên tiếp để đảm bảo luồng SDLC từ BA đến Clarifier luôn chạy đúng thứ tự. + + Trả về: + CompiledGraph: LangGraph runnable đã compile, sẵn sàng để stream() hoặc invoke(). """ graph: StateGraph = StateGraph(SDLCState) @@ -732,9 +1207,10 @@ def build_workflow(): return graph.compile() -# ── Singleton ───────────────────────────────────────────────────────────────── -# Double-checked locking đảm bảo đồ thị chỉ được biên dịch đúng một lần -# dù nhiều request đến cùng lúc khi khởi động. +# ── Singleton workflow — chỉ compile một lần, cache module-level ──────────────── +# Double-checked locking: kiểm tra _workflow trước lock (fast path), kiểm tra lại +# sau lock (safe path). Đảm bảo StateGraph chỉ được compile đúng một lần dù +# nhiều HTTP request đến đồng thời lúc container vừa khởi động. _workflow = None _workflow_lock = threading.Lock() @@ -763,14 +1239,19 @@ def run_single_step( ollama_base_url: str = OLLAMA_BASE_URL, rag_api_url: str = RAG_API_URL, ) -> str: - """ - Chạy một bước agent đơn lẻ mà không cần chạy toàn bộ workflow. + """Chạy một agent đơn lẻ mà không cần khởi động toàn bộ workflow. - Xây dựng cùng context mà workflow node sẽ thấy - (output phụ thuộc + extra_context tùy chọn + RAG tùy chọn), - sau đó gọi LLM trực tiếp. Hữu ích cho: - - Test từng agent riêng lẻ qua POST /agent/{role}. - - Chạy từng bước thủ công khi người dùng dán output trước vào *prev_outputs*. + Xây dựng context giống hệt như workflow node sẽ thấy: + - Output rút gọn của các agent phụ thuộc (từ prev_outputs) + - extra_context tùy chọn + - RAG context tùy chọn (nếu rag_enabled=True) + + Được dùng bởi: + - POST /agent/{role}: test một agent đơn lẻ qua API + - Clarifier Regen Loop: re-run agent cụ thể với outputs đã cập nhật + + Trả về: + str: Output markdown của agent. """ if role not in AGENTS: raise ValueError( @@ -779,37 +1260,21 @@ def run_single_step( agent = AGENTS[role] step_outputs: dict[str, str] = prev_outputs or {} - context_parts: list[str] = [f"## Mục tiêu kinh doanh / Yêu cầu đầu vào\n{user_input}"] - - if tech_stack: - stack_lines = "\n".join(f"- {item}" for item in tech_stack) - context_parts.append( - f"\n## Công nghệ bắt buộc\n" - f"Các công nghệ sau BẮT BUỘC phải sử dụng. Không đề xuất lựa chọn khác " - f"trừ khi được yêu cầu rõ ràng.\n{stack_lines}" - ) - - # Apply same per-dep char limits as the workflow node to avoid context overflow. + # Áp dụng cùng giới hạn ký tự per-dep như workflow node để tránh overflow context. dep_max_chars = _PER_DEP_CHARS.get(role, MAX_PREV_OUTPUT_CHARS) - for dep in agent.depends_on: - if dep in step_outputs: - dep_name = AGENTS[dep].name - context_parts.append( - f"\n## Kết quả {dep_name}\n{_truncate(step_outputs[dep], dep_max_chars)}" - ) - - if extra_context: - context_parts.append(f"\n## Context bổ sung\n{extra_context}") - - if rag_enabled and rag_api_url: - _hint = agent.rag_query_hint or agent.name - rag_question = f"{_hint}: {user_input}" - rag_text = _query_rag(rag_api_url, rag_question, project, rag_top_k) - if rag_text: - context_parts.append( - f"\n## Context từ Knowledge Base\n{_truncate(rag_text)}" - ) - + context_parts = _build_context_parts( + role=role, + agent=agent, + user_input=user_input, + step_outputs=step_outputs, + tech_stack=tech_stack, + dep_max_chars=dep_max_chars, + rag_enabled=rag_enabled, + rag_api_url=rag_api_url, + project=project, + rag_top_k=rag_top_k, + extra_context=extra_context, + ) context = "\n".join(context_parts) if role in _ARTIFACT_ROLES: extra_instruction = _compute_extra_instruction(role, tech_stack) @@ -823,3 +1288,4 @@ def run_single_step( extra_instruction, ) return _call_agent(agent, ollama_base_url, context) + diff --git a/docker-compose.yml b/docker-compose.yml index c9d0f3d..5663ec4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -155,6 +155,7 @@ services: TESTER_MODEL: ${TESTER_MODEL} DEVSECOPS_MODEL: ${DEVSECOPS_MODEL} CLARIFIER_MODEL: ${CLARIFIER_MODEL:-qwen3.6:35b} + CLARIFIER_REGEN_LOOPS: ${CLARIFIER_REGEN_LOOPS:-1} MEMORY_DIR: /data/memory ARTIFACT_DIR: /data/artifacts MAX_FILES_PER_ROLE: ${MAX_FILES_PER_ROLE:-6} diff --git a/open-webui-tools/yan_agent_workflow.py b/open-webui-tools/yan_agent_workflow.py index 0457f57..1a7728c 100644 --- a/open-webui-tools/yan_agent_workflow.py +++ b/open-webui-tools/yan_agent_workflow.py @@ -2,11 +2,75 @@ title: YAN SDLC Agent Workflow author: YAN description: > - Khởi chạy SDLC Agent Workflow 15 bước: BA → PM → SA → TA → Designer → Team Lead → FE → Mobile → DBA → BE → DA → Tech Lead → Tester → DevSecOps → Clarifier. - Hỗ trợ chạy full workflow (async + polling) hoặc từng agent đơn lẻ (sync). + Tool khởi chạy SDLC Agent Workflow 15 bước toàn diện: + BA → PM → SA → TA → Designer → Team Lead → FE → Mobile → DBA + → BE → DA → Tech Lead → Tester → DevSecOps → Clarifier. + Hỗ trợ chạy full workflow (async + polling) hoặc từng agent + đơn lẻ (sync). Kết quả bao gồm output markdown, file code + được trích xuất, và Clarifier audit report. required_open_webui_version: 0.3.0 requirements: requests version: 2.0.0 + +Mô tả chi tiết +-------------- +Tool là giao tiếp trực tiếp giữa Open WebUI và YAN Agent API (cổng 8091). +Cho phép người dùng khởi động và theo dõi toàn bộ SDLC pipeline AI +trực tiếp từ giao diện chat mà không cần gọi API thủ công. + +Valves (biến cấu hình) +---------------------- + agent_api_url URL của agent-api trong nội bộ Docker network. + Mặc định: http://agent-api:8091 + + timeout Timeout (giây) cho mỗi single-step agent call. + Mặc định: 600 giây. + + poll_interval Khoảng cách (giây) giữa các lần poll trạng thái workflow. + Mặc định: 15 giây. + + poll_max_attempts Số lần poll tối đa trước khi timeout. + Mặc định: 120 (120 × 15s = 30 phút). + Tăng nếu workflow dùng model lớn, chạy lâu hơn. + + rag_enabled Có query RAG knowledge base cho mỗi agent step không. + Mặc định: true. Tắt khi không có tài liệu đã ingest. + + rag_top_k Số chunk RAG mỗi agent nhận. Mặc định: 5. + + default_project Project mặc định để lọc RAG. Null = không lọc. + +Hàm công khai +-------------- + run_sdlc_workflow(user_input, project, tech_stack) → str + Khởi chạy full workflow 15 bước, poll đến khi hoàn tất, + trả về summary kết quả (status, thời gian, output mỗi bước). + Tham số: + user_input Mô tả yêu cầu / mục tiêu kiếnh doanh. Bắt buộc. + project Tên project để lọc RAG. Tùy chọn. + tech_stack Chuỗi các công nghệ bắt buộc, phân cách bằng dấu phẩy. + Ví dụ: "NestJS, React, PostgreSQL, Kubernetes" + + run_agent_step(role, user_input, project, tech_stack) → str + Chạy đồng bộ một agent đơn lẻ, trả về output ngay. + Hữu ích khi muốn xem nhanh kết quả của một bước cụ thể. + Ví dụ: role="sa" để xem kiến trúc giải pháp. + + get_workflow_result(workflow_id, role) → str + Lấy output của một workflow đã chạy xong theo ID. + role tùy chọn: nếu có thì chỉ trả output của role đó. + Nếu không có role: trả summary toàn bộ workflow. + + list_agent_roles() → str + Liệt kê tất cả 15 role theo thứ tự thực thi, kèm tên, model + và danh sách phụ thuộc. Dùng để khám phá cấu hình hiện tại. + +Ví dụ sử dụng trong chat +------------------------- + Chạy SDLC workflow: xây dựng module marketplace B2B, project=yanlib + Chạy agent sa: thiết kế kiến trúc hệ thống authentication đa tenant + Lấy kết quả workflow abc123, role=be + Liệt kê tất cả agent roles """ import time @@ -14,7 +78,8 @@ import requests from pydantic import BaseModel, Field -# Thứ tự thực thi chuẩn — phải khớp với WORKFLOW_STEPS trong agent-api/agents.py +# Danh sách role hợp lệ theo thứ tự thực thi chuẩn của SDLC pipeline. +# Phải khớp chính xác với WORKFLOW_STEPS trong agent-api/agents.py. _VALID_ROLES = [ "ba", "pm", diff --git a/open-webui-tools/yan_knowledge_base.py b/open-webui-tools/yan_knowledge_base.py index ba29c94..0133a9b 100644 --- a/open-webui-tools/yan_knowledge_base.py +++ b/open-webui-tools/yan_knowledge_base.py @@ -1,11 +1,55 @@ """ title: YAN Knowledge Base author: YAN -description: Query nội bộ — hỏi đáp từ các tài liệu PRD, spec, architecture đã ingest vào Qdrant qua rag-api. - Hỗ trợ filter theo project và module để tăng độ chính xác retrieval. +description: > + Tool hỏi đáp tài liệu nội bộ qua RAG API. Cho phép chạt trực tiếp + với các PRD, spec, kiến trúc, schema đã được ingest vào Qdrant. + Hỗ trợ lọc kết quả theo project và module để tăng độ chính xác retrieval. required_open_webui_version: 0.3.0 requirements: requests version: 1.1.0 + +Mô tả chi tiết +-------------- +Tool này là giao tiếp trực tiếp giữa Open WebUI và YAN RAG API (cổng 8090). +Khi được gọi, tool gửi câu hỏi của người dùng tới endpoint POST /ask của +rag-api, nhận kết quả RAG hybrid (Qdrant vector search + Neo4j graph enrichment ++ Ollama LLM), và trả về câu trả lời kèm danh sách file nguồn. + +Valves (biến cấu hình) +---------------------- + rag_api_url URL của rag-api trong nội bộ Docker network. + Mặc định: http://rag-api:8090 + Khi gọi từ ngoài container: http://localhost:8090 + + timeout Timeout tính bằng giây cho mỗi request. + Mặc định: 120 giây. + + top_k Số chunk kết quả trả về. Null = dùng giá trị RAG_TOP_K + trong env của rag-api (mặc định 5). + + default_project Tên project mặc định nếu người dùng không chỉ định. + Null = tìm kiếm trên tất cả collection. + + default_module Tên module mặc định. Null = toàn bộ project. + Ví dụ: auth, billing, marketplace. + +Hàm công khai +-------------- + ask_internal_docs(question, project, module) → str + Tham số: + question Câu hỏi cần trả lời. Không được rỗng. + project Tên project cần query. Null = search tất cả project. + module Lọc theo module trong project. Null = toàn bộ project. + Trả về: + Câu trả lời dưới dạng text, kèm danh sách file nguồn + và điểm similarity score. + +Ví dụ sử dụng trong chat +------------------------- + Tìm trong knowledge base: JWT refresh token flow hoạt động thế nào? + Hỏi về billing schema của dự án yanlib, module billing + Kiến trúc marketplace là gì? (project=marketplace) """ import requests @@ -41,13 +85,18 @@ def __init__(self): self.valves = self.Valves() def ask_internal_docs(self, question: str, project: str | None = None, module: str | None = None) -> str: - """ - Hỏi-đáp với tài liệu nội bộ: PRD, spec, architecture, billing, auth, marketplace, v.v. - Dùng khi cần tìm thông tin trong các tài liệu kỹ thuật của dự án. - :param question: Câu hỏi cần trả lời - :param project: Tên project cần query (ví dụ: yanlib). Để trống để search tất cả. - :param module: Lọc theo module trong project (ví dụ: auth, billing, marketplace). Để trống để search toàn bộ project. - :return: Câu trả lời kèm tên file nguồn + """Hỏi đáp với tài liệu nội bộ của dự án qua RAG API. + + Dùng khi cần tìm thông tin trong các tài liệu kỹ thuật đã ingest: + PRD, spec, kiến trúc, schema, auth, billing, marketplace... + + Tham số: + question: Câu hỏi cần trả lời. Không được rỗng. + project: Tên project cần query (ví dụ: yanlib). Null = search tất cả. + module: Lọc theo module trong project (ví dụ: auth, billing). Null = toàn project. + + Trả về: + Câu trả lời kèm danh sách file nguồn và điểm similarity score. """ resolved_project = project or self.valves.default_project resolved_module = module or self.valves.default_module diff --git a/rag-api/app.py b/rag-api/app.py index afb7c12..f5380c9 100644 --- a/rag-api/app.py +++ b/rag-api/app.py @@ -1,28 +1,74 @@ """ -app.py — YAN Local RAG API (cổng 8090) - -Endpoints ---------- -GET /health Thông tin service: Qdrant URL, Ollama URL, model. -GET /projects Liệt kê project và trạng thái index. -POST /ingest Ingest tài liệu vào Qdrant (+ Neo4j nếu GRAPH_ENABLED). -POST /reset-ingest Xóa collection rồi ingest lại từ đầu. -POST /ask Hỏi đáp RAG: embed câu hỏi, tìm kiếm vector, sinh câu trả lời bằng LLM. - Hỗ trợ filter theo *project* và *module*. -GET /graph/status Trạng thái kết nối Neo4j. -GET /graph/projects/{project}/entities Liệt kê thực thể Neo4j của một project. - -Module Filter -------------- -POST /ask chấp nhận trường *module* tùy chọn. Nếu có, chỉ tìm kiếm trong các chunk -có payload.module == . Module được sử dụng khi ingest từ cấu trúc thư mục: - data/raw/{project}/{module}/file.md → module = tên thư mục con - data/raw/{project}/file.md → module = project - -Singleton Clients ------------------ - get_qdrant_client() và get_chat_model() dùng module-level instance cache - để tái sử dụng kết nối qua các request (không khởi tạo lại từng lần). +app.py — YAN Local RAG API (cổng 8090) +======================================= + +Mô tả +----- +FastAPI application cung cấp hai chức năng cốt lõi: + 1. Ingest tài liệu — tải, chia chunk, embed và lưu vào Qdrant (+ Neo4j nếu + GRAPH_ENABLED=true). + 2. Hỏi đáp RAG — embed câu hỏi, tìm kiếm Qdrant, làm giàu qua Neo4j + graph traversal, sinh câu trả lời bằng LLM chat model. + +Danh sách endpoint +------------------ +GET /health + Trả về trạng thái hoạt động: URL Qdrant, URL Ollama, tên embedding model, + tên chat model, giá trị RAG_TOP_K và trạng thái kết nối Neo4j. + +GET /projects + Liệt kê tất cả project trong RAW_DATA_DIR, tên Qdrant collection tương ứng, + trạng thái đã index hay chưa và số lượng vector đã lưu. + +POST /ingest + Ingest tài liệu vào Qdrant và Neo4j (nếu GRAPH_ENABLED=true). + Body tùy chọn: {"project": "tên_project", "reset": false} + - Nếu không truyền project: ingest tất cả project có trong RAW_DATA_DIR. + - reset=true: xóa collection cũ trước, sau đó ingest lại toàn bộ. + - Idempotent: chạy lại cùng file sẽ upsert đúng point_id, không tạo bản sao. + +POST /reset-ingest + Xóa toàn bộ collection (hoặc một project cụ thể) rồi ingest lại từ đầu. + Tương đương gọi /ingest với reset=true. + +POST /ask + Hỏi đáp RAG hybrid (Qdrant + Neo4j + LLM). + Body bắt buộc: {"question": "câu hỏi"} + Body tùy chọn: {"project": "...", "module": "...", "top_k": 5} + - project: lọc kết quả theo collection. Null = tìm trên tất cả collection. + - module: lọc kết quả theo metadata chunk.module. Null = toàn bộ project. + - top_k: số chunk trả về. Null = dùng biến môi trường RAG_TOP_K. + Quy trình xử lý: + embed(question) → Qdrant search → Neo4j graph enrichment → LLM answer + +GET /graph/status + Trả về trạng thái kết nối Neo4j và thống kê số lượng node (Project, + Document, Chunk, Entity). + +GET /graph/projects/{project}/entities + Liệt kê tất cả thực thể đã trích xuất cho một project (yêu cầu + GRAPH_ENTITY_EXTRACTION=true khi ingest). + +Lọc theo module +--------------- +Khi ingest, mỗi chunk được gán metadata `module` tự động từ cấu trúc thư mục: + data/raw/{project}/{module}/file.md → chunk.module = tên thư mục con + data/raw/{project}/file.md → chunk.module = project (flat file) + +Đây cho phép POST /ask lọc chính xác theo module để giảm nhiễu: + {"question": "...", "project": "yanlib", "module": "auth"} + +Quản lý singleton client +------------------------ +get_qdrant_client() và get_chat_model() lưu instance ở cấp module (module-level +cache). Mọi request dùng chung một kết nối — không khởi tạo lại từng lần. +Giúp tránh connection leak và giảm overhead kết nối. + +Xử lý lỗi GPU OOM khi embedding +--------------------------------- +Nếu GPU hết bộ nhớ trong lúc embed (thường xảy ra khi nhiều model lớn chạy +song song), rag-api tự retry tối đa _EMBED_MAX_RETRIES lần với delay ngắn +trước khi báo lỗi. Từ khóa OOM được phát hiện qua _OOM_KEYWORDS. """ import logging @@ -50,7 +96,8 @@ def _require_env(name: str) -> str: LOG_LEVEL = _require_env("LOG_LEVEL") -# Retry config cho embed khi GPU OOM (model lớn đang chạy song song) +# Cấu hình retry khi GPU hết bộ nhớ trong lúc embed — thường xảy ra khi +# nhiều model lớn chạy song song và VRAM bị tranh chấp. _OOM_KEYWORDS = ("out of memory", "cudaMalloc", "llama runner process has terminated") _EMBED_MAX_RETRIES = 3 @@ -59,13 +106,13 @@ def _require_env(name: str) -> str: format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", ) -# Tắt spam thông báo schema Neo4j (cảnh báo MENTIONS/Entity chưa tồn tại). -# Xuất hiện mỗi lần gọi /ask khi đồ thị chưa được khởi tạo. +# Tắt log spam về schema Neo4j chưa tồn tại khi đồ thị chưa được khởi tạo. +# Xuất hiện mỗi lần gọi /ask khi Neo4j chưa có node Entity hoặc MENTIONS. logging.getLogger("neo4j.notifications").setLevel(logging.ERROR) logger = logging.getLogger("rag-api") -# ── Module Graph (tùy chọn — giảm cấp nhẹ nhàng nếu Neo4j không khả dụng) ──────── +# ── Import module graph tùy chọn — giảm cấp nhẹ nhàng nếu Neo4j không khả dụng ────── _graph_module = None if GRAPH_ENABLED: try: @@ -79,7 +126,8 @@ def _require_env(name: str) -> str: EMBEDDING_MODEL = _require_env("EMBEDDING_MODEL") CHAT_MODEL = _require_env("CHAT_MODEL") RAG_TOP_K = int(_require_env("RAG_TOP_K")) -# Timeout (giây) cho mỗi lần gọi Ollama từ rag-api. Phải >= RAG_TIMEOUT của agent-api. +# Timeout (giây) cho mỗi lần gọi Ollama từ rag-api. Nếu đặt thấp hơn +# RAG_TIMEOUT của agent-api có thể gây kết quả rỗng bất ngờ. OLLAMA_REQUEST_TIMEOUT: float = float(os.environ.get("OLLAMA_REQUEST_TIMEOUT", "600")) app = FastAPI(title="YAN Local RAG API", version="1.0.0") @@ -121,7 +169,10 @@ class AskResponse(BaseModel): def get_qdrant_client() -> QdrantClient: - """Trả về singleton QdrantClient. Khởi tạo lần đầu và tái sử dụng sau.""" + """Trả về singleton QdrantClient. Khởi tạo lần đầu khi được gọi, tái sử dụng sau. + + Tránh tạo nhiều kết nối song song — Qdrant client không cần pool thường trú. + """ global _qdrant_client if _qdrant_client is None: _qdrant_client = QdrantClient(url=QDRANT_URL) @@ -129,7 +180,11 @@ def get_qdrant_client() -> QdrantClient: def get_chat_model() -> ChatOllama: - """Trả về singleton ChatOllama. Khởi tạo lần đầu và tái sử dụng sau.""" + """Trả về singleton ChatOllama. Khởi tạo lần đầu khi được gọi, tái sử dụng sau. + + Giả sử CHAT_MODEL và OLLAMA_BASE_URL không thay đổi trong vòng đời service. + Thay model yêu cầu restart service để tạo instance mới. + """ global _llm if _llm is None: _llm = ChatOllama( @@ -162,9 +217,10 @@ def health() -> dict[str, Any]: @app.post("/ingest") def ingest_endpoint(req: IngestRequest = IngestRequest()) -> dict[str, Any]: - """ - Ingest tài liệu từ data/raw/{project}/ vào Qdrant. - Nếu *project* là None, ingest toàn bộ project. + """Ingest tài liệu từ data/raw/{project}/ vào Qdrant (và Neo4j nếu GRAPH_ENABLED). + + Nếu *project* là None, ingest toàn bộ project có trong RAW_DATA_DIR. + Bắt lỗi và trả về HTTP 500 nếu ingest thất bại để caller biết rõ nguyên nhân. """ try: return ingest(project=req.project, reset=req.reset) @@ -175,9 +231,10 @@ def ingest_endpoint(req: IngestRequest = IngestRequest()) -> dict[str, Any]: @app.post("/reset-ingest") def reset_ingest_endpoint(req: IngestRequest = IngestRequest()) -> dict[str, Any]: - """ - Xóa collection Qdrant (và graph Neo4j nếu GRAPH_ENABLED) rồi ingest lại từ đầu. - Tương đương POST /ingest với reset=true. + """Xóa collection Qdrant (và graph Neo4j nếu GRAPH_ENABLED) rồi ingest lại từ đầu. + + Tương đương POST /ingest với reset=True. Dùng khi cần làm sạch hoàn toàn + sau khi đổi EMBEDDING_MODEL hoặc cấu trúc thư mục thô. """ try: return ingest(project=req.project, reset=True) diff --git a/rag-api/graph.py b/rag-api/graph.py index 16647ff..aea47a1 100644 --- a/rag-api/graph.py +++ b/rag-api/graph.py @@ -1,15 +1,55 @@ """ -graph.py — Lớp tích hợp Neo4j GraphRAG. - -Mô hình đồ thị: - (:Project)-[:HAS_DOCUMENT]->(:Document)-[:HAS_CHUNK]->(:Chunk) - (:Chunk)-[:NEXT_CHUNK]->(:Chunk) - (:Chunk)-[:MENTIONS]->(:Entity) - (:Entity)-[:CO_OCCURS_WITH]->(:Entity) - -Qdrant xử lý tìm kiếm vector; Neo4j làm giàu kết quả qua co-occurrence thực thể -và graph traversal. Cả hai store dùng chung Qdrant point ID làm Chunk.id -để tham chiếu chéo liền mạch. +graph.py — Lớp tích hợp Neo4j GraphRAG cho YAN RAG stack +========================================================== + +Mô tả +----- +Module quản lý toàn bộ tương tác với Neo4j: tạo/cập nhật cấu trúc đồ thị +khi ingest tài liệu, trích xuất thực thể bằng LLM, xây dựng quan hệ +co-occurrence, và traversal đồ thị để làm giàu kết quả tìm kiếm vector. + +Kiến trúc đồ thị +---------------- +Cấu trúc node và relationship trong Neo4j: + + (:Project {name}) + └─[:HAS_DOCUMENT]─► + (:Document {id, source_file, project, module}) + └─[:HAS_CHUNK]─► + (:Chunk {id, text, chunk_index, source_file, module, project}) + ├─[:NEXT_CHUNK]─► (:Chunk) — chuỗi chunk liên tiếp trong file + ├─[:MENTIONS]─► (:Entity) — thực thể được đề cập (khi extraction bật) + └─[:CO_OCCURS_WITH]─► (:Entity) — thực thể xuất hiện cùng nhau + +Chú ý quan trọng về Chunk.id: + Chunk.id = Qdrant point ID (UUID5 tất định từ path + content hash). + Điều này cho phép tham chiếu chéo chính xác giữa Qdrant và Neo4j + mà không cần join table hay mapping riêng. + +Tích hợp Hybrid RAG +-------------------- +Qdrant đảm nhận tìm kiếm vector (cosine similarity) để lấy top-k chunk +liên quan nhất về mặt ngữ nghĩa. Neo4j làm giàu kết quả bằng cách: + 1. Với mỗi chunk tìm được từ Qdrant, tìm thêm các chunk khác đề cập + cùng thực thể (qua quan hệ MENTIONS + CO_OCCURS_WITH). + 2. Trả về danh sách chunk mở rộng, đã loại trùng, sắp xếp theo score. +Hai nguồn kết quả được gộp lại trước khi đưa vào LLM. + +Trích xuất thực thể (tùy chọn) +-------------------------------- +Khi GRAPH_ENTITY_EXTRACTION=true, sau khi lưu chunk vào Neo4j, module gọi +LLM chat model để trích xuất danh sách thực thể từ nội dung chunk: + - Tên thực thể (entity name) + - Loại thực thể (PERSON, ORG, PRODUCT, CONCEPT, LOCATION, OTHER) +Thực thể được lưu thành node (:Entity) với quan hệ MENTIONS từ Chunk. +Các thực thể xuất hiện trong cùng chunk sẽ có quan hệ CO_OCCURS_WITH. + +Chú ý hiệu suất +--------------- +- GRAPH_ENTITY_EXTRACTION=true làm chậm quá trình ingest đáng kể vì gọi LLM + cho mỗi chunk. Nên tắt khi ingest lần đầu số lượng lớn tài liệu. +- GRAPH_ENABLED=false sẽ bỏ qua hoàn toàn Neo4j — RAG chỉ dùng Qdrant. +- Kết nối Neo4j được tái sử dụng qua module-level driver singleton. """ import logging @@ -28,7 +68,7 @@ def _require_env(name: str) -> str: return value -# ── Cấu hình ───────────────────────────────────────────────────────────────── +# ── Cấu hình — đọc từ biến môi trường khi import ────────────────────────────────────── GRAPH_ENABLED: bool = os.environ.get("GRAPH_ENABLED", "true").lower() == "true" GRAPH_ENTITY_EXTRACTION: bool = os.environ.get( "GRAPH_ENTITY_EXTRACTION", "false").lower() == "true" @@ -41,12 +81,15 @@ def _require_env(name: str) -> str: else: NEO4J_URI = NEO4J_USERNAME = NEO4J_PASSWORD = "" -# ── Driver singleton ───────────────────────────────────────────────────────── +# ── Driver singleton — một kết nối Neo4j duy nhất cho toàn service ─────────────── _driver: Driver | None = None def get_driver() -> Driver: - """Trả về singleton Neo4j Driver. Khởi tạo lần đầu và tái sử dụng sau.""" + """Trả về singleton Neo4j Driver. Khởi tạo lần đầu, tái sử dụng cho mọi request sau. + + Driver quản lý connection pool nội bộ — không cần tạo nhiều driver. + """ global _driver if _driver is None: _driver = GraphDatabase.driver( @@ -55,7 +98,11 @@ def get_driver() -> Driver: def ping() -> bool: - """Kiểm tra kết nối Neo4j. Trả về False thay vì raise exception.""" + """Kiểm tra kết nối Neo4j bằng cách gọi verify_connectivity(). + + Trả về True nếu kết nối thành công, False nếu có ngoại lệ. + Không raise exception — được dùng trong /health endpoint. + """ try: get_driver().verify_connectivity() return True diff --git a/rag-api/ingest.py b/rag-api/ingest.py index aeecbb5..3d49060 100644 --- a/rag-api/ingest.py +++ b/rag-api/ingest.py @@ -1,31 +1,85 @@ """ -ingest.py — Pipeline nạp tài liệu vào Qdrant và Neo4j cho YAN RAG stack. - -Quy trình xử lý ------------------ - load_documents() [song song, ThreadPoolExecutor] - → split_documents() - → _run_async_pipeline() [asyncio + httpx trực tiếp đến Ollama /api/embed] - • asyncio.gather: toàn bộ batch submit đồng thời - • Semaphore giới hạn request đồng thời = INGEST_EMBED_WORKERS - • AsyncQdrantClient: upsert không blocking - → save_graph (Neo4j, tùy chọn) +ingest.py — Pipeline nạp tài liệu vào Qdrant và Neo4j cho YAN RAG stack +========================================================================= + +Mô tả +----- +Module xử lý toàn bộ quy trình ingest: tải tài liệu từ disk, chia thành +các chunk nhỏ, embed bằng Ollama, upsert vào Qdrant và tùy chọn lưu đồ thị +quan hệ vào Neo4j. + +Quy trình xử lý chi tiết +------------------------- + load_documents() + Tải tất cả file trong thư mục project song song bằng ThreadPoolExecutor. + Hỗ trợ các định dạng: .md .txt .pdf .docx .csv .json .jsonl .yaml .yml + .xml .html .py .js .ts .go .rs .java .cs .sh .sql .log + + split_documents() + Chia tài liệu thành chunk bằng RecursiveCharacterTextSplitter. + Kích thước chunk và overlap lấy từ biến môi trường CHUNK_SIZE và + CHUNK_OVERLAP. Mỗi chunk giữ lại metadata nguồn. + + _run_async_pipeline() + Pipeline bất đồng bộ sử dụng asyncio + httpx gọi trực tiếp đến + Ollama endpoint /api/embed (không qua LangChain để tối đa hiệu suất). + - asyncio.gather(): toàn bộ batch embed được submit đồng thời + - asyncio.Semaphore(INGEST_EMBED_WORKERS): giới hạn số request embed + đồng thời, tránh làm quá tải GPU/CPU + - AsyncQdrantClient: upsert vector không blocking + Sau mỗi batch embed+upsert, kết quả được ghi vào Qdrant theo batch + kích thước UPSERT_BATCH_SIZE. + + save_graph() (tùy chọn, khi GRAPH_ENABLED=true) + Ánh xạ mỗi chunk vào Neo4j theo mô hình: + (:Project)-[:HAS_DOCUMENT]->(:Document)-[:HAS_CHUNK]->(:Chunk) + Nếu GRAPH_ENTITY_EXTRACTION=true, LLM trích xuất thực thể từ nội dung + chunk và lưu quan hệ (:Chunk)-[:MENTIONS]->(:Entity) và + (:Entity)-[:CO_OCCURS_WITH]->(:Entity). Idempotency ----------- - make_point_id() sinh UUID5 tất định từ path + content hash. - Chạy /ingest nhiều lần trên cùng file sẽ upsert đúng point_id — không tạo bản sao. - -Metadata chunk --------------- - Mỗi chunk Qdrant có payload: source_file, relative_path, file_type, - chunk_index, content_hash, embedding_model, module, doc_type, chunk_type, - language, status, created_at, page_content, project. - -Graph enrichment (tùy chọn) +make_point_id() sinh UUID5 tất định từ tổ hợp (đường dẫn file + hash nội dung). +Chạy /ingest nhiều lần trên cùng một file sẽ upsert đúng point_id đã tồn tại +— Qdrant tự cập nhật payload, không tạo bản ghi trùng lặp. + +Metadata của mỗi chunk trong Qdrant +------------------------------------ +Mỗi PointStruct có payload đầy đủ: + source_file — tên file gốc + relative_path — đường dẫn tương đối trong data/raw/ + file_type — phần mở rộng (md, pdf, py ...) + chunk_index — số thứ tự chunk trong file + content_hash — MD5 hash nội dung chunk (dùng cho idempotency) + embedding_model — tên model embedding đã dùng + module — tên thư mục con (dùng để filter theo module trong /ask) + doc_type — loại tài liệu: prd, schema, api, architecture, spec, other + chunk_type — loại nội dung: paragraph, table, code, heading + language — ngôn ngữ lập trình nếu là code chunk + status — trạng thái: active + created_at — timestamp ISO 8601 lúc ingest + page_content — nội dung văn bản đầy đủ của chunk + project — tên project + +Suy luận module từ cấu trúc thư mục +------------------------------------- + data/raw/{project}/{module}/file.md → chunk.module = tên thư mục con + data/raw/{project}/file.md → chunk.module = project +Module được dùng bởi POST /ask với tham số module= để lọc kết quả search. + +Các biến môi trường sử dụng ---------------------------- - Nếu GRAPH_ENABLED=true, mỗi chunk được ánh xạ vào Neo4j. - Nếu GRAPH_ENTITY_EXTRACTION=true, LLM sẽ trích xuất thực thể và lưu quan hệ MENTIONS. + EMBEDDING_MODEL — tên model Ollama để embed + OLLAMA_BASE_URL — URL Ollama server + QDRANT_URL — URL Qdrant server + COLLECTION_NAME — tiền tố tên collection + CHUNK_SIZE — kích thước chunk (ký tự) + CHUNK_OVERLAP — overlap giữa chunk kề nhau (ký tự) + UPSERT_BATCH_SIZE — số vector upsert vào Qdrant mỗi batch + INGEST_EMBED_WORKERS — số request embed asyncio đồng thời + GRAPH_ENABLED — true/false bật Neo4j + GRAPH_ENTITY_EXTRACTION — true/false bật LLM entity extraction + NEO4J_URI / NEO4J_USERNAME / NEO4J_PASSWORD / NEO4J_DATABASE """ import asyncio @@ -74,8 +128,9 @@ def _require_env(name: str) -> str: CHUNK_SIZE = int(_require_env("CHUNK_SIZE")) CHUNK_OVERLAP = int(_require_env("CHUNK_OVERLAP")) UPSERT_BATCH_SIZE = int(_require_env("UPSERT_BATCH_SIZE")) -# Số batch embed/upsert chạy đồng thời trong async pipeline. -# Nên điều chỉnh theo tài nguyên thực tế; mặc định 1 để ưu tiên ổn định. +# Số batch embed/upsert chạy đồng thời trong asyncio pipeline. +# Giá trị này cân bằng giữa hiệu suất và ổn định GPU — tăng khi có nhiều +# GPU/CPU sẵn có và OLLAMA_NUM_PARALLEL lớn. INGEST_EMBED_WORKERS: int = max(1, int(os.environ.get("INGEST_EMBED_WORKERS", "1"))) GRAPH_ENABLED: bool = os.environ.get("GRAPH_ENABLED", "true").lower() == "true" @@ -94,12 +149,20 @@ def _require_env(name: str) -> str: def get_collection_name(project: str) -> str: - """Tạo tên Qdrant collection: {COLLECTION_NAME}__{project}.""" + """Tạo tên Qdrant collection theo quy ước {COLLECTION_NAME}__{project}. + + Dùng hai dấu gạch dưới __ làm separator để tránh nhầm lẫn với dấu gạch + trong tên project (ví dụ: "yan_raw_docs__my-project"). + """ return f"{COLLECTION_NAME}__{project}" def get_projects(raw_data_dir: str = RAW_DATA_DIR) -> list[str]: - """Trả về danh sách tên project đã sắp xếp (thư mục con cấp một trong raw_data_dir).""" + """Trả về danh sách tên project đã sắp xếp — là các thư mục con cấp 1 trong raw_data_dir. + + Mỗi thư mục con tương ứng một project độc lập với collection Qdrant riêng. + Trả về [] nếu raw_data_dir không tồn tại. + """ root = Path(raw_data_dir) if not root.exists(): return []