"""CRUD API for the global business → agent / chain mapping (业务链条管理). Binds a fixed business code (3Q / 6BF / 7BF / 8BF) to an optional 智能体 and an optional 业务链条 (a **public** ``roundtable_chains`` row). Used to auto-pick the business chain when a task page launches the multi-agent roundtable. Global / shared — there is no user scoping; all accounts see and edit the same rows. The four business rows are seeded on startup; this API only lists them and updates the agent / chain columns (no create/delete). Routes (prefix ``/api/business-mapping``): GET "" list all business mappings (display order) PUT "/{business_code}" update one row's agent / chain selection """ from __future__ import annotations import logging from datetime import datetime from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel, Field logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/business-mapping", tags=["business-mapping"]) # ── schemas ──────────────────────────────────────────────────────────────── class BusinessMappingResponse(BaseModel): business_code: str label: str = "" agent_id: str | None = None agent_name: str | None = None chain_id: str | None = None chain_title: str | None = None sort_order: int = 0 created_at: datetime | str | None = None updated_at: datetime | str | None = None class BusinessMappingListResponse(BaseModel): mappings: list[BusinessMappingResponse] class BusinessMappingUpdateRequest(BaseModel): # All optional → callers send only what changed; explicit null clears a field. agent_id: str | None = Field(default=None, max_length=64) agent_name: str | None = Field(default=None, max_length=191) chain_id: str | None = Field(default=None, max_length=64) chain_title: str | None = Field(default=None, max_length=512) # ── helpers ────────────────────────────────────────────────────────────── def _get_store(request: Request): store = getattr(request.app.state, "business_mapping_store", None) if store is None: raise HTTPException(status_code=503, detail="Business mapping store not available") return store # ── routes ─────────────────────────────────────────────────────────────── @router.get("", response_model=BusinessMappingListResponse) async def list_mappings(request: Request) -> BusinessMappingListResponse: store = _get_store(request) rows = await store.list_mappings() return BusinessMappingListResponse( mappings=[BusinessMappingResponse(**r) for r in rows] ) @router.put("/{business_code}", response_model=BusinessMappingResponse) async def update_mapping( request: Request, business_code: str, body: BusinessMappingUpdateRequest ) -> BusinessMappingResponse: store = _get_store(request) # Only forward fields the caller explicitly sent (distinguish omitted vs null). fields = body.model_dump(exclude_unset=True) row = await store.update_mapping(business_code, **fields) if row is None: raise HTTPException(status_code=404, detail="Business mapping not found") return BusinessMappingResponse(**row)