182 lines
6.0 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对话日志记录模块
用于记录大模型对话的输入和输出信息,方便排查问题
"""
import os
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Any
from threading import Lock
class AILogger:
"""AI对话日志记录器"""
def __init__(self, log_dir: Optional[str] = None):
"""
初始化日志记录器
Args:
log_dir: 日志文件保存目录,默认为项目根目录下的 logs/ai_conversations 目录
"""
if log_dir is None:
# 默认日志目录:项目根目录下的 logs/ai_conversations
project_root = Path(__file__).parent.parent
log_dir = project_root / "logs" / "ai_conversations"
self.log_dir = Path(log_dir)
self.log_dir.mkdir(parents=True, exist_ok=True)
# 线程锁,确保日志写入的线程安全
self._lock = Lock()
# 是否启用日志记录(可通过环境变量控制)
self.enabled = os.getenv('AI_LOG_ENABLED', 'true').lower() == 'true'
print(f"[AI日志] 日志记录器初始化完成,日志目录: {self.log_dir}")
print(f"[AI日志] 日志记录状态: {'启用' if self.enabled else '禁用'}")
def log_conversation(
self,
prompt: str,
api_request: Dict[str, Any],
api_response: Optional[Dict[str, Any]] = None,
extracted_data: Optional[Dict[str, Any]] = None,
error: Optional[str] = None,
session_id: Optional[str] = None
) -> str:
"""
记录一次完整的AI对话
Args:
prompt: 输入提示词
api_request: API请求参数
api_response: API响应内容完整响应
extracted_data: 提取后的结构化数据
error: 错误信息(如果有)
session_id: 会话ID可选用于关联多次对话
Returns:
日志文件路径
"""
if not self.enabled:
return ""
try:
with self._lock:
# 生成时间戳和会话ID
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # 精确到毫秒
if session_id is None:
session_id = f"session_{int(time.time() * 1000)}"
# 创建日志记录
log_entry = {
"timestamp": datetime.now().isoformat(),
"session_id": session_id,
"prompt": prompt,
"api_request": {
"endpoint": api_request.get("endpoint", "unknown"),
"model": api_request.get("model", "unknown"),
"messages": api_request.get("messages", []),
"temperature": api_request.get("temperature"),
"max_tokens": api_request.get("max_tokens"),
"enable_thinking": api_request.get("enable_thinking", False),
},
"api_response": api_response,
"extracted_data": extracted_data,
"error": error,
"success": error is None
}
# 保存到文件(按日期组织)
date_str = datetime.now().strftime("%Y%m%d")
log_file = self.log_dir / f"conversation_{date_str}_{timestamp}.json"
with open(log_file, 'w', encoding='utf-8') as f:
json.dump(log_entry, f, ensure_ascii=False, indent=2)
print(f"[AI日志] 对话日志已保存: {log_file.name}")
return str(log_file)
except Exception as e:
print(f"[AI日志] 保存日志失败: {e}")
return ""
def log_request_only(
self,
prompt: str,
api_request: Dict[str, Any],
session_id: Optional[str] = None
) -> str:
"""
仅记录请求信息(在发送请求前调用)
Args:
prompt: 输入提示词
api_request: API请求参数
session_id: 会话ID
Returns:
日志文件路径
"""
return self.log_conversation(
prompt=prompt,
api_request=api_request,
session_id=session_id
)
def get_recent_logs(self, limit: int = 10) -> list:
"""
获取最近的日志文件列表
Args:
limit: 返回的日志文件数量
Returns:
日志文件路径列表(按时间倒序)
"""
try:
log_files = sorted(
self.log_dir.glob("conversation_*.json"),
key=lambda x: x.stat().st_mtime,
reverse=True
)
return [str(f) for f in log_files[:limit]]
except Exception as e:
print(f"[AI日志] 获取日志列表失败: {e}")
return []
def read_log(self, log_file: str) -> Optional[Dict]:
"""
读取指定的日志文件
Args:
log_file: 日志文件路径
Returns:
日志内容字典如果读取失败返回None
"""
try:
log_path = Path(log_file)
if not log_path.is_absolute():
log_path = self.log_dir / log_file
with open(log_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"[AI日志] 读取日志文件失败: {e}")
return None
# 全局日志记录器实例
_ai_logger: Optional[AILogger] = None
def get_ai_logger() -> AILogger:
"""获取全局AI日志记录器实例"""
global _ai_logger
if _ai_logger is None:
_ai_logger = AILogger()
return _ai_logger