8.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Enterprise AI agent platform powering three production scenarios:
- Presales — Generate proposal documents (Word/Excel) in ~30 min (down from days)
- Security — Create incident reports in minutes (down from hours)
- Knowledge QA — Provide citation-backed answers with ≥90% reference coverage
Tech Stack: LangGraph orchestration, Claude Sonnet 4.6, Qwen2.5 local models, Milvus vector DB, FastAPI, PostgreSQL, Redis
Common Commands
Development Setup
# Install dependencies (must pass with --frozen)
uv sync --frozen
# Start all services (Docker required)
make up
# Stop services
make down
Testing
# Run all tests (excludes Claude API tests)
make test
# Run unit tests with coverage (must be ≥80%)
make test-unit
# Run integration tests (requires Docker)
make test-integration
# Run specific test file
uv run pytest tests/unit/test_scorer.py -v
# Run single test
uv run pytest tests/unit/test_scorer.py::test_evidence_scorer_below_threshold -v
Code Quality
# Lint (ruff + mypy)
make lint
# Format code
make format
# Static gate (no Docker required)
uv sync --frozen
uv run ruff check . --select E,F,I --quiet
uv run mypy core/ api/ --ignore-missing-imports --no-error-summary
uv run pytest tests/unit/ -x -q --tb=short
Database
# Run migrations
make migrate
# Seed test data
make seed
# Create new migration
uv run alembic revision --autogenerate -m "description"
Health & Monitoring
# Check service health
make health
# View API logs
make logs
Architecture Overview
Core Layers (P5-P8)
The platform follows a strict layered architecture:
P8 — Orchestration Layer (core/engine/)
- LangGraph StateGraph with 5 nodes: ask, plan, act, verify, report
- Interrupt/resume for human-in-the-loop approval gates
- PostgreSQL checkpointer for state persistence
P7 — Profile & Tool Layer (core/profile/, core/tools/)
- SOP YAML profiles define allowed tools, prompts, and thresholds per scenario
- Tool whitelist enforcement happens in
ToolExecutor, NOT in individual tools - All tools return
ToolResult(success=bool, data=dict, error=str, meta=dict)
P6 — LLM & Router Layer (core/llm/, core/router/)
ClaudeClientwraps Anthropic SDK with retry, timeout, token trackingModelRouterroutes between cloud (Claude) and local (Qwen) models- Structured output via tool calling with
tool_choice={"type": "any"}
P5 — Audit & Observability (core/audit/, core/observability/)
- 3-tier audit: L1 PostgreSQL (append-only), L2 OpenTelemetry spans, L3 AES-256-GCM encrypted payloads
- PII sanitization via
Sanitizer.mask()before logging - Cost tracking per request with Decimal precision
API Layer (api/)
- FastAPI with async handlers, Pydantic request/response models
- Middleware stack: Tracing → Logging → RateLimiting → CORS
- JWT auth via
require_authdependency (not global middleware) - RBAC permission matrix: admin, manager, analyst, viewer roles
- SSE streaming for
/api/v1/chatendpoint
Database Layer (db/)
- SQLAlchemy 2.0 async ORM with asyncpg driver
- Alembic migrations in
db/migrations/versions/ - Repository pattern in
db/repositories/
Critical Architecture Rules
R1 — Tool Whitelist Enforcement
ToolExecutor.execute_tool() checks profile.allowed_tools BEFORE calling any tool. Tool implementations NEVER perform whitelist checks themselves.
R2 — Exception Boundary
BaseTool.execute(): NEVER raises exceptions, always returnsToolResult(success=False)- LangGraph nodes: NEVER let exceptions escape; set
state.errorand route to END ToolExecutor: MAY raiseToolNotAllowedErrorfor policy violations
R3 — State Serialization
AgentState is a Pydantic BaseModel and must be JSON-serializable at all times. Large objects (documents, embeddings) are stored externally; state holds only UUIDs/references.
R4 — Approval Gates
Any tool with requires_approval=True MUST call interrupt(ApprovalPayload(...)) before execution. Resume via Command(resume=ApprovalDecision(...)).
R5 — Audit Completeness
Every tool execution (success, failure, OR skipped) MUST produce an AuditEvent. Missing audit entries are P0 bugs. The audit_event table is append-only (enforced by DB trigger).
R6 — Sensitive Data
Sanitizer.mask() MUST be applied to all user inputs before they appear in logs, audit payloads, or API responses. Patterns: phone 138****8888, ID card ****19700101****, password [REDACTED].
R7 — Async Discipline
No synchronous blocking inside async def. CPU-bound operations use asyncio.to_thread(). Every external I/O call has explicit timeout= parameter.
R8 — Decimal for Money
All cost/fee/score calculations use Decimal with explicit precision and ROUND_HALF_UP. DB columns: Numeric(12,6) for USD, Numeric(12,4) for CNY, Numeric(5,4) for evidence scores. NEVER use float.
Settings Access Pattern
# CORRECT — lazy singleton, test-safe
from core.config import get_settings
s = get_settings()
# WRONG — breaks test monkeypatching
from core.config import settings # this symbol does not exist
Testing Standards
Test Isolation
- unit/: Zero external I/O; mock DB, HTTP, Claude, Milvus, Redis
- integration/: Real services via testcontainers or Docker stack
- contracts/: Real Claude API;
@pytest.mark.claude_api; skipped ifANTHROPIC_API_KEYunset
Secret Injection
Use settings_override fixture in tests/conftest.py. NEVER assign os.environ[...] directly in test files.
Coverage Requirements
- Overall: ≥80% across
core/andapi/(enforced by--cov-fail-under=80) core/engine/: ≥90%core/verify/: ≥95% (formula is critical path)
Test Naming Convention
test_<unit_under_test>_<scenario>_<expected_outcome>
Examples:
test_ask_node_missing_required_fields_returns_clarification
test_tool_executor_unlisted_tool_raises_and_audits
test_evidence_scorer_below_reject_threshold_returns_insufficient
Evidence Scoring Formula
DO NOT modify without updating tests and Profile schema defaults:
score = (
Decimal("0.4") * retrieval_score
+ Decimal("0.3") * completeness_score
+ Decimal("0.3") * historical_accuracy
)
# Thresholds (overridable per Profile):
# sufficient : score >= 0.85 → normal output
# limited : score >= 0.60 → output with ⚠️ warning
# insufficient : score < 0.60 → reject, explain gaps
Claude API Contract
- Use
anthropicSDK version locked inuv.lock - Structured output:
tool_choice={"type": "any"}to force tool calling - Parse tool results:
content_block.input(dict) directly — NEVER regex-parse - Prompt Caching: add
cache_control={"type": "ephemeral"}to system message blocks - Token tracking: record
usage.input_tokens,usage.output_tokens,usage.cache_read_input_tokensafter EVERY API call - Timeout: pass
httpx.Timeout(connect=10, read=60, write=10, pool=5)toAsyncAnthropicconstructor
FastAPI Patterns
Error Response Format
ALL error responses must use:
class ErrorResponse(BaseModel):
code: str # machine-readable, e.g. "TOOL_NOT_ALLOWED"
message: str # human-readable Chinese message
trace_id: str
details: dict = Field(default_factory=dict)
HTTP Status Codes
- 200: Success
- 202: Async task accepted
- 400: Client validation error
- 401: Missing/invalid JWT
- 403: Insufficient permission
- 404: Resource not found
- 409: State conflict (e.g. already decided)
- 422: Pydantic validation failure
- 500: Unhandled server error
- 503: Dependency unhealthy
AgentEngine Lifecycle
Initialized ONCE in api/main.py lifespan startup, stored at app.state.engine. NEVER instantiate AgentEngine inside route handlers.
Commit Convention
feat(engine): implement interrupt/resume approval in act_node
fix(verify): correct EWMA weight formula in EvidenceScorer
test(api): add integration test for /approve expired-token path
chore(deps): add alembic, cryptography to pyproject.toml
refactor(tools): extract _resolve_params to standalone module
Important Files
AGENTS.md: Project root instructions (read this first)core/AGENTS.md: Core engine submodule instructionsapi/AGENTS.md: API layer submodule instructionstests/AGENTS.md: Testing standards and patternspyproject.toml: Python dependencies and tool configurationuv.lock: Locked dependency versions (MUST be committed)Makefile: Common development commandsdocker-compose.yml: Local development servicesalembic.ini: Database migration configuration