mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-19 20:47:47 +00:00
- 新增影视解说专属提示词模块,覆盖剧情分析、文案生成、片段规划、脚本匹配与修复全流程 - 注册影视解说模块到全局提示词系统,更新初始化加载逻辑 - 重构Tavily搜索服务,拆分通用搜索函数适配短剧和影视两类作品 - 更新WebUI界面,新增影视解说配置项、多语言翻译与版本号展示 - 升级项目版本号从0.7.9到0.8.1 - 调整LLM服务与适配器逻辑,支持自定义prompt分类适配不同解说类型 - 完善相关工具类与单元测试,覆盖影视解说场景调用流程
138 lines
4.0 KiB
Python
138 lines
4.0 KiB
Python
"""Tavily-powered web search helpers for plot analysis."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
import requests
|
|
from loguru import logger
|
|
|
|
|
|
TAVILY_API_BASE_URL = "https://api.tavily.com"
|
|
DEFAULT_SEARCH_DEPTH = "basic"
|
|
DEFAULT_MAX_RESULTS = 5
|
|
DEFAULT_TIMEOUT = 20
|
|
|
|
|
|
class TavilySearchError(RuntimeError):
|
|
"""Raised when Tavily search cannot be completed."""
|
|
|
|
|
|
def _trim_text(value: Any, max_chars: int) -> str:
|
|
text = str(value or "").strip()
|
|
if len(text) <= max_chars:
|
|
return text
|
|
return f"{text[:max_chars].rstrip()}..."
|
|
|
|
|
|
def search_short_drama(
|
|
short_name: str,
|
|
api_key: str | None = None,
|
|
*,
|
|
search_depth: str = DEFAULT_SEARCH_DEPTH,
|
|
max_results: int = DEFAULT_MAX_RESULTS,
|
|
timeout: int = DEFAULT_TIMEOUT,
|
|
) -> dict[str, Any]:
|
|
"""Search web context for a short drama name with Tavily."""
|
|
return search_story_context(
|
|
short_name,
|
|
api_key,
|
|
search_keywords="短剧 剧情 介绍 人物 结局",
|
|
empty_name_message="短剧名称不能为空",
|
|
search_depth=search_depth,
|
|
max_results=max_results,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def search_story_context(
|
|
title: str,
|
|
api_key: str | None = None,
|
|
*,
|
|
search_keywords: str = "剧情 介绍 人物 结局",
|
|
empty_name_message: str = "作品名称不能为空",
|
|
search_depth: str = DEFAULT_SEARCH_DEPTH,
|
|
max_results: int = DEFAULT_MAX_RESULTS,
|
|
timeout: int = DEFAULT_TIMEOUT,
|
|
) -> dict[str, Any]:
|
|
"""Search web context for a story title with Tavily."""
|
|
title = str(title or "").strip()
|
|
if not title:
|
|
raise TavilySearchError(empty_name_message)
|
|
|
|
api_key = (api_key or os.getenv("TAVILY_API_KEY") or "").strip()
|
|
if not api_key:
|
|
raise TavilySearchError("Tavily API Key 未配置")
|
|
|
|
query = f"{title} {search_keywords}".strip()
|
|
payload = {
|
|
"query": query,
|
|
"search_depth": search_depth or DEFAULT_SEARCH_DEPTH,
|
|
"topic": "general",
|
|
"max_results": max(1, min(int(max_results or DEFAULT_MAX_RESULTS), 10)),
|
|
"include_answer": True,
|
|
"include_raw_content": False,
|
|
"include_images": False,
|
|
}
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{TAVILY_API_BASE_URL}/search",
|
|
headers={
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
json=payload,
|
|
timeout=timeout,
|
|
)
|
|
except requests.RequestException as exc:
|
|
raise TavilySearchError(f"Tavily 请求失败: {exc}") from exc
|
|
|
|
if response.status_code >= 400:
|
|
message = _trim_text(response.text, 500)
|
|
raise TavilySearchError(f"Tavily 请求失败: HTTP {response.status_code} {message}")
|
|
|
|
try:
|
|
data = response.json()
|
|
except ValueError as exc:
|
|
raise TavilySearchError("Tavily 返回内容不是有效 JSON") from exc
|
|
|
|
logger.info(
|
|
"Tavily 剧情检索完成: query={}, results={}",
|
|
query,
|
|
len(data.get("results") or []),
|
|
)
|
|
return data
|
|
|
|
def format_search_context(search_data: dict[str, Any], *, max_chars: int = 6000) -> str:
|
|
"""Format Tavily response into compact LLM context."""
|
|
if not search_data:
|
|
return ""
|
|
|
|
lines = [
|
|
"# Tavily 联网检索结果",
|
|
f"检索 query: {search_data.get('query', '')}",
|
|
]
|
|
|
|
answer = _trim_text(search_data.get("answer"), 1200)
|
|
if answer:
|
|
lines.extend(["", "## 综合回答", answer])
|
|
|
|
results = search_data.get("results") or []
|
|
if results:
|
|
lines.extend(["", "## 搜索来源"])
|
|
for index, result in enumerate(results, start=1):
|
|
title = _trim_text(result.get("title"), 120)
|
|
url = _trim_text(result.get("url"), 240)
|
|
content = _trim_text(result.get("content") or result.get("raw_content"), 700)
|
|
lines.extend(
|
|
[
|
|
f"{index}. 标题: {title}",
|
|
f" 来源: {url}",
|
|
f" 摘要: {content}",
|
|
]
|
|
)
|
|
|
|
return _trim_text("\n".join(lines).strip(), max_chars)
|