2026-06-08 14:24:00 +08:00
|
|
|
"""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"],
|
2026-06-09 14:35:54 +08:00
|
|
|
blocklist_tags=[],
|
2026-06-08 14:24:00 +08:00
|
|
|
)
|
|
|
|
|
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}")
|