NarratoAI/app/services/tavily_search.py
viccy 342fc15f3b feat(tts,search,video): 新增OmniVoice TTS、联网搜索与多视频剪辑支持
新增OmniVoice语音合成引擎全流程支持,包含配置项、WebUI界面与服务实现
集成Tavily联网搜索能力,支持短剧剧情分析前自动检索剧情背景信息
新增多视频源剪辑支持,完善脚本校验规则并重构剪辑逻辑适配多视频路径
重构LLM剧情分析Prompt,优化输出格式适配多场景与联网检索结果
调整streamlit版本至1.56.0修复兼容性问题
新增相关单元测试与多语言翻译,更新配置示例文件
2026-06-07 01:24:32 +08:00

117 lines
3.4 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."""
short_name = str(short_name or "").strip()
if not short_name:
raise TavilySearchError("短剧名称不能为空")
api_key = (api_key or os.getenv("TAVILY_API_KEY") or "").strip()
if not api_key:
raise TavilySearchError("Tavily API Key 未配置")
query = f"{short_name} 短剧 剧情 介绍 人物 结局"
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)