mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
566 lines
26 KiB
Python
566 lines
26 KiB
Python
"""
|
|
LLM分析器模块
|
|
负责使用LLM进行话题分析、用户称号分析和金句分析
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from datetime import datetime
|
|
import asyncio
|
|
from typing import List, Dict, Tuple
|
|
from astrbot.api import logger
|
|
from ...src.models.data_models import SummaryTopic, UserTitle, GoldenQuote, TokenUsage
|
|
|
|
|
|
class LLMAnalyzer:
|
|
"""LLM分析器"""
|
|
|
|
def __init__(self, context, config_manager):
|
|
self.context = context
|
|
self.config_manager = config_manager
|
|
|
|
async def _call_provider_with_retry(self, provider, prompt: str, max_tokens: int, temperature: float, umo: str = None):
|
|
"""调用LLM提供者,带超时、重试与退避。支持自定义服务商。"""
|
|
timeout = self.config_manager.get_llm_timeout()
|
|
retries = self.config_manager.get_llm_retries()
|
|
backoff = self.config_manager.get_llm_backoff()
|
|
|
|
# 获取自定义服务商参数
|
|
custom_api_key = self.config_manager.get_custom_api_key()
|
|
custom_api_base = self.config_manager.get_custom_api_base_url()
|
|
custom_model = self.config_manager.get_custom_model_name()
|
|
|
|
last_exc = None
|
|
for attempt in range(1, retries + 1):
|
|
try:
|
|
if custom_api_key and custom_api_base and custom_model:
|
|
logger.info(f"使用自定义LLM提供商: {custom_api_base} model={custom_model}")
|
|
import aiohttp
|
|
async with aiohttp.ClientSession() as session:
|
|
headers = {
|
|
"Authorization": f"Bearer {custom_api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
payload = {
|
|
"model": custom_model,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"max_tokens": max_tokens,
|
|
"temperature": temperature
|
|
}
|
|
aio_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
async with session.post(custom_api_base, json=payload, headers=headers, timeout=aio_timeout) as resp:
|
|
if resp.status != 200:
|
|
error_text = await resp.text()
|
|
logger.error(f"自定义LLM服务商请求失败: HTTP {resp.status}, 内容: {error_text}")
|
|
try:
|
|
response_json = await resp.json()
|
|
except Exception as json_err:
|
|
error_text = await resp.text()
|
|
logger.error(f"自定义LLM服务商响应JSON解析失败: {json_err}, 内容: {error_text}")
|
|
return None
|
|
# 兼容 OpenAI 格式,安全访问嵌套字段
|
|
content = None
|
|
try:
|
|
choices = response_json.get("choices")
|
|
if choices and isinstance(choices, list) and len(choices) > 0:
|
|
message = choices[0].get("message")
|
|
if message and isinstance(message, dict):
|
|
content = message.get("content")
|
|
if content is None:
|
|
logger.error(f"自定义LLM响应格式异常: {response_json}")
|
|
return None
|
|
except Exception as key_err:
|
|
logger.error(f"自定义LLM响应结构解析失败: {key_err}, 响应内容: {response_json}")
|
|
return None
|
|
# 构造一个兼容原有逻辑的对象
|
|
class CustomResponse:
|
|
completion_text = content
|
|
raw_completion = response_json
|
|
return CustomResponse()
|
|
else:
|
|
# 确保使用当前指定的模型
|
|
if provider is None:
|
|
provider = self.context.get_using_provider(umo=umo)
|
|
provider_id = 'unknown'
|
|
if provider:
|
|
try:
|
|
meta = provider.meta()
|
|
provider_id = meta.id
|
|
except Exception as e:
|
|
logger.debug(f"获取提供商ID失败: {e}")
|
|
logger.info(f"获取到的 provider ID: {provider_id}")
|
|
if not provider or provider_id == 'unknown':
|
|
logger.warning(f"获取的提供商不正确 (Provider ID: {provider_id})")
|
|
|
|
|
|
logger.info(f"使用LLM provider: {provider}")
|
|
if not provider:
|
|
logger.error("provider 为空,无法调用 text_chat,直接返回 None")
|
|
return None
|
|
coro = provider.text_chat(prompt=prompt, max_tokens=max_tokens, temperature=temperature)
|
|
return await asyncio.wait_for(coro, timeout=timeout)
|
|
except asyncio.TimeoutError as e:
|
|
last_exc = e
|
|
logger.warning(f"LLM请求超时: 第{attempt}次, timeout={timeout}s")
|
|
except Exception as e:
|
|
last_exc = e
|
|
logger.warning(f"LLM请求失败: 第{attempt}次, 错误: {last_exc}")
|
|
# 若非最后一次,等待退避后重试
|
|
if attempt < retries:
|
|
await asyncio.sleep(backoff * attempt)
|
|
|
|
# 最终仍失败,记录错误并返回 None 由调用方处理降级,避免抛出异常
|
|
logger.error(f"LLM请求全部重试失败: {last_exc}")
|
|
return None
|
|
|
|
async def analyze_topics(self, messages: List[Dict], umo: str = None) -> Tuple[List[SummaryTopic], TokenUsage]:
|
|
"""使用LLM分析话题"""
|
|
try:
|
|
# 提取文本消息
|
|
text_messages = []
|
|
for msg in messages:
|
|
sender = msg.get("sender", {})
|
|
nickname = sender.get("nickname", "") or sender.get("card", "")
|
|
msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M")
|
|
|
|
for content in msg.get("message", []):
|
|
if content.get("type") == "text":
|
|
text = content.get("data", {}).get("text", "").strip()
|
|
if text and len(text) > 2 and not text.startswith(("/")):
|
|
text_messages.append({
|
|
"sender": nickname,
|
|
"time": msg_time,
|
|
"content": text
|
|
})
|
|
|
|
if not text_messages:
|
|
return [], TokenUsage()
|
|
|
|
# 构建LLM提示词,清理消息内容
|
|
def clean_message_content(content):
|
|
"""清理消息内容,移除可能影响JSON解析的字符"""
|
|
# 替换中文引号
|
|
content = content.replace('"', '"').replace('"', '"')
|
|
content = content.replace(''', "'").replace(''', "'")
|
|
# 移除或替换其他特殊字符
|
|
content = content.replace('\n', ' ').replace('\r', ' ')
|
|
content = content.replace('\t', ' ')
|
|
# 移除可能的控制字符
|
|
content = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', content)
|
|
return content.strip()
|
|
|
|
messages_text = "\n".join([
|
|
f"[{msg['time']}] {msg['sender']}: {clean_message_content(msg['content'])}"
|
|
for msg in text_messages
|
|
])
|
|
|
|
max_topics = self.config_manager.get_max_topics()
|
|
prompt = f"""
|
|
你是一个帮我进行群聊信息总结的助手,生成总结内容时,你需要严格遵守下面的几个准则:
|
|
请分析接下来提供的群聊记录,提取出最多{max_topics}个主要话题。
|
|
|
|
对于每个话题,请提供:
|
|
1. 话题名称(突出主题内容,尽量简明扼要)
|
|
2. 主要参与者(最多5人)
|
|
3. 话题详细描述(包含关键信息和结论)
|
|
|
|
注意:
|
|
- 对于比较有价值的点,稍微用一两句话详细讲讲,比如不要生成 "Nolan 和 SOV 讨论了 galgame 中关于性符号的衍生情况" 这种宽泛的内容,而是生成更加具体的讨论内容,让其他人只看这个消息就能知道讨论中有价值的,有营养的信息。
|
|
- 对于其中的部分信息,你需要特意提到主题施加的主体是谁,是哪个群友做了什么事情,而不要直接生成和群友没有关系的语句。
|
|
- 对于每一条总结,尽量讲清楚前因后果,以及话题的结论,是什么,为什么,怎么做,如果用户没有讲到细节,则可以不用这么做。
|
|
|
|
群聊记录:
|
|
{messages_text}
|
|
|
|
重要:必须返回标准JSON格式,严格遵守以下规则:
|
|
1. 只使用英文双引号 " 不要使用中文引号 " "
|
|
2. 字符串内容中的引号必须转义为 \"
|
|
3. 多个对象之间用逗号分隔
|
|
4. 数组元素之间用逗号分隔
|
|
5. 不要在JSON外添加任何文字说明
|
|
6. 描述内容避免使用特殊符号,用普通文字表达
|
|
|
|
请严格按照以下JSON格式返回,确保可以被标准JSON解析器解析:
|
|
[
|
|
{{
|
|
"topic": "话题名称",
|
|
"contributors": ["用户1", "用户2"],
|
|
"detail": "话题描述内容"
|
|
}},
|
|
{{
|
|
"topic": "另一个话题",
|
|
"contributors": ["用户3", "用户4"],
|
|
"detail": "另一个话题的描述"
|
|
}}
|
|
]
|
|
|
|
注意:返回的内容必须是纯JSON,不要包含markdown代码块标记或其他格式
|
|
"""
|
|
|
|
# 调用LLM
|
|
response = await self._call_provider_with_retry(None, prompt, max_tokens=10000, temperature=0.6, umo=umo)
|
|
if response is None:
|
|
logger.error("话题分析调用LLM失败: provider返回None(重试失败)")
|
|
return [], TokenUsage()
|
|
|
|
# 提取token使用统计
|
|
token_usage = TokenUsage()
|
|
# 安全地提取 usage,避免 response.raw_completion.usage 为 None 导致的 AttributeError
|
|
usage = None
|
|
if getattr(response, 'raw_completion', None) is not None:
|
|
usage = getattr(response.raw_completion, 'usage', None)
|
|
if usage:
|
|
token_usage.prompt_tokens = getattr(usage, 'prompt_tokens', 0) or 0
|
|
token_usage.completion_tokens = getattr(usage, 'completion_tokens', 0) or 0
|
|
token_usage.total_tokens = getattr(usage, 'total_tokens', 0) or 0
|
|
|
|
# 解析响应
|
|
if hasattr(response, 'completion_text'):
|
|
result_text = response.completion_text
|
|
else:
|
|
result_text = str(response)
|
|
|
|
# 尝试解析JSON
|
|
try:
|
|
# 提取JSON部分
|
|
json_match = re.search(r'\[.*?\]', result_text, re.DOTALL)
|
|
if json_match:
|
|
json_text = json_match.group()
|
|
logger.debug(f"话题分析JSON原文: {json_text[:500]}...")
|
|
|
|
# 强化JSON清理和修复
|
|
json_text = self._fix_json(json_text)
|
|
logger.debug(f"修复后的JSON: {json_text[:300]}...")
|
|
|
|
topics_data = json.loads(json_text)
|
|
topics = [SummaryTopic(**topic) for topic in topics_data[:max_topics]]
|
|
logger.info(f"话题分析成功,解析到 {len(topics)} 个话题")
|
|
return topics, token_usage
|
|
else:
|
|
logger.warning(f"话题分析响应中未找到JSON格式,响应内容: {result_text[:200]}...")
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"话题分析JSON解析失败: {e}")
|
|
logger.debug(f"修复后的JSON: {json_text if 'json_text' in locals() else 'N/A'}")
|
|
logger.debug(f"原始响应: {result_text}")
|
|
|
|
# 如果JSON解析失败,尝试用正则表达式提取话题信息
|
|
topics = self._extract_topics_with_regex(result_text, max_topics)
|
|
if topics:
|
|
logger.info(f"正则表达式提取成功,获得 {len(topics)} 个话题")
|
|
return topics, token_usage
|
|
else:
|
|
# 最后的降级方案
|
|
logger.info("正则表达式提取失败,使用默认话题...")
|
|
return [SummaryTopic(
|
|
topic="群聊讨论",
|
|
contributors=["群友"],
|
|
detail="今日群聊内容丰富,涵盖多个话题"
|
|
)], token_usage
|
|
|
|
return [], token_usage
|
|
|
|
except Exception as e:
|
|
logger.error(f"话题分析失败: {e}")
|
|
return [], TokenUsage()
|
|
|
|
def _fix_json(self, text: str) -> str:
|
|
"""修复JSON格式问题"""
|
|
# 移除markdown代码块标记
|
|
text = re.sub(r'```json\s*', '', text)
|
|
text = re.sub(r'```\s*$', '', text)
|
|
|
|
# 基础清理
|
|
text = text.replace('\n', ' ').replace('\r', ' ')
|
|
text = re.sub(r'\s+', ' ', text)
|
|
|
|
# 替换中文引号为英文引号
|
|
text = text.replace('"', '"').replace('"', '"')
|
|
text = text.replace(''', "'").replace(''', "'")
|
|
|
|
# 处理字符串内容中的特殊字符
|
|
# 转义字符串内的双引号
|
|
def escape_quotes_in_strings(match):
|
|
content = match.group(1)
|
|
# 转义内部的双引号
|
|
content = content.replace('"', '\\"')
|
|
return f'"{content}"'
|
|
|
|
# 先处理字段值中的引号
|
|
text = re.sub(r'"([^"]*(?:"[^"]*)*)"', escape_quotes_in_strings, text)
|
|
|
|
# 修复截断的JSON
|
|
if not text.endswith(']'):
|
|
last_complete = text.rfind('}')
|
|
if last_complete > 0:
|
|
text = text[:last_complete + 1] + ']'
|
|
|
|
# 修复常见的JSON格式问题
|
|
# 1. 修复缺失的逗号
|
|
text = re.sub(r'}\s*{', '}, {', text)
|
|
|
|
# 2. 确保字段名有引号
|
|
text = re.sub(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', text)
|
|
|
|
# 3. 移除多余的逗号
|
|
text = re.sub(r',\s*}', '}', text)
|
|
text = re.sub(r',\s*]', ']', text)
|
|
|
|
return text
|
|
|
|
def _extract_topics_with_regex(self, result_text: str, max_topics: int) -> List[SummaryTopic]:
|
|
"""使用正则表达式提取话题信息"""
|
|
try:
|
|
topics = []
|
|
|
|
# 更强的正则表达式提取话题信息,处理转义字符
|
|
# 匹配每个完整的话题对象
|
|
topic_pattern = r'\{\s*"topic":\s*"([^"]+)"\s*,\s*"contributors":\s*\[([^\]]+)\]\s*,\s*"detail":\s*"([^"]*(?:\\.[^"]*)*)"\s*\}'
|
|
matches = re.findall(topic_pattern, result_text, re.DOTALL)
|
|
|
|
if not matches:
|
|
# 尝试更宽松的匹配
|
|
topic_pattern = r'"topic":\s*"([^"]+)"[^}]*"contributors":\s*\[([^\]]+)\][^}]*"detail":\s*"([^"]*(?:\\.[^"]*)*)"'
|
|
matches = re.findall(topic_pattern, result_text, re.DOTALL)
|
|
|
|
for match in matches[:max_topics]:
|
|
topic_name = match[0].strip()
|
|
contributors_str = match[1].strip()
|
|
detail = match[2].strip()
|
|
|
|
# 清理detail中的转义字符
|
|
detail = detail.replace('\\"', '"').replace('\\n', ' ').replace('\\t', ' ')
|
|
|
|
# 解析参与者列表
|
|
contributors = []
|
|
for contrib in re.findall(r'"([^"]+)"', contributors_str):
|
|
contributors.append(contrib.strip())
|
|
|
|
if not contributors:
|
|
contributors = ["群友"]
|
|
|
|
topics.append(SummaryTopic(
|
|
topic=topic_name,
|
|
contributors=contributors[:5], # 最多5个参与者
|
|
detail=detail
|
|
))
|
|
|
|
return topics
|
|
except Exception as e:
|
|
logger.error(f"正则表达式提取失败: {e}")
|
|
return []
|
|
|
|
async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[UserTitle], TokenUsage]:
|
|
"""使用LLM分析用户称号"""
|
|
try:
|
|
# 准备用户数据
|
|
user_summaries = []
|
|
for user_id, stats in user_analysis.items():
|
|
if stats["message_count"] < 5: # 过滤活跃度太低的用户
|
|
continue
|
|
|
|
# 分析用户特征
|
|
night_messages = sum(stats["hours"][h] for h in range(0, 6))
|
|
day_messages = stats["message_count"] - night_messages
|
|
avg_chars = stats["char_count"] / stats["message_count"] if stats["message_count"] > 0 else 0
|
|
|
|
user_summaries.append({
|
|
"name": stats["nickname"],
|
|
"qq": int(user_id),
|
|
"message_count": stats["message_count"],
|
|
"avg_chars": round(avg_chars, 1),
|
|
"emoji_ratio": round(stats["emoji_count"] / stats["message_count"], 2),
|
|
"night_ratio": round(night_messages / stats["message_count"], 2),
|
|
"reply_ratio": round(stats["reply_count"] / stats["message_count"], 2)
|
|
})
|
|
|
|
if not user_summaries:
|
|
return [], TokenUsage()
|
|
|
|
# 按消息数量排序,取前N名
|
|
max_user_titles = self.config_manager.get_max_user_titles()
|
|
user_summaries.sort(key=lambda x: x["message_count"], reverse=True)
|
|
user_summaries = user_summaries[:max_user_titles]
|
|
|
|
# 构建LLM提示词
|
|
users_text = "\n".join([
|
|
f"- {user['name']} (QQ:{user['qq']}): "
|
|
f"发言{user['message_count']}条, 平均{user['avg_chars']}字, "
|
|
f"表情比例{user['emoji_ratio']}, 夜间发言比例{user['night_ratio']}, "
|
|
f"回复比例{user['reply_ratio']}"
|
|
for user in user_summaries
|
|
])
|
|
|
|
prompt = f"""
|
|
请为以下群友分配合适的称号和MBTI类型。每个人只能有一个称号,每个称号只能给一个人。
|
|
|
|
可选称号:
|
|
- 龙王: 发言频繁但内容轻松的人
|
|
- 技术专家: 经常讨论技术话题的人
|
|
- 夜猫子: 经常在深夜发言的人
|
|
- 表情包军火库: 经常发表情的人
|
|
- 沉默终结者: 经常开启话题的人
|
|
- 评论家: 平均发言长度很长的人
|
|
- 阳角: 在群里很有影响力的人
|
|
- 互动达人: 经常回复别人的人
|
|
- ... (你可以自行进行拓展添加)
|
|
|
|
用户数据:
|
|
{users_text}
|
|
|
|
请以JSON格式返回,格式如下:
|
|
[
|
|
{{
|
|
"name": "用户名",
|
|
"qq": 123456789,
|
|
"title": "称号",
|
|
"mbti": "MBTI类型",
|
|
"reason": "获得此称号的原因"
|
|
}}
|
|
]
|
|
"""
|
|
|
|
# 调用LLM
|
|
response = await self._call_provider_with_retry(None, prompt, max_tokens=1500, temperature=0.5, umo=umo)
|
|
if response is None:
|
|
logger.error("用户称号分析调用LLM失败: provider返回None(重试失败)")
|
|
return [], TokenUsage()
|
|
|
|
# 提取token使用统计
|
|
token_usage = TokenUsage()
|
|
# 安全地提取 usage,避免 response.raw_completion.usage 为 None 导致的 AttributeError
|
|
usage = None
|
|
if getattr(response, 'raw_completion', None) is not None:
|
|
usage = getattr(response.raw_completion, 'usage', None)
|
|
if usage:
|
|
token_usage.prompt_tokens = getattr(usage, 'prompt_tokens', 0) or 0
|
|
token_usage.completion_tokens = getattr(usage, 'completion_tokens', 0) or 0
|
|
token_usage.total_tokens = getattr(usage, 'total_tokens', 0) or 0
|
|
|
|
# 解析响应
|
|
if hasattr(response, 'completion_text'):
|
|
result_text = response.completion_text
|
|
else:
|
|
result_text = str(response)
|
|
|
|
# debug日志:打印原始响应
|
|
logger.debug(f"用户称号分析原始响应: {result_text[:500]}...")
|
|
|
|
# 尝试解析JSON
|
|
try:
|
|
json_match = re.search(r'\[.*\]', result_text, re.DOTALL)
|
|
if json_match:
|
|
logger.debug(f"用户称号分析JSON原文: {json_match.group()[:500]}...")
|
|
titles_data = json.loads(json_match.group())
|
|
return [UserTitle(**title) for title in titles_data], token_usage
|
|
except Exception as e:
|
|
logger.error(f"用户称号分析JSON解析失败: {e}")
|
|
logger.debug(f"原始响应: {result_text}")
|
|
|
|
return [], token_usage
|
|
|
|
except Exception as e:
|
|
logger.error(f"用户称号分析失败: {e}")
|
|
return [], TokenUsage()
|
|
|
|
async def analyze_golden_quotes(self, messages: List[Dict], umo: str = None) -> Tuple[List[GoldenQuote], TokenUsage]:
|
|
"""使用LLM分析群聊金句"""
|
|
try:
|
|
# 提取有趣的文本消息
|
|
interesting_messages = []
|
|
for msg in messages:
|
|
sender = msg.get("sender", {})
|
|
nickname = sender.get("nickname", "") or sender.get("card", "")
|
|
msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M")
|
|
|
|
for content in msg.get("message", []):
|
|
if content.get("type") == "text":
|
|
text = content.get("data", {}).get("text", "").strip()
|
|
# 过滤长度适中、可能有趣的消息
|
|
if 5 <= len(text) <= 100 and not text.startswith(("http", "www", "/")):
|
|
interesting_messages.append({
|
|
"sender": nickname,
|
|
"time": msg_time,
|
|
"content": text
|
|
})
|
|
|
|
if not interesting_messages:
|
|
return [], TokenUsage()
|
|
|
|
# 构建LLM提示词
|
|
messages_text = "\n".join([
|
|
f"[{msg['time']}] {msg['sender']}: {msg['content']}"
|
|
for msg in interesting_messages
|
|
])
|
|
|
|
# 计算金句数量
|
|
max_golden_quotes = self.config_manager.get_max_golden_quotes()
|
|
|
|
prompt = f"""
|
|
请从以下群聊记录中挑选出{max_golden_quotes}句最具冲击力、最令人惊叹的"金句"。这些金句需满足:
|
|
- 核心标准:**逆天的神人发言**,即具备颠覆常识的脑洞、逻辑跳脱的表达或强烈反差感的原创内容
|
|
- 典型特征:包含某些争议话题元素、夸张类比、反常规结论、一本正经的"胡说八道"或突破语境的清奇思路,并且具备一定的冲击力,让人印象深刻。
|
|
|
|
对于每个金句,请提供:
|
|
1. 原文内容(完整保留发言细节)
|
|
2. 发言人昵称
|
|
3. 选择理由(具体说明其"逆天"之处,如逻辑颠覆点/脑洞角度/反差感/争议话题元素)
|
|
|
|
此外,我将对你进行严格约束:
|
|
- 优先筛选 **逆天指数最高** 的内容:发情、性压抑话题 > 争议话题 > 元素级 > 颠覆认知级 > 逻辑跳脱级 > 趣味调侃级,剔除单纯玩梗或网络热词堆砌的普通发言
|
|
- 重点标记包含极端类比、反常识论证或无厘头结论的内容,并且包含一定的争议话题元素。
|
|
|
|
群聊记录:
|
|
{messages_text}
|
|
|
|
请以JSON格式返回,格式如下:
|
|
[
|
|
{{
|
|
"content": "金句原文",
|
|
"sender": "发言人昵称",
|
|
"reason": "选择这句话的理由(需明确说明逆天特质)"
|
|
}}
|
|
]
|
|
"""
|
|
|
|
# 调用LLM
|
|
response = await self._call_provider_with_retry(None, prompt, max_tokens=1500, temperature=0.7, umo=umo)
|
|
if response is None:
|
|
logger.error("金句分析调用LLM失败: provider返回None(重试失败)")
|
|
return [], TokenUsage()
|
|
|
|
# 提取token使用统计
|
|
token_usage = TokenUsage()
|
|
# 安全地提取 usage,避免 response.raw_completion.usage 为 None 导致的 AttributeError
|
|
usage = None
|
|
if getattr(response, 'raw_completion', None) is not None:
|
|
usage = getattr(response.raw_completion, 'usage', None)
|
|
if usage:
|
|
token_usage.prompt_tokens = getattr(usage, 'prompt_tokens', 0) or 0
|
|
token_usage.completion_tokens = getattr(usage, 'completion_tokens', 0) or 0
|
|
token_usage.total_tokens = getattr(usage, 'total_tokens', 0) or 0
|
|
|
|
# 解析响应
|
|
if hasattr(response, 'completion_text'):
|
|
result_text = response.completion_text
|
|
else:
|
|
result_text = str(response)
|
|
|
|
# debug日志:打印原始响应
|
|
logger.debug(f"金句分析原始响应: {result_text[:500]}...")
|
|
|
|
# 尝试解析JSON
|
|
try:
|
|
json_match = re.search(r'\[.*\]', result_text, re.DOTALL)
|
|
if json_match:
|
|
logger.debug(f"金句分析JSON原文: {json_match.group()[:500]}...")
|
|
quotes_data = json.loads(json_match.group())
|
|
return [GoldenQuote(**quote) for quote in quotes_data[:max_golden_quotes]], token_usage
|
|
except Exception as e:
|
|
logger.error(f"金句分析JSON解析失败: {e}")
|
|
logger.debug(f"原始响应: {result_text}")
|
|
|
|
return [], token_usage
|
|
|
|
except Exception as e:
|
|
logger.error(f"金句分析失败: {e}")
|
|
return [], TokenUsage() |