chore(logging): tune title diagnostics and file logs

This commit is contained in:
JOJO 2026-05-25 22:57:29 +08:00
parent eaf270766f
commit 1e83bc9f68
3 changed files with 137 additions and 18 deletions

View File

@ -443,12 +443,32 @@ def start_background_jobs():
_load_last_active_cache() _load_last_active_cache()
socketio.start_background_task(idle_reaper_loop) socketio.start_background_task(idle_reaper_loop)
TITLE_DEBUG_DIR = Path(LOGS_DIR).expanduser().resolve() / "title_debug"
TITLE_DEBUG_FILE = TITLE_DEBUG_DIR / "title_generation.log"
def _title_debug_log(message: str, **extra: Any) -> None:
try:
TITLE_DEBUG_DIR.mkdir(parents=True, exist_ok=True)
payload = {
"ts": datetime.now().isoformat(timespec="milliseconds"),
"message": str(message),
}
if extra:
payload["extra"] = extra
with TITLE_DEBUG_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
except Exception:
pass
async def _generate_title_async(user_message: str) -> Optional[str]: async def _generate_title_async(user_message: str) -> Optional[str]:
"""使用快速模型生成对话标题。""" """使用快速模型生成对话标题。"""
if not user_message: if not user_message:
_title_debug_log("skip_empty_user_message")
return None return None
client = DeepSeekClient(thinking_mode=False, web_mode=True) client = DeepSeekClient(thinking_mode=False, web_mode=True)
_title_debug_log("start_generate_title", user_message_preview=str(user_message)[:200], user_message_len=len(str(user_message)))
try: try:
prompt_text = TITLE_PROMPT_PATH.read_text(encoding="utf-8") prompt_text = TITLE_PROMPT_PATH.read_text(encoding="utf-8")
except Exception: except Exception:
@ -466,11 +486,17 @@ async def _generate_title_async(user_message: str) -> Optional[str]:
try: try:
content = resp.get("choices", [{}])[0].get("message", {}).get("content") content = resp.get("choices", [{}])[0].get("message", {}).get("content")
if content: if content:
return " ".join(str(content).strip().split()) normalized = " ".join(str(content).strip().split())
_title_debug_log("title_api_success", title_preview=normalized[:200], title_len=len(normalized))
return normalized
_title_debug_log("title_api_empty_content", resp_preview=str(resp)[:500])
except Exception: except Exception:
_title_debug_log("title_api_parse_error", resp_preview=str(resp)[:500])
continue continue
except Exception as exc: except Exception as exc:
debug_log(f"[TitleGen] 生成标题异常: {exc}") debug_log(f"[TitleGen] 生成标题异常: {exc}")
_title_debug_log("title_api_exception", error=str(exc))
_title_debug_log("title_api_no_result")
return None return None
@ -482,6 +508,7 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati
async def _runner(): async def _runner():
title = await _generate_title_async(user_message) title = await _generate_title_async(user_message)
if not title: if not title:
_title_debug_log("title_not_generated", conversation_id=conversation_id, username=username)
return return
# 限长,避免标题过长 # 限长,避免标题过长
safe_title = title[:80] safe_title = title[:80]
@ -490,8 +517,11 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati
ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title) ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title)
except Exception as exc: except Exception as exc:
debug_log(f"[TitleGen] 保存标题失败: {exc}") debug_log(f"[TitleGen] 保存标题失败: {exc}")
_title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id)
if not ok: if not ok:
_title_debug_log("title_save_failed", conversation_id=conversation_id, safe_title=safe_title)
return return
_title_debug_log("title_save_success", conversation_id=conversation_id, safe_title=safe_title)
try: try:
socketio.emit('conversation_changed', { socketio.emit('conversation_changed', {
'conversation_id': conversation_id, 'conversation_id': conversation_id,
@ -503,11 +533,13 @@ def generate_conversation_title_background(web_terminal: WebTerminal, conversati
}, room=f"user_{username}") }, room=f"user_{username}")
except Exception as exc: except Exception as exc:
debug_log(f"[TitleGen] 推送标题更新失败: {exc}") debug_log(f"[TitleGen] 推送标题更新失败: {exc}")
_title_debug_log("title_emit_exception", error=str(exc), conversation_id=conversation_id, username=username)
try: try:
asyncio.run(_runner()) asyncio.run(_runner())
except Exception as exc: except Exception as exc:
debug_log(f"[TitleGen] 任务执行失败: {exc}") debug_log(f"[TitleGen] 任务执行失败: {exc}")
_title_debug_log("title_background_runner_exception", error=str(exc), conversation_id=conversation_id, username=username)
def cache_monitor_snapshot(execution_id: Optional[str], stage: str, snapshot: Optional[Dict[str, Any]]): def cache_monitor_snapshot(execution_id: Optional[str], stage: str, snapshot: Optional[Dict[str, Any]]):
"""缓存工具执行前/后的文件快照。""" """缓存工具执行前/后的文件快照。"""

View File

@ -1,13 +1,57 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import json
import os
import re import re
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from core.web_terminal import WebTerminal from core.web_terminal import WebTerminal
from config import LOGS_DIR
from utils.api_client import DeepSeekClient from utils.api_client import DeepSeekClient
TITLE_DEBUG_DIR = Path(LOGS_DIR).expanduser().resolve() / "title_debug"
TITLE_DEBUG_FILE = TITLE_DEBUG_DIR / "title_generation.log"
def _title_debug_log(message: str, **extra: Any) -> None:
try:
TITLE_DEBUG_DIR.mkdir(parents=True, exist_ok=True)
payload = {
"ts": datetime.now().isoformat(timespec="milliseconds"),
"message": str(message),
}
if extra:
payload["extra"] = extra
with TITLE_DEBUG_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
except Exception:
pass
def _env_optional(name: str) -> Optional[str]:
value = os.environ.get(name)
if value is None:
env_path = Path(__file__).resolve().parents[1] / ".env"
if env_path.exists():
try:
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, val = line.split("=", 1)
if key.strip() == name:
value = val.strip().strip('"').strip("'")
break
except Exception:
value = None
if value is None:
return None
value = value.strip()
return value or None
async def _generate_title_async( async def _generate_title_async(
user_message: str, user_message: str,
@ -16,9 +60,29 @@ async def _generate_title_async(
) -> Optional[str]: ) -> Optional[str]:
"""使用快速模型生成对话标题。""" """使用快速模型生成对话标题。"""
if not user_message: if not user_message:
_title_debug_log("skip_empty_user_message")
return None return None
client = DeepSeekClient(thinking_mode=False, web_mode=True) client = DeepSeekClient(thinking_mode=False, web_mode=True)
title_base = _env_optional("AGENT_TITLE_API_BASE_URL")
title_key = _env_optional("AGENT_TITLE_API_KEY")
title_model = _env_optional("AGENT_TITLE_MODEL_ID")
if title_base:
client.fast_api_config["base_url"] = title_base
client.api_base_url = title_base
if title_key:
client.fast_api_config["api_key"] = title_key
client.api_key = title_key
if title_model:
client.fast_api_config["model_id"] = title_model
client.model_id = title_model
_title_debug_log("start_generate_title", user_message_preview=str(user_message)[:200], user_message_len=len(str(user_message)))
_title_debug_log(
"title_api_config",
base_url=client.fast_api_config.get("base_url"),
model_id=client.fast_api_config.get("model_id"),
has_api_key=bool(client.fast_api_config.get("api_key")),
)
try: try:
prompt_text = Path(title_prompt_path).read_text(encoding="utf-8") prompt_text = Path(title_prompt_path).read_text(encoding="utf-8")
except Exception: except Exception:
@ -38,11 +102,17 @@ async def _generate_title_async(
try: try:
content = resp.get("choices", [{}])[0].get("message", {}).get("content") content = resp.get("choices", [{}])[0].get("message", {}).get("content")
if content: if content:
return " ".join(str(content).strip().split()) normalized = " ".join(str(content).strip().split())
_title_debug_log("title_api_success", title_preview=normalized[:200], title_len=len(normalized))
return normalized
_title_debug_log("title_api_empty_content", resp_preview=str(resp)[:500])
except Exception: except Exception:
_title_debug_log("title_api_parse_error", resp_preview=str(resp)[:500])
continue continue
except Exception as exc: except Exception as exc:
debug_logger(f"[TitleGen] 生成标题异常: {exc}") debug_logger(f"[TitleGen] 生成标题异常: {exc}")
_title_debug_log("title_api_exception", error=str(exc))
_title_debug_log("title_api_no_result")
return None return None
@ -62,6 +132,7 @@ def generate_conversation_title_background(
async def _runner(): async def _runner():
title = await _generate_title_async(user_message, title_prompt_path, debug_logger) title = await _generate_title_async(user_message, title_prompt_path, debug_logger)
if not title: if not title:
_title_debug_log("title_not_generated", conversation_id=conversation_id, username=username)
return return
safe_title = title[:80] safe_title = title[:80]
@ -70,8 +141,11 @@ def generate_conversation_title_background(
ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title) ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title)
except Exception as exc: except Exception as exc:
debug_logger(f"[TitleGen] 保存标题失败: {exc}") debug_logger(f"[TitleGen] 保存标题失败: {exc}")
_title_debug_log("title_save_exception", error=str(exc), conversation_id=conversation_id)
if not ok: if not ok:
_title_debug_log("title_save_failed", conversation_id=conversation_id, safe_title=safe_title)
return return
_title_debug_log("title_save_success", conversation_id=conversation_id, safe_title=safe_title)
# 添加标题更新事件到任务事件流(用于轮询机制) # 添加标题更新事件到任务事件流(用于轮询机制)
try: try:
@ -91,6 +165,7 @@ def generate_conversation_title_background(
) )
except Exception as exc: except Exception as exc:
debug_logger(f"[TitleGen] 添加任务事件失败: {exc}") debug_logger(f"[TitleGen] 添加任务事件失败: {exc}")
_title_debug_log("title_task_event_exception", error=str(exc), conversation_id=conversation_id, username=username)
try: try:
socketio_instance.emit( socketio_instance.emit(
@ -105,11 +180,13 @@ def generate_conversation_title_background(
) )
except Exception as exc: except Exception as exc:
debug_logger(f"[TitleGen] 推送标题更新失败: {exc}") debug_logger(f"[TitleGen] 推送标题更新失败: {exc}")
_title_debug_log("title_emit_exception", error=str(exc), conversation_id=conversation_id, username=username)
try: try:
asyncio.run(_runner()) asyncio.run(_runner())
except Exception as exc: except Exception as exc:
debug_logger(f"[TitleGen] 任务执行失败: {exc}") debug_logger(f"[TitleGen] 任务执行失败: {exc}")
_title_debug_log("title_background_runner_exception", error=str(exc), conversation_id=conversation_id, username=username)
def get_thinking_state(terminal: WebTerminal) -> Dict[str, Any]: def get_thinking_state(terminal: WebTerminal) -> Dict[str, Any]:

View File

@ -14,6 +14,15 @@ except ImportError:
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT
def _env_flag(name: str, default: bool = False) -> bool:
value = str(os.environ.get(name, "")).strip().lower()
if not value:
return default
return value in {"1", "true", "yes", "on"}
_FILE_LOG_ENABLED = _env_flag("AGENT_FILE_LOG_ENABLED", default=False)
def setup_logger(name: str, log_file: str = None) -> logging.Logger: def setup_logger(name: str, log_file: str = None) -> logging.Logger:
""" """
设置日志记录器 设置日志记录器
@ -40,22 +49,23 @@ def setup_logger(name: str, log_file: str = None) -> logging.Logger:
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
logger.addHandler(console_handler) logger.addHandler(console_handler)
# 文件处理器 # 文件处理器(默认关闭,避免在工作区生成 agent_debug/agent_*.log
if log_file: if _FILE_LOG_ENABLED:
file_path = Path(LOGS_DIR) / log_file if log_file:
else: file_path = Path(LOGS_DIR) / log_file
# 默认日志文件 else:
today = datetime.now().strftime("%Y%m%d") # 默认日志文件
file_path = Path(LOGS_DIR) / f"agent_{today}.log" today = datetime.now().strftime("%Y%m%d")
file_path = Path(LOGS_DIR) / f"agent_{today}.log"
# 确保日志目录存在
file_path.parent.mkdir(parents=True, exist_ok=True) # 确保日志目录存在
file_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(file_path, encoding='utf-8')
file_handler.setLevel(getattr(logging, LOG_LEVEL)) file_handler = logging.FileHandler(file_path, encoding='utf-8')
file_handler.setFormatter(formatter) file_handler.setLevel(getattr(logging, LOG_LEVEL))
logger.addHandler(file_handler) file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger return logger
class TaskLogger: class TaskLogger: