fix(agent): unify logging API + add molecular-cot coverage to agent module

Phase 1 — API unification:
- Replace direct log() from cot_logger with canonical logger.reason/reflect/explore
  across _confirmation.py, _persistence.py, _tool_resolver.py, app.py

Phase 2 — Gap filling:
- tools.py: add REASON/REFLECT/EXPLORE to 17 C3 tool functions (was 0 logs)
- app.py agent_handler (C4): add lifecycle REASON on entry, REFLECT on exit,
  EXPLORE on OutputParserException + general exception
- langgraph_setup.py create_agent (C4): add REASON with model/config_source,
  EXPLORE on env-var/InMemorySaver fallback, REFLECT on graph compilation
- _tool_resolver.py infer_tool_from_text (C3, 13 branches): REASON on inference
- _persistence.py: REFLECT on save_conversation success, EXPLORE on prefetch

Phase 3 — Plain log migration:
- middleware.py: plain logger.info() -> logger.reason()
- run.py: 7 plain logger.info/warning/error -> molecular REASON/EXPLORE

Phase 4 — Cleanup:
- cot_logger.py: deprecate MarkerLogger (@DEPRECATED + @REPLACED_BY)
- molecular-cot-logging SKILL.md: remove cot_span Section IV (never implemented),
  renumber sections V-VII -> IV-VI, add cot_span rejection rationale

Verification: pytest 27/27, axiom rebuild 5844/2993/0 warnings
This commit is contained in:
2026-06-30 15:48:46 +03:00
parent 131c7cdfa4
commit a0619ca049
10 changed files with 359 additions and 148 deletions

View File

@@ -15,7 +15,7 @@ from typing import Any
import httpx
from src.core.cot_logger import log
from src.core.logger import logger
SAVE_API_URL = os.getenv("FASTAPI_URL", "http://localhost:8000") + "/api/agent/conversations/save"
TITLE_MAX_LENGTH = 80
@@ -170,13 +170,13 @@ def _get_llm_config() -> dict[str, Any] | None:
async def _call_llm_for_title(user_text: str) -> str | None:
"""Call LLM to generate a 3-5 word Russian title. Returns title or None on failure."""
from src.core.cot_logger import log as _log
from src.core.logger import logger as _logger
try:
import asyncio as _asyncio
config = await _asyncio.to_thread(_get_llm_config)
if not config or not config.get("configured"):
_log("AgentChat.Persistence", "EXPLORE", "LLM title: no provider configured", {})
_logger.explore("LLM title: no provider configured", extra={"src": "AgentChat.Persistence"})
return None
clean_text = clean_title(user_text)[:200]
@@ -211,8 +211,11 @@ async def _call_llm_for_title(user_text: str) -> str | None:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(api_url, json=payload, headers=headers)
if resp.status_code != 200:
_log("AgentChat.Persistence", "EXPLORE", "LLM title: API error",
{"status": resp.status_code}, error=resp.text[:200])
_logger.explore(
"LLM title: API error",
payload={"status": resp.status_code}, error=resp.text[:200],
extra={"src": "AgentChat.Persistence"},
)
return None
data = resp.json()
title = data.get("choices", [{}])[0].get("message", {}).get("content", "")
@@ -223,7 +226,7 @@ async def _call_llm_for_title(user_text: str) -> str | None:
if title:
return title
except Exception as e:
_log("AgentChat.Persistence", "EXPLORE", "LLM title generation failed", {}, error=str(e))
_logger.explore("LLM title generation failed", error=str(e), extra={"src": "AgentChat.Persistence"})
return None
@@ -263,11 +266,17 @@ async def generate_llm_title(conv_id: str, user_text: str) -> None:
}
async with httpx.AsyncClient(timeout=5) as client:
await client.post(SAVE_API_URL, json=payload, headers=headers)
log("AgentChat.Persistence", "REFLECT", "LLM title updated",
{"conv_id": conv_id, "title": title[:40]})
logger.reflect(
"LLM title updated",
payload={"conv_id": conv_id, "title": title[:40]},
extra={"src": "AgentChat.Persistence"},
)
except Exception as e:
log("AgentChat.Persistence", "EXPLORE", "LLM title save failed",
{"conv_id": conv_id}, error=str(e))
logger.explore(
"LLM title save failed",
payload={"conv_id": conv_id}, error=str(e),
extra={"src": "AgentChat.Persistence"},
)
finally:
_title_locks.pop(conv_id, None)
# #endregion AgentChat.Persistence.GenerateLlmTitle
@@ -314,7 +323,12 @@ async def prefetch_dashboards(env_id: str) -> str:
if total > limit:
suffix = f"\n... {total - limit} more dashboards omitted. Ask for a narrower search if needed."
return f"Available dashboards in environment '{env_id or 'default'}' ({total} total):\n" + "\n".join(lines) + suffix
except Exception:
except Exception as e:
logger.explore(
"Prefetch dashboards failed",
payload={"env_id": env_id}, error=str(e),
extra={"src": "AgentChat.Persistence.PrefetchDashboards"},
)
return ""
# #endregion AgentChat.Persistence.PrefetchDashboards
@@ -369,8 +383,16 @@ async def save_conversation(conv_id: str, user_text: str, user_id: str = "admin"
async with httpx.AsyncClient(timeout=10) as client:
await client.post(SAVE_API_URL, json=payload, headers=headers)
logger.reflect(
"Conversation saved",
payload={"conv_id": conv_id, "msg_count": len(messages), "title": title[:40]},
extra={"src": "AgentChat.Persistence.SaveConversation"},
)
except Exception as e:
log("AgentChat.Persistence", "EXPLORE", "Failed to save conversation",
{"conv_id": conv_id}, error=str(e))
logger.explore(
"Failed to save conversation",
payload={"conv_id": conv_id}, error=str(e),
extra={"src": "AgentChat.Persistence"},
)
# #endregion AgentChat.Persistence.SaveConversation
# #endregion AgentChat.Persistence