Files
diary-news/backend/app/api/admin_llm.py

134 lines
5.2 KiB
Python
Raw Normal View History

"""Admin LLM 设置(仅 owner)。
- GET /admin/llm-settings 读当前设置(单行)
- PUT /admin/llm-settings 更新(可只传部分字段)
- POST /admin/llm-settings/reset 恢复默认提示词
- POST /admin/llm-settings/test 测一次连通性(发个最小 chat 请求)
- POST /admin/llm-enrich/{article_id} 手动触发某篇的 LLM 增强
"""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
from app.core.deps import require_owner
from app.database import AsyncSessionLocal
from app.models.article import Article
from app.models.llm_setting import LlmSetting
from app.schemas.llm import LlmSettingOut, LlmSettingUpdate, get_default_prompts
from app.services.llm.client import LlmClient
logger = logging.getLogger("news.admin_llm")
router = APIRouter(prefix="/admin/llm", tags=["admin-llm"], dependencies=[Depends(require_owner)])
@router.get("/settings", response_model=LlmSettingOut)
async def get_settings():
async with AsyncSessionLocal() as session:
row = (await session.execute(select(LlmSetting).where(LlmSetting.id == 1))).scalar_one_or_none()
if row is None:
# 返回默认值(不写库)
defaults = get_default_prompts()
return LlmSettingOut(
format_prompt=defaults["format_prompt"],
classify_prompt=defaults["classify_prompt"],
commentary_prompt=defaults["commentary_prompt"],
image_prompt_template=defaults["image_prompt_template"],
blocklist_tags=[],
)
return LlmSettingOut.model_validate(row)
@router.put("/settings", response_model=LlmSettingOut)
async def update_settings(body: LlmSettingUpdate):
async with AsyncSessionLocal() as session:
row = (await session.execute(select(LlmSetting).where(LlmSetting.id == 1))).scalar_one_or_none()
if row is None:
row = LlmSetting(id=1, **get_default_prompts())
session.add(row)
await session.flush()
# 只更新传入的字段
update_data = body.model_dump(exclude_unset=True)
for k, v in update_data.items():
setattr(row, k, v)
await session.commit()
await session.refresh(row)
return LlmSettingOut.model_validate(row)
class ResetResponse(BaseModel):
reset: bool
detail: str = ""
@router.post("/settings/reset", response_model=ResetResponse)
async def reset_settings():
"""恢复默认提示词。"""
async with AsyncSessionLocal() as session:
row = (await session.execute(select(LlmSetting).where(LlmSetting.id == 1))).scalar_one_or_none()
defaults = get_default_prompts()
if row is None:
row = LlmSetting(id=1, **defaults)
session.add(row)
else:
row.format_prompt = defaults["format_prompt"]
row.classify_prompt = defaults["classify_prompt"]
row.commentary_prompt = defaults["commentary_prompt"]
row.image_prompt_template = defaults["image_prompt_template"]
await session.commit()
return ResetResponse(reset=True, detail="已恢复默认提示词")
class TestResponse(BaseModel):
ok: bool
detail: str = ""
configured: bool
@router.post("/settings/test", response_model=TestResponse)
async def test_connection():
"""最小测试:发一个 'hi' chat 请求,确认 key + 端点通。"""
async with AsyncSessionLocal() as session:
row = (await session.execute(select(LlmSetting).where(LlmSetting.id == 1))).scalar_one_or_none()
chat_model = row.chat_model if row else "agnes-2.0-flash"
client = LlmClient(chat_model=chat_model)
if not client.is_configured():
return TestResponse(ok=False, configured=False, detail="AGNES_API_KEY 未配置")
try:
reply = await client.chat(
system="你是测试助手,只用 1 个词回答 OK 或 FAIL。",
user="ping",
temperature=0.0,
max_tokens=10,
)
return TestResponse(ok=True, configured=True, detail=f"reply={reply[:50]!r}")
except Exception as e:
return TestResponse(ok=False, configured=True, detail=f"{type(e).__name__}: {e}")
class EnrichTriggerResponse(BaseModel):
triggered: bool
detail: str = ""
results: dict[str, str] | None = None
@router.post("/enrich/{article_id}", response_model=EnrichTriggerResponse)
async def trigger_enrich(article_id: int):
"""手动触发某篇的 4 项 LLM 增强(同步等待,不会丢在后台)。"""
from app.services.llm.enrichment import enrich_article
async with AsyncSessionLocal() as session:
row = (await session.execute(select(Article).where(Article.id == article_id))).scalar_one_or_none()
if not row:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Article not found")
try:
results = await enrich_article(article_id)
return EnrichTriggerResponse(triggered=True, detail=f"done for {article_id}", results=results)
except Exception as e:
logger.exception("manual enrich failed for %s", article_id)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, f"{type(e).__name__}: {e}")