Files
joke/api/app/routers/generate.py
T
bwstudio db8cea02f8 feat: 提示词和 AI 参数移至数据库管理
- AiSetting 模型新增 6 个提示词字段 + 3 个阶段温度字段
- main.py 迁移逻辑改为检测 ai_settings 表的新字段并填充默认值
- generate.py 从数据库读取生成提示词,去掉硬编码
- optimizer 从 API 读取各阶段提示词和温度,删除 prompts.py
- crawler 从 API 读取提取/改写提示词和温度,删除 prompts.py
- settings/active 端点去掉 token 认证(供爬虫/优化器使用)
- 后台设置页新增提示词编辑区和温度调节控件
- 新增 _ensure_default_settings 自动创建默认配置
2026-06-13 17:27:09 +08:00

187 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""AI 笑话生成器 API — 提示词从数据库读取"""
import json
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from openai import OpenAI
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.setting import AiSetting
from app.schemas.joke import GenerateRequest, GenerateResponse
router = APIRouter(prefix="/generate", tags=["生成器"])
STYLE_MAP = {
"cold": "冷幽默 / 无厘头",
"warm": "温馨幽默 / 暖心搞笑",
"twist": "反转 / 神转折",
"pun": "谐音梗 / 文字游戏",
"sketch": "段子 / 吐槽调侃",
"irony": "讽刺幽默 / 黑色幽默",
}
LENGTH_MAP = {
"short": "30-80字,非常简短",
"medium": "80-150字,正常长度",
"long": "150-300字,可以描述一个小场景",
}
def _load_prompt(setting: AiSetting) -> str:
"""从数据库读取生成提示词,没有则返回默认值"""
prompt = setting.generate_prompt
if not prompt or not prompt.strip():
prompt = GENERATE_PROMPT_DEFAULT
return prompt
GENERATE_PROMPT_DEFAULT = """你是一位幽默大师,专门创作轻松搞笑的短笑话。
{context}
要求:
1. 根据场景和关键词创作一条原创笑话
2. 笑话要有反转或意外结局
3. 语言风格:{style}
4. 字数要求:{length_requirement}
5. 直接输出笑话内容,不需要解释
请严格按照以下 JSON 格式输出,不要加任何额外说明:
{{
"title": "笑话标题",
"content": "笑话正文",
"score": 8,
"reason": "这个笑话巧妙结合了场景和关键词,结尾有反转"
}}
score 是 1-10 的整数评分,reason 是用一句话说明亮点。"""
def _build_prompt(
scenarios: list[str],
keywords: list[str],
style: str = "twist",
length: str = "medium",
setting: AiSetting | None = None,
) -> str:
"""构建 AI prompt,支持风格和长度控制"""
parts = []
if scenarios:
parts.append(f"场景:{', '.join(scenarios)}")
if keywords:
parts.append(f"关键词:{', '.join(keywords)}")
if not parts:
parts.append("场景:日常生活的各种趣事(不指定具体场景)")
prompt_template = _load_prompt(setting) if setting else GENERATE_PROMPT_DEFAULT
return prompt_template.format(
context="\n".join(parts),
style=STYLE_MAP.get(style, "反转 / 神转折"),
length_requirement=LENGTH_MAP.get(length, "80-150字,正常长度"),
)
@router.post("", response_model=GenerateResponse)
def generate_joke(
req: GenerateRequest,
db: Session = Depends(get_db),
):
"""调用 AI 生成笑话,提示词从数据库读取"""
# 获取激活的 AI 配置
setting = db.query(AiSetting).filter(AiSetting.is_active == True).first()
if not setting:
setting = db.query(AiSetting).first()
if not setting or not setting.api_key:
raise HTTPException(status_code=503, detail="AI 服务未配置,请联系管理员")
try:
client = OpenAI(base_url=setting.api_base, api_key=setting.api_key)
prompt = _build_prompt(req.scenarios, req.keywords, req.style, req.length, setting)
response = client.chat.completions.create(
model=setting.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=req.temperature,
max_tokens=setting.max_tokens,
)
raw = response.choices[0].message.content
return _parse_json_response(raw)
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="AI 返回格式异常,请重新生成")
except Exception as e:
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
def _parse_json_response(raw: str | None) -> GenerateResponse:
"""解析 AI 返回的 JSON 格式"""
if not raw or not raw.strip():
return GenerateResponse(title="生成的笑话", content="(内容生成失败,请重新生成)", score=0)
raw = raw.strip()
if raw.startswith("```"):
lines = raw.split("\n")
if len(lines) >= 3:
raw = "\n".join(lines[1:-1]).strip()
if raw.endswith(","):
raw = raw[:-1]
try:
data = json.loads(raw)
except json.JSONDecodeError:
try:
start = raw.index("{")
end = raw.rindex("}") + 1
raw = raw[start:end]
data = json.loads(raw)
except (ValueError, json.JSONDecodeError):
return _parse_response(raw)
return GenerateResponse(
title=data.get("title", "生成的笑话"),
content=data.get("content", ""),
score=data.get("score", 0),
reason=data.get("reason", ""),
created_at=datetime.now(),
)
def _parse_response(raw: str | None) -> GenerateResponse:
"""解析 AI 返回内容(非 JSON 回退)"""
if not raw or not raw.strip():
raise ValueError("AI 返回内容为空")
title = ""
content = raw
for line in raw.split("\n"):
line = line.strip()
if line.startswith("标题:") or line.startswith("标题:"):
title = line.split("", 1)[-1].split(":", 1)[-1].strip()
lines = content.split("\n")
for i, l in enumerate(lines):
if l.strip() == line:
lines[i] = ""
break
content = "\n".join(lines).strip()
break
if not title:
first_line = raw.split("\n")[0].strip()
if first_line.startswith("标题"):
first_line = first_line.split("", 1)[-1].split(":", 1)[-1].strip()
title = first_line[:30] if len(first_line) > 30 else first_line
if not content.strip():
content = "(内容生成失败,请重新生成)"
return GenerateResponse(
title=title or "生成的笑话",
content=content.strip(),
created_at=datetime.now(),
)