Skip to content

coderiukl/Multi-Agent-Job-Assistant

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🤖 Multi-Agent Job Search

Hệ thống tuyển dụng thông minh — ReAct Supervisor điều phối các domain agent (Match / Analyze / Scrape) để hỗ trợ HR và candidate qua chat, hybrid search, và scrape thị trường (TopCV / ITviec).

React FastAPI LangGraph Claude Qdrant PostgreSQL Langfuse Docker


✨ Giới Thiệu

Multi-Agent Job Search là một nền tảng AI hỗ trợ tuyển dụng đa vai trò:

  • 🧑‍💼 HR: tạo & quản lý JD, xem ứng viên matching theo score, phân tích chi tiết CV vs JD.
  • 👤 Candidate: upload CV, hỏi đáp về độ phù hợp, tìm việc trong kho nội bộ hoặc cào tự động từ TopCV / ITviec.
  • 🤖 Chatbot: một ReAct Supervisor (LangGraph) tự chọn tool phù hợp (search / match / analyze / scrape) thay vì hardcode if/else.
  • 📊 Observability: trace mọi LLM call + tool call qua Langfuse (session_id, user_id, token usage).
  • Cron: cào thị trường định kỳ 6h/lần cho 8 từ khoá hot.

🚀 Tính Năng Chính

Nhóm Chức năng
👤 Auth Đăng ký / đăng nhập, phân quyền HR/candidate, bcrypt password
📝 JD CRUD JD, parse JD (LLM + regex fallback), 6-chunk embedding profile/skills/requirements/responsibilities/benefits
📄 CV Upload PDF, OCR (Tesseract) + LLM cleanup, parse có cấu trúc, embed multi-field
🔎 Matching Hybrid search (dense BGE-M3 + sparse BM25) qua Qdrant RRF fusion
🧠 Analysis Claude Sonnet 4.6 sinh fit_summary, strengths/weaknesses, missing_skills, recommendation
💬 Chat ReAct Supervisor + 4 tool, memory đa lượt (Postgres)
🕷️ Scraper Tavily search TopCV + ITviec, dedupe URL, rate-limit 60s, guardrail title + skill, async job queue
⏰ Cron Cào 8 top keywords / 6h, idempotent, langfuse trace
📡 Observability Langfuse v4 — generation/agent/tool spans, session_id, user_id, token usage

🧱 Tech Stack

Layer Công nghệ
🎨 Frontend React 18, Vite, Pure CSS
⚙️ Backend FastAPI, Pydantic v2, SQLAlchemy 2, Uvicorn
🤖 Orchestrator LangGraph prebuilt ReAct agent, LangChain Core, LangChain OpenAI
🧠 LLM Claude Opus / Sonnet 4.6 (heavy), GPT-5.5 (light), proxy qua 9router (OpenAI-compat)
🧬 Embedding BAAI/bge-m3 (dense 1024d), Qdrant/bm25 (sparse)
🧠 Vector DB Qdrant — named dense + sparse vectors, hybrid RRF
🗄️ RDBMS PostgreSQL 16 (users, sessions, conversations, messages, applications)
🕷️ Scraper Tavily Search API (TopCV, ITviec)
📡 Tracing Langfuse v4 (OTEL-based)
📄 PDF/OCR PyMuPDF, Tesseract
📦 DevOps Docker, Docker Compose 4 service + 1 cron sidecar

🗺️ Kiến Trúc Tổng Quan

graph TB
    subgraph Client["🌐 Client"]
        UI[React + Vite UI]
    end

    subgraph FastAPI["⚙️ FastAPI Backend"]
        direction TB
        ROUTES[Routes: auth · cv · jd · chat]
        CHAT[ChatService]
        SUPER["🤖 ReAct Supervisor (LangGraph)"]
        TOOLS["🛠️ Tools: search_internal · match · analyze · scrape"]
        AGENTS["Domain Agents: Matching · Analysis · Scraper"]
        ROUTES --> CHAT --> SUPER --> TOOLS --> AGENTS
    end

    subgraph Storage["💾 Storage"]
        QD[(Qdrant<br/>JD/CV chunks)]
        PG[(PostgreSQL<br/>users + chat memory)]
        FS[/uploads /<br/>PDF files/]
    end

    subgraph External["☁️ External"]
        LLM[9router / Claude Sonnet 4.6]
        TAV[Tavily Search API]
        LF[Langfuse Cloud]
    end

    subgraph Bg["⏰ Background"]
        CRON["Cron sidecar<br/>(6h interval)"]
    end

    UI -->|HTTP| ROUTES
    AGENTS -->|embed + search| QD
    ROUTES -->|session/auth/memory| PG
    ROUTES -.->|CV PDF| FS
    AGENTS -->|prompt| LLM
    AGENTS -->|scrape| TAV
    SUPER -.->|trace + spans| LF
    CRON -->|scrape_and_index| AGENTS
Loading

🔄 Luồng Hoạt Động Chính

1️⃣ Chat Flow (ReAct Supervisor)

sequenceDiagram
    participant U as User
    participant API as /chat/{id}/messages
    participant SUP as Supervisor
    participant LLM as Claude Sonnet
    participant T as Tool
    participant Q as Qdrant
    participant LF as Langfuse

    U->>API: POST { message, jd_id?, cv_id? }
    API->>SUP: invoke(conversation, message)
    activate SUP
    SUP->>LF: start span "chat.message" (session_id, user_id)
    SUP->>LLM: system prompt + history + user msg
    LLM-->>SUP: tool_call (vd: search_internal_jd)
    SUP->>T: execute tool
    T->>Q: hybrid_search / scroll
    Q-->>T: chunks
    T-->>SUP: tool result
    SUP->>LLM: tool result → final reasoning
    LLM-->>SUP: reply text
    SUP-->>API: ChatResponse
    SUP->>LF: flush trace
    deactivate SUP
    API-->>U: { reply, tool_calls }
Loading

2️⃣ Job Scrape (Async + Guardrail + Rate-limit)

sequenceDiagram
    participant U as User
    participant API as /jd/scrape
    participant BG as BackgroundTask
    participant SC as JobScraperAgent
    participant TAV as Tavily
    participant Q as Qdrant

    U->>API: POST { query, sources, limit }
    API->>BG: enqueue job_id
    API-->>U: { job_id, status:"queued" }
    activate BG
    BG->>SC: scrape_and_index()
    SC->>SC: rate-limit check (60s/query)
    SC->>TAV: search_jobs
    TAV-->>SC: [URLs + snippets]
    loop mỗi URL
        SC->>Q: dedupe by url_hash
        SC->>TAV: extract_content(url)
        SC->>SC: parse JD + guardrail (skills + title)
        SC->>Q: embed + upsert
    end
    BG-->>API: mark_done(result)
    deactivate BG

    U->>API: GET /jd/scrape/{job_id} (poll mỗi 3s)
    API-->>U: { status:"done", result:{ scraped, indexed, dup, invalid } }
Loading

3️⃣ CV Matching

flowchart LR
    A[Upload CV PDF] --> B[PyMuPDF + Tesseract OCR]
    B --> C[LLM cleanup OCR<br/>nếu confidence < 85]
    C --> D[Parse CV: summary, skills, exp, projects]
    D --> E[Multi-field embedding]
    E --> F[(Qdrant: dense + sparse)]
    G[HR chọn JD] --> H[MatchingAgent]
    F --> H
    H --> I[Hybrid search RRF]
    I --> J[Top-K candidates + scores]
    J --> K[AnalysisAgent → Claude Sonnet]
    K --> L[fit_summary + strengths + missing_skills]
Loading

📁 Cấu Trúc Dự Án

multi-agent-job-search/
├── backend/
│   ├── agents/                       # Domain agents + supervisor
│   │   ├── supervisor.py             # ReAct orchestrator (LangGraph prebuilt)
│   │   ├── tools.py                  # StructuredTool factories
│   │   ├── analysis_agent.py         # CV vs JD chi tiết
│   │   ├── matching_agent.py         # JD → top candidates
│   │   ├── scraper_agent.py          # Tavily scrape + dedupe + rate-limit
│   │   └── schemas.py                # Pydantic schemas (args + results)
│   ├── database/
│   │   ├── db.py                     # SQLAlchemy engine + session
│   │   ├── models.py                 # User, Session, Conversation, Message, Application
│   │   └── init_db.py
│   ├── routes/
│   │   ├── auth_route.py             # /auth/register, /login, /me
│   │   ├── cv_route.py               # /cv/upload, /evaluate, /apply
│   │   ├── jd_route.py               # /jd/create, /update, /scrape, /match
│   │   └── chat_route.py             # /chat/conversations, /messages
│   ├── services/
│   │   ├── llm/                      # 9router OpenAI-compat client
│   │   │   ├── client.py             # call_llm, call_llm_json, retry, usage capture
│   │   │   ├── prompts.py            # 5 prompt templates
│   │   │   └── tasks.py              # task wrappers (analyze, extract, chat)
│   │   ├── observability/            # Langfuse v4 wrapper (no-op nếu thiếu key)
│   │   ├── scraper/
│   │   │   ├── tavily_service.py     # TavilyClient wrap + lru cache
│   │   │   └── scrape_jobs.py        # in-memory job status tracking
│   │   ├── chat/                     # memory_service, schemas, chat_service
│   │   ├── cv/                       # PDF extractor, OCR, regex parser
│   │   ├── jd/                       # JD service, regex parser
│   │   ├── auth_service.py
│   │   ├── embedding_service.py      # BGE-M3 + BM25
│   │   └── qdrant_service.py         # Hybrid search RRF
│   ├── Dockerfile
│   └── main.py                       # FastAPI lifespan + wire services
├── frontend/
│   ├── src/
│   │   ├── main.jsx
│   │   └── styles.css
│   ├── Dockerfile
│   ├── nginx.conf
│   └── package.json
├── scripts/
│   └── cron_crawl.py                 # Crawl 8 top keyword × 5 JD / 6h
├── docs/                             # Phase notes
├── uploads/                          # CV PDFs (gitignored)
├── docker-compose.yml                # qdrant + postgres + backend + frontend + cron
├── requirements.txt
└── .env                              # gitignored

✅ Yêu Cầu Môi Trường

  • 🐳 Docker Desktop (Windows / macOS) hoặc Docker Engine + Compose plugin (Linux). Tối thiểu 6GB RAM cho container backend (embed models lớn).
  • 🟢 Node.js 20+ (chỉ cần nếu chạy frontend ngoài Docker).
  • 🐍 Python 3.10+ (chỉ cần nếu chạy backend ngoài Docker).
  • 🔑 API keys:
    • 9router / LLM provider (Claude / GPT-5.5 qua proxy OpenAI-compat).
    • Tavily (tavily.com, free 1000 req/tháng).
    • Langfuse (cloud.langfuse.com, optional — bỏ trống nếu không cần trace).

⚙️ Cấu Hình .env

Tạo file .env ở root (nếu chưa có):

# === Database ===
DATABASE_URL=postgresql://jobagent:jobagent123@localhost:5432/jobagent

# === LLM (9router OpenAI-compat) ===
LLM_PROVIDER=9router
LLM_BASE_URL=http://host.docker.internal:20128/v1/chat/completions
LLM_API_KEY=sk-...

# Per-task model overrides
LLM_MODEL_SUPERVISOR=cc/claude-sonnet-4-6
LLM_MODEL_CV_PARSE=cc/claude-sonnet-4-6
LLM_MODEL_JD_PARSE=cc/claude-sonnet-4-6
LLM_MODEL_CV_JD_ANALYSIS=cc/claude-sonnet-4-6
LLM_MODEL_GENERAL_CHAT=cc/claude-sonnet-4-6
LLM_MODEL_INTENT=cx/gpt-5.5
LLM_MODEL_OCR_CLEANUP=cx/gpt-5.5

# === Tavily (scrape thị trường) ===
TAVILY_API_KEY=tvly-...
SCRAPER_MAX_RESULTS=10
SCRAPER_INCLUDE_DOMAINS=topcv.vn,itviec.com

# === Langfuse (optional - tracing) ===
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com

⚠️ Không commit .env — đã có trong .gitignore.


🐳 Chạy Bằng Docker (Khuyến Nghị)

Windows (PowerShell)

# 1. Clone
git clone <repo-url>
cd multi-agent-job-search

# 2. Tạo .env theo template ở trên
notepad .env

# 3. Build + up tất cả service (lần đầu ~10-15 phút do tải embedding model)
docker compose up -d --build

# 4. Theo dõi log backend đến khi "Application startup complete"
docker compose logs -f backend

macOS / Linux (bash / zsh)

git clone <repo-url>
cd multi-agent-job-search

cp .env.example .env   # hoặc tạo mới theo template
nano .env              # điền API keys

docker compose up -d --build

docker compose logs -f backend

Truy cập service

Service URL Note
🎨 Frontend http://localhost:5173 React UI
⚙️ Backend http://localhost:8000 FastAPI + auto docs /docs
🧬 Qdrant http://localhost:6333/dashboard Vector DB UI
🗄️ PostgreSQL localhost:5432 User: jobagent / Pass: jobagent123
⏰ Cron (background) docker compose logs cron

Lệnh thường dùng

docker compose ps                          # check status
docker compose logs -f backend             # tail backend log
docker compose restart backend             # restart 1 service
docker compose run --rm cron python -m scripts.cron_crawl   # trigger crawl ngay
docker compose down                        # stop all
docker compose down -v                     # stop + xoá volume (mất dữ liệu)

🛠️ Chạy Thủ Công (Dev Local)

Backend

Windows (PowerShell):

python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txt

# Cần Qdrant + Postgres chạy sẵn
docker compose up -d qdrant postgres

uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000

macOS / Linux:

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

docker compose up -d qdrant postgres

uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000

Backend cần dùng DATABASE_URL=postgresql://jobagent:jobagent123@localhost:5432/jobagent (đổi postgreslocalhost khi không chạy trong Docker network).

Frontend

cd frontend
npm install
npm run dev

Cron (chạy 1 lần để test scrape thị trường)

# Trong venv backend
python -m scripts.cron_crawl

🧭 Quy Trình Sử Dụng

  1. 🌐 Mở http://localhost:5173.
  2. 🔐 Register tài khoản → chọn role HR hoặc Candidate.
  3. 🧑‍💼 HR: tạo JD qua form đầy đủ (title, skills, salary, mô tả).
  4. 📄 Candidate: chọn 1 JD → upload CV → bấm Apply.
  5. 🎯 HR: vào JD → bấm Matching để xem top candidates theo score.
  6. 💬 Mở chat — hỏi bất kỳ: "JD này phù hợp với CV nào?", "Top 5 ứng viên?", "Có vị trí Python Senior nào ở HN?" — Supervisor tự gọi tool.
  7. 🕷️ Candidate: search "VPN Engineer" → rỗng → bấm "Tìm trên thị trường" → Tavily cào TopCV/ITviec → JD mới xuất hiện sau ~30s.
  8. 📊 Mở cloud.langfuse.com → xem trace mỗi request + token cost.

🧪 Verify Phase Live

# Login
TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"<email>","password":"<pwd>"}' | jq -r .data.access_token)

# Submit scrape job
JOB=$(curl -s -X POST http://localhost:8000/jd/scrape \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"Senior Python Developer Ha Noi","limit":5}' | jq -r .data.job_id)

# Poll status
curl -s http://localhost:8000/jd/scrape/$JOB \
  -H "Authorization: Bearer $TOKEN" | jq

Expected: status queuedrunningdone sau ~20s với result.indexed > 0.

Re-submit cùng query trong 60s → error: "rate_limited: retry sau Xs" (rate-limit hoạt động).


🩺 Troubleshooting

Symptom Fix
503 Tavily chưa cấu hình Set TAVILY_API_KEY trong .envdocker compose restart backend
[Langfuse] init failed Sai key. Để trống cả 2 keys nếu không cần — sẽ silent skip
Backend không start, log host.docker.internal: Name or service not known (Linux) Linux không có host.docker.internal. Đổi LLM_BASE_URL thành IP host hoặc thêm extra_hosts: ["host.docker.internal:host-gateway"] trong docker-compose backend
Embedding chậm lần đầu (Downloading...) Model BGE-M3 ~2GB. Lần đầu ~3-5 phút
port 5432 already in use Postgres host đang chạy. Đổi ports: ["5433:5432"] trong docker-compose
Cron không log gì docker compose logs cron — đợi cron sleep cycle hoặc chạy thủ công docker compose run --rm cron python -m scripts.cron_crawl

📌 Ghi Chú

  • Ownership: JD external từ scraper có hr_id = null + external = true. Candidate xem + link sang trang gốc, không ứng tuyển qua hệ thống.
  • Privacy: Parser CV/JD KHÔNG trích xuất PII (email, phone, address) — chỉ giữ professional content.
  • Cost: LLM Sonnet 4.6 cho tool reasoning, GPT-5.5 cho task nhẹ (intent / OCR cleanup) — tối ưu chi phí.
  • Rollback: Phase B/C đã verify live, không còn fallback workflow cũ. Để revert dùng git tag/branch.

🏁 Tóm Tắt Stack

React UI
   ↓ HTTP
FastAPI + ReAct Supervisor (LangGraph)
   ├→ Tools → Domain Agents (Match · Analyze · Scrape · Search)
   │                ↓
   ├→ Qdrant (hybrid search BGE-M3 + BM25)
   ├→ PostgreSQL (auth + chat memory)
   ├→ Tavily (TopCV + ITviec scraping)
   ├→ 9router / Claude Sonnet (reasoning)
   └→ Langfuse (observability)

Một câu hỏi user → Supervisor tự decide → tool tự execute → reply có data thật, mọi step được trace.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors