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

@@ -14,6 +14,7 @@ from langchain_core.tools import tool
from pydantic import BaseModel, Field
from src.agent.context import get_service_jwt, get_user_jwt
from src.core.logger import logger
FASTAPI_URL = os.getenv("FASTAPI_URL", "http://localhost:8000")
TOOL_RESPONSE_LIMIT = 4000
@@ -178,9 +179,15 @@ class SearchDashboardsInput(BaseModel):
@tool(args_schema=SearchDashboardsInput)
async def search_dashboards(query: str, env_id: str | None = None) -> str:
"""Search and list dashboards by name, with optional environment filter."""
logger.reason("Search dashboards", payload={"query": query, "env_id": env_id},
extra={"src": "AgentChat.Tools.SearchDashboards"})
params = {"q": query, "env_id": env_id or ""}
resp = await _get("/api/dashboards", params=params)
if resp.status_code != 200:
logger.explore("Dashboard search failed",
payload={"status": resp.status_code, "query": query},
error=resp.text[:200],
extra={"src": "AgentChat.Tools.SearchDashboards"})
return f"Error {resp.status_code}: Unable to fetch dashboards. {resp.text}"
try:
@@ -189,6 +196,9 @@ async def search_dashboards(query: str, env_id: str | None = None) -> str:
total = data.get("total", 0)
if total == 0:
logger.reflect("No dashboards found",
payload={"query": query, "total": 0},
extra={"src": "AgentChat.Tools.SearchDashboards"})
return f"No dashboards found matching '{query}' in environment '{env_id or 'default'}'."
lines = [f"Found {total} dashboard(s) in environment '{env_id or 'default'}':"]
@@ -199,8 +209,14 @@ async def search_dashboards(query: str, env_id: str | None = None) -> str:
lines.append(f" - {title}")
lines.append(f" Owners: {owners}")
lines.append(f" Last modified: {modified}")
logger.reflect("Dashboards listed",
payload={"query": query, "total": total},
extra={"src": "AgentChat.Tools.SearchDashboards"})
return "\n".join(lines)
except (json.JSONDecodeError, KeyError) as e:
logger.explore("Failed to parse dashboards response",
payload={"query": query}, error=str(e),
extra={"src": "AgentChat.Tools.SearchDashboards"})
return f"Could not parse dashboards response: {e}"
# #endregion AgentChat.Tools.SearchDashboards
@@ -220,9 +236,18 @@ class HealthSummaryInput(BaseModel):
@tool(args_schema=HealthSummaryInput)
async def get_health_summary(env_id: str | None = None) -> str:
"""Get system health summary — dashboard validation status, recent failures."""
logger.reason("Get health summary", payload={"env_id": env_id},
extra={"src": "AgentChat.Tools.HealthSummary"})
resp = await _get("/api/health/summary", params={"environment_id": env_id or ""})
if resp.status_code != 200:
logger.explore("Health check failed",
payload={"status": resp.status_code},
error=resp.text[:200],
extra={"src": "AgentChat.Tools.HealthSummary"})
return f"Health check failed: HTTP {resp.status_code}"
logger.reflect("Health summary retrieved",
payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.HealthSummary"})
return _trim_response(resp.text, 2000)
# #endregion AgentChat.Tools.HealthSummary
@@ -236,8 +261,11 @@ async def get_health_summary(env_id: str | None = None) -> str:
@tool
async def list_environments() -> str:
"""List configured deployment environments."""
logger.reason("List environments", extra={"src": "AgentChat.Tools.ListEnvironments"})
resp = await _get("/api/settings/environments")
result = _api_result(resp)
logger.reflect("Environments listed", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.ListEnvironments"})
return _trim_response(_safe_json_text(result))
# #endregion AgentChat.Tools.ListEnvironments
@@ -251,8 +279,13 @@ async def list_environments() -> str:
@tool
async def get_task_status(task_id: str) -> str:
"""Check the status of a background task by its task_id."""
logger.reason("Get task status", payload={"task_id": task_id},
extra={"src": "AgentChat.Tools.TaskStatus"})
resp = await _get(f"/api/tasks/{task_id}")
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Task status retrieved", payload={"task_id": task_id, "status": resp.status_code},
extra={"src": "AgentChat.Tools.TaskStatus"})
return result
# #endregion AgentChat.Tools.TaskStatus
@@ -278,8 +311,12 @@ async def show_capabilities() -> str:
@tool
async def list_llm_providers() -> str:
"""List configured LLM providers."""
logger.reason("List LLM providers", extra={"src": "AgentChat.Tools.ListLlmProviders"})
resp = await _get("/api/llm/providers")
return _api_result(resp)
result = _api_result(resp)
logger.reflect("LLM providers listed", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.ListLlmProviders"})
return result
# #endregion AgentChat.Tools.ListLlmProviders
@@ -292,8 +329,12 @@ async def list_llm_providers() -> str:
@tool
async def get_llm_status() -> str:
"""Check whether the LLM runtime is configured and usable."""
logger.reason("Get LLM status", extra={"src": "AgentChat.Tools.LlmStatus"})
resp = await _get("/api/llm/status")
return _api_result(resp)
result = _api_result(resp)
logger.reflect("LLM status retrieved", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.LlmStatus"})
return result
# #endregion AgentChat.Tools.LlmStatus
@@ -318,12 +359,17 @@ async def run_backup(
dashboard_ids: list[int] | None = None,
) -> str:
"""Run a Superset backup for an environment, optionally scoped to dashboard IDs."""
vars = {"env_id": environment_id, "dashboard_id": dashboard_id, "dashboard_ids": dashboard_ids}
logger.reason("Run backup", payload=vars, extra={"src": "AgentChat.Tools.RunBackup"})
params: dict[str, Any] = {"environment_id": environment_id}
ids = _dashboard_ids(dashboard_id, dashboard_ids)
if ids:
params["dashboard_ids"] = ids
ids_l = _dashboard_ids(dashboard_id, dashboard_ids)
if ids_l:
params["dashboard_ids"] = ids_l
resp = await _post("/api/tasks", {"plugin_id": "superset-backup", "params": params})
return _api_result(resp, {201})
result = _api_result(resp, {201})
logger.reflect("Backup task created", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.RunBackup"})
return result
# #endregion AgentChat.Tools.RunBackup
@@ -352,6 +398,9 @@ async def execute_migration(
fix_cross_filters: bool = True,
) -> str:
"""Execute dashboard migration between two environments."""
logger.reason("Execute migration",
payload={"source": source_env_id, "target": target_env_id, "ids_count": len(selected_ids)},
extra={"src": "AgentChat.Tools.ExecuteMigration"})
payload = {
"source_env_id": source_env_id,
"target_env_id": target_env_id,
@@ -360,7 +409,10 @@ async def execute_migration(
"fix_cross_filters": fix_cross_filters,
}
resp = await _post("/api/migration/execute", payload)
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Migration executed", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.ExecuteMigration"})
return result
# #endregion AgentChat.Tools.ExecuteMigration
@@ -394,12 +446,18 @@ async def create_branch(
env_id: str | None = None,
) -> str:
"""Create a branch in a dashboard Git repository."""
logger.reason("Create branch",
payload={"dashboard_ref": dashboard_ref, "branch_name": branch_name, "from_branch": from_branch},
extra={"src": "AgentChat.Tools.CreateBranch"})
resp = await _post(
f"/api/git/repositories/{dashboard_ref}/branches",
{"name": branch_name, "from_branch": from_branch},
params={"env_id": env_id} if env_id else None,
)
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Branch created", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.CreateBranch"})
return result
# #endregion AgentChat.Tools.CreateBranch
@@ -424,12 +482,18 @@ async def commit_changes(
env_id: str | None = None,
) -> str:
"""Stage and commit changes in a dashboard Git repository."""
logger.reason("Commit changes",
payload={"dashboard_ref": dashboard_ref, "message": message[:80]},
extra={"src": "AgentChat.Tools.CommitChanges"})
resp = await _post(
f"/api/git/repositories/{dashboard_ref}/commit",
{"message": message, "files": files or []},
params={"env_id": env_id} if env_id else None,
)
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Changes committed", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.CommitChanges"})
return result
# #endregion AgentChat.Tools.CommitChanges
@@ -452,12 +516,18 @@ async def deploy_dashboard(
env_id: str | None = None,
) -> str:
"""Deploy a dashboard from Git to a target environment."""
logger.reason("Deploy dashboard",
payload={"dashboard_ref": dashboard_ref, "target_env": environment_id},
extra={"src": "AgentChat.Tools.DeployDashboard"})
resp = await _post(
f"/api/git/repositories/{dashboard_ref}/deploy",
{"environment_id": environment_id},
params={"env_id": env_id} if env_id else None,
)
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Dashboard deployed", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.DeployDashboard"})
return result
# #endregion AgentChat.Tools.DeployDashboard
@@ -482,6 +552,9 @@ async def run_llm_documentation(
provider_id: str | None = None,
) -> str:
"""Generate dataset documentation via the LLM documentation task."""
logger.reason("Run LLM documentation",
payload={"dataset_id": dataset_id, "environment_id": environment_id},
extra={"src": "AgentChat.Tools.LlmDocumentation"})
resolved_provider = await _resolve_active_provider_id(provider_id)
params = {
"dataset_id": str(dataset_id),
@@ -490,7 +563,11 @@ async def run_llm_documentation(
if resolved_provider:
params["provider_id"] = resolved_provider
resp = await _post("/api/tasks", {"plugin_id": "llm_documentation", "params": params})
return _api_result(resp, {201})
result = _api_result(resp, {201})
logger.reflect("LLM documentation task created",
payload={"status": resp.status_code, "provider_id": resolved_provider},
extra={"src": "AgentChat.Tools.LlmDocumentation"})
return result
# #endregion AgentChat.Tools.LlmDocumentation
@@ -524,10 +601,19 @@ async def run_llm_validation(
) -> str:
"""Create and immediately run an LLM dashboard validation policy, or run an existing policy."""
if policy_id:
logger.reason("Run existing validation policy",
payload={"policy_id": policy_id},
extra={"src": "AgentChat.Tools.LlmValidation"})
resp = await _post(f"/api/validation-tasks/{policy_id}/run")
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Validation policy run", payload={"policy_id": policy_id, "status": resp.status_code},
extra={"src": "AgentChat.Tools.LlmValidation"})
return result
ids = _dashboard_ids(dashboard_id, dashboard_ids)
logger.reason("Create LLM validation policy",
payload={"environment_id": environment_id, "dashboard_count": len(ids)},
extra={"src": "AgentChat.Tools.LlmValidation"})
sources = []
if dashboard_url:
sources.append({"type": "dashboard_url", "value": dashboard_url})
@@ -560,7 +646,11 @@ async def run_llm_validation(
except (json.JSONDecodeError, KeyError) as exc:
return f"Validation policy created but response could not be parsed: {exc}. Raw: {create_resp.text}"
run_resp = await _post(f"/api/validation-tasks/{created_id}/run")
return _api_result(run_resp)
result = _api_result(run_resp)
logger.reflect("Validation policy created and run",
payload={"policy_id": created_id, "status": run_resp.status_code},
extra={"src": "AgentChat.Tools.LlmValidation"})
return result
# #endregion AgentChat.Tools.LlmValidation
@@ -573,8 +663,12 @@ async def run_llm_validation(
@tool
async def list_maintenance_events() -> str:
"""List active and completed maintenance events."""
logger.reason("List maintenance events", extra={"src": "AgentChat.Tools.ListMaintenance"})
resp = await _get("/api/maintenance/events")
return _api_result(resp)
result = _api_result(resp)
logger.reflect("Maintenance events listed", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.ListMaintenance"})
return result
# #endregion AgentChat.Tools.ListMaintenance
@@ -603,6 +697,9 @@ async def start_maintenance(
message: str | None = None,
) -> str:
"""Start a maintenance event and apply banners to affected dashboards."""
logger.reason("Start maintenance",
payload={"environment_id": environment_id, "tables": tables},
extra={"src": "AgentChat.Tools.StartMaintenance"})
payload = {
"tables": tables,
"environment_id": environment_id,
@@ -611,7 +708,10 @@ async def start_maintenance(
"message": message,
}
resp = await _post("/api/maintenance/start", payload)
return _api_result(resp, {200, 202})
result = _api_result(resp, {200, 202})
logger.reflect("Maintenance started", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.StartMaintenance"})
return result
# #endregion AgentChat.Tools.StartMaintenance
@@ -637,13 +737,25 @@ async def end_maintenance(
) -> str:
"""End one maintenance event, or end all active events when end_all is true."""
if end_all:
logger.reason("End all maintenance events",
payload={"environment_id": environment_id},
extra={"src": "AgentChat.Tools.EndMaintenance"})
payload = {"environment_id": environment_id} if environment_id else {}
resp = await _post("/api/maintenance/end-all", payload)
return _api_result(resp, {202})
result = _api_result(resp, {202})
logger.reflect("All maintenance ended", payload={"status": resp.status_code},
extra={"src": "AgentChat.Tools.EndMaintenance"})
return result
if not maintenance_id:
return "Error: maintenance_id is required unless end_all=true."
logger.reason("End maintenance event",
payload={"maintenance_id": maintenance_id},
extra={"src": "AgentChat.Tools.EndMaintenance"})
resp = await _post(f"/api/maintenance/{maintenance_id}/end")
return _api_result(resp, {202})
result = _api_result(resp, {202})
logger.reflect("Maintenance ended", payload={"status": resp.status_code, "maintenance_id": maintenance_id},
extra={"src": "AgentChat.Tools.EndMaintenance"})
return result
# #endregion AgentChat.Tools.EndMaintenance