Merge remote-tracking branch 'origin/master' into xuhd
This commit is contained in:
commit
06508cfb7c
27 files changed
+1054
-72
No files matched your search
@@ -30,6 +30,7 @@ from app.gateway.routers import (
|
||||
artifacts,
|
||||
assistants_compat,
|
||||
auth,
|
||||
business_mapping,
|
||||
channels,
|
||||
checkpoint_migration,
|
||||
feedback,
|
||||
@@ -766,6 +767,16 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"Roundtable functional agent seeding failed at startup (non-fatal)"
|
||||
)
|
||||
|
||||
# 业务链条管理:幂等补建固定业务行(3Q/6BF/7BF/8BF),agent/chain 初始为空。
|
||||
try:
|
||||
business_mapping_store = getattr(app.state, "business_mapping_store", None)
|
||||
if business_mapping_store is not None:
|
||||
inserted = await business_mapping_store.ensure_seed_defaults()
|
||||
if inserted:
|
||||
logger.info("Seeded %d business mapping rows", inserted)
|
||||
except Exception:
|
||||
logger.exception("Business mapping seeding failed at startup (non-fatal)")
|
||||
|
||||
# 一次性把 .deer-flow/agents/{id}/ 目录里历史遗留的 agent 导入 agents 表。
|
||||
# 路由层原本每次请求都跑一遍这个 sync(扫文件系统 + N 个 agent 各一次 DB
|
||||
# SELECT/UPDATE),把它挪到启动期 + 路由层只保留 TTL 兜底,显著降低
|
||||
@@ -1124,6 +1135,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
||||
# Roundtable-planning business chains at /api/roundtable-chains
|
||||
app.include_router(roundtable_chains.router)
|
||||
|
||||
# Global business → agent / chain mapping at /api/business-mapping
|
||||
app.include_router(business_mapping.router)
|
||||
|
||||
# Standalone parallel multi-agent panel: fan-out streaming + saved tasks
|
||||
app.include_router(parallel_agents.router)
|
||||
app.include_router(parallel_agent_tasks.router)
|
||||
|
||||
@@ -179,6 +179,10 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
|
||||
app.state.roundtable_chain_store = make_roundtable_chain_store(sf)
|
||||
|
||||
from deerflow.persistence.business_mapping import make_business_mapping_store
|
||||
|
||||
app.state.business_mapping_store = make_business_mapping_store(sf)
|
||||
|
||||
from deerflow.persistence.parallel_agent_tasks import make_parallel_agent_task_store
|
||||
|
||||
app.state.parallel_agent_task_store = make_parallel_agent_task_store(sf)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""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)
|
||||
@@ -14,7 +14,7 @@ models:
|
||||
display_name: DeepSeek Chat
|
||||
use: langchain_openai:ChatOpenAI
|
||||
model: deepseek-chat
|
||||
api_key: sk-e1c22b7931dc446f8b67e57878edd105
|
||||
api_key: sk-3fee562f834a4c08bb0d471ef44cb445
|
||||
base_url: https://api.deepseek.com
|
||||
request_timeout: 600.0
|
||||
max_retries: 2
|
||||
@@ -41,7 +41,7 @@ models:
|
||||
display_name: DeepSeek V4 Flash
|
||||
use: langchain_openai:ChatOpenAI
|
||||
model: deepseek-v4-flash
|
||||
api_key: sk-e1c22b7931dc446f8b67e57878edd105
|
||||
api_key: sk-3fee562f834a4c08bb0d471ef44cb445
|
||||
base_url: https://api.deepseek.com
|
||||
request_timeout: 600.0
|
||||
max_retries: 2
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"""Global business → agent / chain mapping persistence (业务链条管理)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from deerflow.persistence.business_mapping.model import BusinessMappingRow
|
||||
from deerflow.persistence.business_mapping.sql import BusinessMappingRepository
|
||||
|
||||
__all__ = [
|
||||
"BusinessMappingRepository",
|
||||
"BusinessMappingRow",
|
||||
"make_business_mapping_store",
|
||||
]
|
||||
|
||||
|
||||
def make_business_mapping_store(session_factory):
|
||||
if session_factory is None:
|
||||
return None
|
||||
return BusinessMappingRepository(session_factory)
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"""ORM model for the global business → agent / chain mapping (业务链条管理).
|
||||
|
||||
A "business mapping" row binds a fixed business code (3Q / 6BF / 7BF / 8BF) to an
|
||||
optional 智能体 (``agent_id``) and an optional 业务链条 (``chain_id`` → a
|
||||
``roundtable_chains`` row, which must be **public** so every account can resolve
|
||||
it). Used to auto-pick the business chain when a task page launches the
|
||||
multi-agent roundtable.
|
||||
|
||||
Unlike ``roundtable_chains`` (per-user), this mapping is **global / shared** —
|
||||
there is no ``user_id`` column; all accounts see and use the same configuration.
|
||||
The fixed business rows are seeded on startup; users only fill in the
|
||||
agent / chain columns from the management page.
|
||||
|
||||
``agent_name`` / ``chain_title`` are denormalized display snapshots so the table
|
||||
renders without cross-referencing and survives a later agent/chain rename or
|
||||
delete (the picked id still drives the auto-flow).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from deerflow.persistence.base import Base
|
||||
from deerflow.persistence.types import BeijingDateTime
|
||||
|
||||
|
||||
class BusinessMappingRow(Base):
|
||||
__tablename__ = "business_mapping"
|
||||
|
||||
# Business code is the natural primary key (e.g. "3Q", "6BF"). Global, no user scope.
|
||||
business_code: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
# Display label shown in the 业务 column (e.g. "任务FX").
|
||||
label: Mapped[str] = mapped_column(String(128), nullable=False, default="")
|
||||
# Picked 智能体 (暂存不用,自动流程当前不消费此字段). NULL = 未配置.
|
||||
agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
agent_name: Mapped[str | None] = mapped_column(String(191), nullable=True)
|
||||
# Picked 业务链条 → roundtable_chains.id (须为 public). NULL = 未配置.
|
||||
chain_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
chain_title: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
# Fixed display order for the management table.
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC)
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
BeijingDateTime(),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(UTC),
|
||||
onupdate=lambda: datetime.now(UTC),
|
||||
)
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
"""SQLAlchemy-backed global business → agent / chain mapping store.
|
||||
|
||||
Global / shared (no ``user_id`` scoping): every account reads and writes the same
|
||||
rows. The four business rows are seeded once on startup (``ensure_seed_defaults``)
|
||||
with empty agent/chain; users fill those in from the management page via
|
||||
``update_mapping``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from deerflow.persistence.business_mapping.model import BusinessMappingRow
|
||||
|
||||
_UNSET = object()
|
||||
|
||||
# Fixed business rows (业务码 → 展示名). Seeded on first boot; agent/chain left empty.
|
||||
# 8BF 页面尚未建好,先占位保留行。顺序即管理表的展示顺序。
|
||||
DEFAULT_BUSINESS_ROWS: list[dict[str, Any]] = [
|
||||
{"business_code": "3Q", "label": "3QFX"},
|
||||
{"business_code": "6BF", "label": "任务FX"},
|
||||
{"business_code": "7BF", "label": "XDFX"},
|
||||
{"business_code": "8BF", "label": "8BF"},
|
||||
]
|
||||
|
||||
|
||||
class BusinessMappingRepository:
|
||||
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
|
||||
self._sf = session_factory
|
||||
|
||||
@staticmethod
|
||||
def _to_dict(row: BusinessMappingRow) -> dict[str, Any]:
|
||||
return {
|
||||
"business_code": row.business_code,
|
||||
"label": row.label,
|
||||
"agent_id": row.agent_id,
|
||||
"agent_name": row.agent_name,
|
||||
"chain_id": row.chain_id,
|
||||
"chain_title": row.chain_title,
|
||||
"sort_order": row.sort_order,
|
||||
"created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at,
|
||||
"updated_at": row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else row.updated_at,
|
||||
}
|
||||
|
||||
async def list_mappings(self) -> list[dict[str, Any]]:
|
||||
stmt = select(BusinessMappingRow).order_by(
|
||||
BusinessMappingRow.sort_order.asc(), BusinessMappingRow.business_code.asc()
|
||||
)
|
||||
async with self._sf() as session:
|
||||
result = await session.execute(stmt)
|
||||
return [self._to_dict(row) for row in result.scalars()]
|
||||
|
||||
async def get_mapping(self, business_code: str) -> dict[str, Any] | None:
|
||||
async with self._sf() as session:
|
||||
row = await session.get(BusinessMappingRow, business_code)
|
||||
return self._to_dict(row) if row is not None else None
|
||||
|
||||
async def update_mapping(
|
||||
self,
|
||||
business_code: str,
|
||||
*,
|
||||
agent_id=_UNSET,
|
||||
agent_name=_UNSET,
|
||||
chain_id=_UNSET,
|
||||
chain_title=_UNSET,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Update an existing (seeded) business row. Returns None if the code is unknown.
|
||||
|
||||
Only forwarded fields are touched; explicit ``None`` clears that field.
|
||||
"""
|
||||
async with self._sf() as session:
|
||||
row = await session.get(BusinessMappingRow, business_code)
|
||||
if row is None:
|
||||
return None
|
||||
if agent_id is not _UNSET:
|
||||
row.agent_id = agent_id or None
|
||||
if agent_name is not _UNSET:
|
||||
row.agent_name = agent_name or None
|
||||
if chain_id is not _UNSET:
|
||||
row.chain_id = chain_id or None
|
||||
if chain_title is not _UNSET:
|
||||
row.chain_title = chain_title or None
|
||||
row.updated_at = datetime.now(UTC)
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
return self._to_dict(row)
|
||||
|
||||
async def ensure_seed_defaults(self) -> int:
|
||||
"""Idempotently insert the fixed business rows if missing. Returns inserted count.
|
||||
|
||||
Never overwrites an existing row (respects user-configured agent/chain).
|
||||
"""
|
||||
inserted = 0
|
||||
now = datetime.now(UTC)
|
||||
async with self._sf() as session:
|
||||
for idx, seed in enumerate(DEFAULT_BUSINESS_ROWS):
|
||||
existing = await session.get(BusinessMappingRow, seed["business_code"])
|
||||
if existing is not None:
|
||||
continue
|
||||
session.add(
|
||||
BusinessMappingRow(
|
||||
business_code=seed["business_code"],
|
||||
label=seed.get("label", ""),
|
||||
agent_id=None,
|
||||
agent_name=None,
|
||||
chain_id=None,
|
||||
chain_title=None,
|
||||
sort_order=idx,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
inserted += 1
|
||||
if inserted:
|
||||
await session.commit()
|
||||
return inserted
|
||||
+2
@@ -22,6 +22,7 @@ from deerflow.persistence.agents.model import (
|
||||
AgentSkillRow,
|
||||
)
|
||||
from deerflow.persistence.admin_stats.model import AdminLeaderboardDailyStatRow
|
||||
from deerflow.persistence.business_mapping.model import BusinessMappingRow
|
||||
from deerflow.persistence.memory_config.model import UserMemoryConfigRow
|
||||
from deerflow.persistence.feedback.model import FeedbackRow
|
||||
from deerflow.persistence.llm_metrics.model import LlmCallMetricRow
|
||||
@@ -61,6 +62,7 @@ __all__ = [
|
||||
"AgentRow",
|
||||
"AgentSkillRow",
|
||||
"AdminLeaderboardDailyStatRow",
|
||||
"BusinessMappingRow",
|
||||
"LlmCallMetricRow",
|
||||
"NotificationRow",
|
||||
"ParallelAgentTaskRow",
|
||||
|
||||
Reference in new issue
Block a user