添加推荐问题修改接口

This commit is contained in:
fengxiangdebaozi committed 2026-05-14 11:07:28 +08:00
1 parent 62292500ba
commit 2f2ba44c7c
13 files changed
+481 -126

No files matched your search

@@ -30,6 +30,7 @@ from app.gateway.routers import (
mcp,
memory,
models,
recommended_questions,
runs,
scheduled_tasks,
skills,
@@ -485,6 +486,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
# Agents API is mounted at /api/agents
app.include_router(agents.router)
# Recommended Questions API is mounted at /api/recommended-questions
app.include_router(recommended_questions.router)
# Suggestions API is mounted at /api/threads/{thread_id}/suggestions
app.include_router(suggestions.router)
@@ -94,6 +94,10 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
app.state.agent_store = make_agent_store(sf)
from deerflow.persistence.recommended_questions import make_recommended_question_store
app.state.recommended_question_store = make_recommended_question_store(sf)
# Run event store (has its own factory with config-driven backend selection)
run_events_config = getattr(config, "run_events", None)
app.state.run_event_store = make_run_event_store(run_events_config)
@@ -1,3 +1,3 @@
from . import artifacts, assistants_compat, mcp, models, skills, suggestions, thread_runs, threads, uploads
from . import artifacts, assistants_compat, mcp, models, recommended_questions, skills, suggestions, thread_runs, threads, uploads
__all__ = ["artifacts", "assistants_compat", "mcp", "models", "skills", "suggestions", "threads", "thread_runs", "uploads"]
__all__ = ["artifacts", "assistants_compat", "mcp", "models", "recommended_questions", "skills", "suggestions", "threads", "thread_runs", "uploads"]
@@ -0,0 +1,101 @@
"""CRUD API for recommended questions."""
from __future__ import annotations
import logging
from datetime import datetime
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["recommended-questions"])
class RecommendedQuestionResponse(BaseModel):
id: str
title: str
content: str
sort_order: int = 0
created_at: datetime | str | None = None
updated_at: datetime | str | None = None
class RecommendedQuestionsListResponse(BaseModel):
questions: list[RecommendedQuestionResponse]
class RecommendedQuestionCreateRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=256)
content: str = Field(..., min_length=1)
sort_order: int = Field(default=0)
class RecommendedQuestionUpdateRequest(BaseModel):
title: str | None = Field(default=None, max_length=256)
content: str | None = None
sort_order: int | None = None
class ReorderRequest(BaseModel):
ids: list[str] = Field(..., description="Question IDs in the desired order")
def _get_store(request: Request):
store = getattr(request.app.state, "recommended_question_store", None)
if store is None:
raise HTTPException(status_code=503, detail="Recommended question store not available")
return store
@router.get("/recommended-questions", response_model=RecommendedQuestionsListResponse)
async def list_recommended_questions(request: Request) -> RecommendedQuestionsListResponse:
store = _get_store(request)
rows = await store.list_all()
return RecommendedQuestionsListResponse(questions=[RecommendedQuestionResponse(**r) for r in rows])
@router.post("/recommended-questions", response_model=RecommendedQuestionResponse, status_code=201)
async def create_recommended_question(request: Request, body: RecommendedQuestionCreateRequest) -> RecommendedQuestionResponse:
store = _get_store(request)
data = {
"id": str(uuid4()),
"title": body.title.strip(),
"content": body.content.strip(),
"sort_order": body.sort_order,
}
row = await store.create(data)
return RecommendedQuestionResponse(**row)
@router.put("/recommended-questions/{question_id}", response_model=RecommendedQuestionResponse)
async def update_recommended_question(request: Request, question_id: str, body: RecommendedQuestionUpdateRequest) -> RecommendedQuestionResponse:
store = _get_store(request)
data: dict = {}
if body.title is not None:
data["title"] = body.title.strip()
if body.content is not None:
data["content"] = body.content.strip()
if body.sort_order is not None:
data["sort_order"] = body.sort_order
row = await store.update(question_id, data)
if row is None:
raise HTTPException(status_code=404, detail="Question not found")
return RecommendedQuestionResponse(**row)
@router.delete("/recommended-questions/{question_id}", status_code=204)
async def delete_recommended_question(request: Request, question_id: str) -> None:
store = _get_store(request)
deleted = await store.delete(question_id)
if not deleted:
raise HTTPException(status_code=404, detail="Question not found")
@router.put("/recommended-questions-reorder", response_model=RecommendedQuestionsListResponse)
async def reorder_recommended_questions(request: Request, body: ReorderRequest) -> RecommendedQuestionsListResponse:
store = _get_store(request)
rows = await store.reorder(body.ids)
return RecommendedQuestionsListResponse(questions=[RecommendedQuestionResponse(**r) for r in rows])
@@ -0,0 +1,44 @@
"""Create recommended_questions table.
Revision ID: 20260514_01
Revises: 20260513_02
Create Date: 2026-05-14
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
revision = "20260514_01"
down_revision = "20260513_02"
branch_labels = None
depends_on = None
def _has_table(table_name: str) -> bool:
inspector = inspect(op.get_bind())
return table_name in inspector.get_table_names()
def upgrade() -> None:
if _has_table("recommended_questions"):
return
op.create_table(
"recommended_questions",
sa.Column("id", sa.String(64), primary_key=True),
sa.Column("title", sa.String(256), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_recommended_questions_sort_order", "recommended_questions", ["sort_order"])
def downgrade() -> None:
if _has_table("recommended_questions"):
op.drop_table("recommended_questions")
@@ -0,0 +1,18 @@
"""Recommended questions persistence."""
from __future__ import annotations
from deerflow.persistence.recommended_questions.model import RecommendedQuestionRow
from deerflow.persistence.recommended_questions.sql import RecommendedQuestionRepository
__all__ = [
"RecommendedQuestionRepository",
"RecommendedQuestionRow",
"make_recommended_question_store",
]
def make_recommended_question_store(session_factory):
if session_factory is None:
return None
return RecommendedQuestionRepository(session_factory)
@@ -0,0 +1,21 @@
"""ORM model for recommended questions."""
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy import DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from deerflow.persistence.base import Base
class RecommendedQuestionRow(Base):
__tablename__ = "recommended_questions"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
title: Mapped[str] = mapped_column(String(256), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
@@ -0,0 +1,85 @@
"""SQLAlchemy-backed recommended questions repository."""
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.recommended_questions.model import RecommendedQuestionRow
def _row_to_dict(row: RecommendedQuestionRow) -> dict[str, Any]:
data = row.to_dict()
for key in ("created_at", "updated_at"):
value = data.get(key)
if isinstance(value, datetime):
data[key] = value.isoformat()
return data
class RecommendedQuestionRepository:
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._sf = session_factory
async def list_all(self) -> list[dict[str, Any]]:
stmt = select(RecommendedQuestionRow).order_by(RecommendedQuestionRow.sort_order.asc(), RecommendedQuestionRow.created_at.asc())
async with self._sf() as session:
result = await session.execute(stmt)
return [_row_to_dict(row) for row in result.scalars()]
async def get(self, question_id: str) -> dict[str, Any] | None:
async with self._sf() as session:
row = await session.get(RecommendedQuestionRow, question_id)
return _row_to_dict(row) if row is not None else None
async def create(self, data: dict[str, Any]) -> dict[str, Any]:
now = datetime.now(UTC)
row = RecommendedQuestionRow(
id=data["id"],
title=data["title"],
content=data["content"],
sort_order=data.get("sort_order", 0),
created_at=now,
updated_at=now,
)
async with self._sf() as session:
session.add(row)
await session.commit()
await session.refresh(row)
return _row_to_dict(row)
async def update(self, question_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
async with self._sf() as session:
row = await session.get(RecommendedQuestionRow, question_id)
if row is None:
return None
for key in ("title", "content", "sort_order"):
if key in data:
setattr(row, key, data[key])
row.updated_at = datetime.now(UTC)
await session.commit()
await session.refresh(row)
return _row_to_dict(row)
async def delete(self, question_id: str) -> bool:
async with self._sf() as session:
row = await session.get(RecommendedQuestionRow, question_id)
if row is None:
return False
await session.delete(row)
await session.commit()
return True
async def reorder(self, ordered_ids: list[str]) -> list[dict[str, Any]]:
async with self._sf() as session:
now = datetime.now(UTC)
for index, question_id in enumerate(ordered_ids):
row = await session.get(RecommendedQuestionRow, question_id)
if row is not None:
row.sort_order = index
row.updated_at = now
await session.commit()
return await self.list_all()