添加推荐问题修改接口
This commit is contained in:
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])
|
||||
+44
@@ -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")
|
||||
+18
@@ -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)
|
||||
+21
@@ -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))
|
||||
+85
@@ -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()
|
||||
Reference in new issue
Block a user