Files

160 lines
5.0 KiB
Python
Raw Permalink 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.
"""
爽文开书优化(fiction.open_book)— LLM 调用逻辑。
"""
from __future__ import annotations
import json
import logging
import re
from typing import Any, Dict, List, Optional
from langchain_core.messages import HumanMessage, SystemMessage
from models.fiction_models import (
EmotionFlow,
FictionGuideWorldbook,
OpenBookResult,
)
from services.fiction_prompt_utils import resolve_prompt
from services.fiction_service import fiction_service
from services.studio_step_respond import resolve_api_config
from utils.llm_client import LLMClient
logger = logging.getLogger(__name__)
_llm_client = LLMClient()
_JSON_FENCE = re.compile(r"```(?:json)?\s*([\s\S]*?)```", re.IGNORECASE)
def _extract_json(raw: str) -> Dict[str, Any]:
text = (raw or "").strip()
if not text:
raise ValueError("模型返回为空")
fence = _JSON_FENCE.search(text)
if fence:
text = fence.group(1).strip()
try:
return json.loads(text)
except json.JSONDecodeError:
start = text.find("{")
end = text.rfind("}")
if start >= 0 and end > start:
return json.loads(text[start : end + 1])
raise ValueError("无法解析模型返回的 JSON")
def _validate_api_config(api_config: Dict[str, str]) -> None:
if not api_config.get("api_key"):
raise ValueError("API Key 未配置,请先在 API 配置页面保存密钥")
if not api_config.get("api_url"):
raise ValueError("API 地址未配置,请先在 API 配置页面保存 mainLLM")
def _format_catalog_for_prompt(flows: List[EmotionFlow]) -> str:
lines: List[str] = []
for flow in flows:
tags = "、".join(flow.tags or [])
lines.append(f"- id: {flow.id}\n intro: {flow.intro}\n tags: {tags}")
return "\n".join(lines) if lines else "(无可用情绪流)"
def _format_guide_global_for_prompt() -> str:
entries = fiction_service.get_guide_global_entries().entries
lines: List[str] = []
for entry in entries:
lines.append(f"[{entry.layer}] {entry.title}\n{entry.content}")
return "\n\n".join(lines) if lines else "(无全局指南)"
def _build_open_book_messages(inspiration: str) -> List[Any]:
default_settings = fiction_service.get_default_settings()
system_prompt = resolve_prompt("openBook", default_settings.prompts.openBook)
catalog = fiction_service.get_emotion_catalog()
catalog_text = _format_catalog_for_prompt(catalog.flows)
guide_global_text = _format_guide_global_for_prompt()
user_content = f"""## 全局创作指南(L0–L3)
{guide_global_text}
## 可选情绪流 catalog
{catalog_text}
## 用户创作灵感
{inspiration.strip()}
请根据以上信息优化开书方案。"""
return [
SystemMessage(content=system_prompt),
HumanMessage(content=user_content),
]
def _normalize_flow_ids(raw_ids: Any, valid_ids: set[str]) -> List[str]:
if not isinstance(raw_ids, list):
return []
result: List[str] = []
for item in raw_ids:
fid = str(item).strip()
if fid in valid_ids and fid not in result:
result.append(fid)
return result
def _parse_open_book_response(
data: Dict[str, Any], valid_flow_ids: set[str]
) -> OpenBookResult:
guide_raw = data.get("guide") or {}
guide = FictionGuideWorldbook(
persona=str(guide_raw.get("persona") or "").strip(),
highlight=str(guide_raw.get("highlight") or "").strip(),
experience=str(guide_raw.get("experience") or "").strip(),
forbiddenZones=str(guide_raw.get("forbiddenZones") or "").strip(),
)
allowed = _normalize_flow_ids(data.get("allowedFlowIds"), valid_flow_ids)
if not allowed and valid_flow_ids:
allowed = [next(iter(valid_flow_ids))]
title = str(data.get("title") or "未命名作品").strip() or "未命名作品"
optimized_intro = str(data.get("optimizedIntro") or "").strip()
return OpenBookResult(
title=title,
optimizedIntro=optimized_intro,
guide=guide,
allowedFlowIds=allowed,
)
async def run_open_book(
inspiration: str,
*,
profile_id: Optional[str] = None,
api_config: Optional[Dict[str, str]] = None,
) -> OpenBookResult:
inspiration = (inspiration or "").strip()
if not inspiration:
raise ValueError("创作灵感不能为空")
resolved = resolve_api_config(profile_id, api_config)
_validate_api_config(resolved)
catalog = fiction_service.get_emotion_catalog()
valid_flow_ids = {f.id for f in catalog.flows}
messages = _build_open_book_messages(inspiration)
response = await _llm_client.chat_completion(
messages=messages,
api_url=resolved["api_url"],
api_key=resolved["api_key"],
model=resolved.get("model", "gpt-4o-mini"),
temperature=0.7,
max_tokens=8000,
request_timeout=120,
stream=False,
)
content = response["choices"][0]["message"]["content"]
data = _extract_json(content)
return _parse_open_book_response(data, valid_flow_ids)