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

@@ -122,7 +122,7 @@ async def handle_resume(
user_jwt: str = "", env_id: str | None = None,
) -> AsyncGenerator[str]:
from src.agent.context import set_user_jwt
from src.core.cot_logger import log
from src.core.logger import logger
set_user_jwt(user_jwt)
pending = _pending_confirmations.pop(conversation_id, None)
@@ -134,7 +134,11 @@ async def handle_resume(
})
return
if action == "confirm":
log("AgentChat.Confirmation", "REASON", "Fast-path confirmation resume", {"tool": pending.get("tool_name"), "conv_id": conversation_id})
logger.reason(
"Fast-path confirmation resume",
payload={"tool": pending.get("tool_name"), "conv_id": conversation_id},
extra={"src": "AgentChat.Confirmation"},
)
tool_name = str(pending.get("tool_name") or "unknown_action")
tool_args = normalize_tool_args(pending.get("tool_args"))
yield json.dumps({
@@ -148,7 +152,11 @@ async def handle_resume(
tool_obj = find_tool(tool_name)
if tool_obj is None:
error = f"Unknown tool: {tool_name}"
log("AgentChat.Confirmation", "EXPLORE", "Unknown tool in resume", {"tool": tool_name}, error=error)
logger.explore(
"Unknown tool in resume",
payload={"tool": tool_name}, error=error,
extra={"src": "AgentChat.Confirmation"},
)
yield json.dumps({
"content": f"{tool_name}{error}",
"metadata": {"type": "tool_error", "tool": tool_name, "error": error},
@@ -157,7 +165,11 @@ async def handle_resume(
try:
output = await tool_obj.ainvoke(tool_args)
except Exception as exc:
log("AgentChat.Confirmation", "EXPLORE", "Tool invocation failed in resume", {"tool": tool_name}, error=str(exc))
logger.explore(
"Tool invocation failed in resume",
payload={"tool": tool_name}, error=str(exc),
extra={"src": "AgentChat.Confirmation"},
)
yield json.dumps({
"content": f"{tool_name}{exc}",
"metadata": {"type": "tool_error", "tool": tool_name, "error": str(exc)},
@@ -171,10 +183,18 @@ async def handle_resume(
"content": str(output),
"metadata": {"type": "stream_token", "token": str(output)},
})
log("AgentChat.Confirmation", "REFLECT", "Fast-path confirmation completed", {"tool": tool_name})
logger.reflect(
"Fast-path confirmation completed",
payload={"tool": tool_name},
extra={"src": "AgentChat.Confirmation"},
)
return
log("AgentChat.Confirmation", "REASON", "LangGraph checkpoint resume", {"conv_id": conversation_id, "action": action})
logger.reason(
"LangGraph checkpoint resume",
payload={"conv_id": conversation_id, "action": action},
extra={"src": "AgentChat.Confirmation"},
)
agent = await create_agent(get_all_tools(), env_id, interrupt_before=[])
if action == "confirm":
config = {"configurable": {"thread_id": conversation_id}}
@@ -205,7 +225,11 @@ async def handle_resume(
"metadata": {"type": "tool_end", "tool": tool_name, "output": {"result": str(output)[:500]}},
})
elif action == "deny":
log("AgentChat.Confirmation", "REFLECT", "Checkpoint resume denied", {"conv_id": conversation_id})
logger.reflect(
"Checkpoint resume denied",
payload={"conv_id": conversation_id},
extra={"src": "AgentChat.Confirmation"},
)
yield json.dumps({
"content": "⏹️ Операция отменена",
"metadata": {"type": "confirm_resolved", "result": "denied"},

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

View File

@@ -9,7 +9,7 @@
from typing import Any
from src.agent.tools import get_all_tools
from src.core.cot_logger import log
from src.core.logger import logger
# #region AgentChat.ToolResolver.Sets [C:1] [TYPE Constants] [SEMANTICS agent-chat,tools,sets]
# @ingroup AgentChat
@@ -56,7 +56,11 @@ def known_agent_tool_names() -> set[str]:
try:
return {str(tool_obj.name) for tool_obj in get_all_tools() if getattr(tool_obj, "name", None)}
except Exception as exc:
log("AgentChat.ToolResolver", "EXPLORE", "tool catalog lookup failed", {"error": str(exc)})
logger.explore(
"tool catalog lookup failed",
payload={"error": str(exc)},
extra={"src": "AgentChat.ToolResolver"},
)
return _SAFE_AGENT_TOOLS | _GUARDED_AGENT_TOOLS | _DANGEROUS_AGENT_TOOLS
# #endregion AgentChat.ToolResolver.KnownNames
@@ -105,39 +109,46 @@ def coerce_tool_call(tool_call: Any) -> tuple[str | None, dict[str, Any]]:
# uses keyword matching to guess the user's intent and auto-trigger HITL.
def infer_tool_from_text(text: str) -> str | None:
lowered = (text or "").lower()
inferred: str | None = None
if any(word in lowered for word in ["окруж", "environment", "env"]):
return "list_environments"
if any(word in lowered for word in ["maintenance", "обслуж", "баннер"]):
inferred = "list_environments"
elif any(word in lowered for word in ["maintenance", "обслуж", "баннер"]):
if any(word in lowered for word in ["start", "созда", "запусти", "начни"]):
return "start_maintenance"
if any(word in lowered for word in ["end", "закрой", "заверши", "останов"]):
return "end_maintenance"
return "list_maintenance_events"
if any(word in lowered for word in ["дашборд", "dashboard", "dashboards", "дашборды"]):
return "search_dashboards"
if any(word in lowered for word in ["здоров", "health", "статус системы", "system status"]):
return "get_health_summary"
if any(word in lowered for word in ["задач", "task", "таск"]):
return "get_task_status"
if any(word in lowered for word in ["llm", "provider", "провайдер", "модель"]):
return "list_llm_providers"
if any(word in lowered for word in ["branch", "ветк"]):
return "create_branch"
if any(word in lowered for word in ["commit", "коммит"]):
return "commit_changes"
if any(word in lowered for word in ["deploy", "депло", "разверн"]):
return "deploy_dashboard"
if any(word in lowered for word in ["миграц", "migration", "migrate"]):
return "execute_migration"
if any(word in lowered for word in ["backup", "бэкап", "резерв"]):
return "run_backup"
if any(word in lowered for word in ["валидац", "validation", "validate"]):
return "run_llm_validation"
if any(word in lowered for word in ["документ", "documentation", "docs"]):
return "run_llm_documentation"
if any(word in lowered for word in ["инструмент", "tool", "capabilit", "умеешь", "можешь"]):
return "show_capabilities"
return None
inferred = "start_maintenance"
elif any(word in lowered for word in ["end", "закрой", "заверши", "останов"]):
inferred = "end_maintenance"
else:
inferred = "list_maintenance_events"
elif any(word in lowered for word in ["дашборд", "dashboard", "dashboards", "дашборды"]):
inferred = "search_dashboards"
elif any(word in lowered for word in ["здоров", "health", "статус системы", "system status"]):
inferred = "get_health_summary"
elif any(word in lowered for word in ["задач", "task", "таск"]):
inferred = "get_task_status"
elif any(word in lowered for word in ["llm", "provider", "провайдер", "модель"]):
inferred = "list_llm_providers"
elif any(word in lowered for word in ["branch", "ветк"]):
inferred = "create_branch"
elif any(word in lowered for word in ["commit", "коммит"]):
inferred = "commit_changes"
elif any(word in lowered for word in ["deploy", "депло", "разверн"]):
inferred = "deploy_dashboard"
elif any(word in lowered for word in ["миграц", "migration", "migrate"]):
inferred = "execute_migration"
elif any(word in lowered for word in ["backup", "бэкап", "резерв"]):
inferred = "run_backup"
elif any(word in lowered for word in ["валидац", "validation", "validate"]):
inferred = "run_llm_validation"
elif any(word in lowered for word in ["документ", "documentation", "docs"]):
inferred = "run_llm_documentation"
elif any(word in lowered for word in ["инструмент", "tool", "capabilit", "умеешь", "можешь"]):
inferred = "show_capabilities"
if inferred:
logger.reason("Tool inferred from user text",
payload={"tool": inferred, "text_preview": (text or "")[:80]},
extra={"src": "AgentChat.ToolResolver.InferFromText"})
return inferred
# #endregion AgentChat.ToolResolver.InferFromText
@@ -155,7 +166,11 @@ def extract_tool_call_from_state(state, user_text: str = "") -> tuple[str | None
if tool_name:
return (str(tool_name), tool_args)
except Exception as exc:
log("AgentChat.ToolResolver", "EXPLORE", "tool_call extraction failed", {"error": str(exc)})
logger.explore(
"tool_call extraction failed",
payload={"error": str(exc)},
extra={"src": "AgentChat.ToolResolver"},
)
if getattr(state, "next", None):
node_or_tool = str(state.next[0])
@@ -164,7 +179,11 @@ def extract_tool_call_from_state(state, user_text: str = "") -> tuple[str | None
inferred_tool = infer_tool_from_text(user_text)
if inferred_tool:
log("AgentChat.ToolResolver", "EXPLORE", "tool_call inferred from user text", {"tool": inferred_tool})
logger.explore(
"tool_call inferred from user text",
payload={"tool": inferred_tool},
extra={"src": "AgentChat.ToolResolver"},
)
return (inferred_tool, {})
return (None, {})
# #endregion AgentChat.ToolResolver.ExtractFromState

View File

@@ -49,7 +49,7 @@ from src.agent.langgraph_setup import create_agent
from src.agent.middleware import log_tool_event
from src.agent.tools import get_tools_for_query
from src.core.auth.jwt import decode_token
from src.core.cot_logger import log
from src.core.logger import logger
JWT_SECRET = os.getenv("JWT_SECRET", "super-secret-key")
MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024 # 10 MB
@@ -96,8 +96,11 @@ def _persist_chat_file(file_path: str, conv_id: str) -> str | None:
shutil.copy2(str(src), str(dest_file))
rel_path = str(dest_file.relative_to(Path(storage_root)))
log("AgentChat.PersistFile", "REFLECT", "Chat file persisted to storage",
{"original": src.name, "rel_path": rel_path, "size": dest_file.stat().st_size})
logger.reflect(
"Chat file persisted to storage",
payload={"original": src.name, "rel_path": rel_path, "size": dest_file.stat().st_size},
extra={"src": "AgentChat.PersistFile"},
)
return rel_path
# #endregion AgentChat.GradioApp.PersistFile
# Per-conversation mutex for HITL resume (FR-026): keyed by conversation_id
@@ -159,6 +162,13 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
try:
# ── Resolve conversation ID early (needed for file persistence) ──
conv_id = conversation_id or str(uuid.uuid4())
is_resume = action in ("confirm", "deny")
logger.reason(
"Agent handler invoked",
payload={"user_id": user_id, "conv_id": conv_id, "action": action, "env_id": env_id,
"is_resume": is_resume, "msg_len": len(str(message))},
extra={"src": "AgentChat.GradioApp.Handler"},
)
# ── Parse message ──
text = message.get("text", "") if isinstance(message, dict) else str(message)
@@ -335,12 +345,24 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
if attempt < max_attempts - 1:
text = "Respond with valid JSON only. Previous response was malformed.\n\n" + text
continue
logger.explore(
"LLM malformed output",
payload={"conv_id": conv_id, "attempt": attempt},
error=str(e),
extra={"src": "AgentChat.GradioApp.Handler"},
)
yield json.dumps({
"content": "❌ Ошибка обработки ответа LLM. Пожалуйста, уточните запрос.",
"metadata": {"type": "error", "code": "LLM_MALFORMED_OUTPUT", "detail": str(e)},
})
except Exception as exc:
logger.explore(
"Agent handler failed",
payload={"conv_id": conv_id, "user_id": user_id},
error=str(exc),
extra={"src": "AgentChat.GradioApp.Handler"},
)
try:
state = await agent.aget_state(config)
if getattr(state, "next", None):
@@ -358,6 +380,11 @@ async def agent_handler( # noqa: C901 — intentionally complex C4 orchestratio
await save_conversation(conv_id, text, user_id, assistant_text="".join(assistant_parts))
# Fire-and-forget: generate LLM title in background (best-effort)
asyncio.create_task(generate_llm_title(conv_id, text))
logger.reflect(
"Agent handler completed",
payload={"conv_id": conv_id, "assistant_len": len("".join(assistant_parts))},
extra={"src": "AgentChat.GradioApp.Handler"},
)
finally:
_user_locks[user_id] = False

View File

@@ -20,6 +20,8 @@ from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from langgraph.prebuilt import create_react_agent
from psycopg.rows import dict_row
from src.core.logger import logger
# ── Monkey-patch: OpenAI SDK for Pydantic BaseModel classes ──
# LangChain BaseTool objects carry an ``args_schema`` field that is a Pydantic
# BaseModel *class* reference (not an instance). When the OpenAI SDK recursively
@@ -111,8 +113,9 @@ async def _fetch_llm_config() -> dict | None:
if config.get("configured"):
_llm_config = config
return config
except Exception:
pass # Keep existing config on failure
except Exception as e:
logger.explore("Failed to fetch LLM config from FastAPI", error=str(e),
extra={"src": "AgentChat.LangGraph.Setup"})
return _llm_config
@@ -151,10 +154,24 @@ async def create_agent(
api_key = config["api_key"]
base_url = config.get("base_url") or "https://api.openai.com/v1"
model = config.get("default_model") or "gpt-4o-mini"
config_source = "FastAPI"
else:
api_key = os.getenv("LLM_API_KEY")
base_url = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")
model = os.getenv("LLM_MODEL", "gpt-4o")
config_source = "env vars"
logger.explore(
"LLM config not found in FastAPI, falling back to env vars",
payload={"model": model, "provider_type": config.get("provider_type") if config else None},
error="No configured LLM provider in FastAPI",
extra={"src": "AgentChat.LangGraph.Setup"},
)
logger.reason(
"Creating LangGraph agent",
payload={"model": model, "config_source": config_source, "tools_count": len(tools), "env_id": env_id},
extra={"src": "AgentChat.LangGraph.Setup"},
)
llm = ChatOpenAI(
model=model,
@@ -179,6 +196,11 @@ async def create_agent(
checkpointer = _CHECKPOINTER
else:
checkpointer = InMemorySaver()
logger.explore(
"Postgres checkpointer unavailable, falling back to InMemorySaver",
error="_CHECKPOINTER is None — checkpoints will be lost on restart",
extra={"src": "AgentChat.LangGraph.Setup"},
)
graph = create_react_agent(
model=llm,
@@ -189,5 +211,10 @@ async def create_agent(
interrupt_before=_interrupt_before_from_env() if interrupt_before is None else interrupt_before,
)
logger.reflect(
"LangGraph agent created",
payload={"model": model, "checkpointer_type": type(checkpointer).__name__, "tools_count": len(tools)},
extra={"src": "AgentChat.LangGraph.Setup"},
)
return graph
# #endregion AgentChat.LangGraph.Setup

View File

@@ -7,11 +7,9 @@
# @REJECTED ConfirmationRiskMiddleware rejected — LangGraph interrupt_before=DANGEROUS_TOOLS handles HITL natively.
from datetime import UTC, datetime
import logging
from src.agent.context import get_user_jwt
logger = logging.getLogger("cot")
from src.core.logger import logger
# #region AgentChat.Middleware.LoggingMiddleware [C:3] [TYPE Function] [SEMANTICS audit,tool,logging]
@@ -48,9 +46,10 @@ async def log_tool_event(event: dict, conversation_id: str) -> None:
elif kind == "on_tool_error":
audit_payload["error"] = str(data.get("error", ""))[:500]
logger.info(
"Tool audit: %(event_type)s%(tool)s — conv=%(conversation_id)s",
audit_payload,
logger.reason(
"Tool audit event",
payload=audit_payload,
extra={"src": "AgentChat.Middleware.LoggingMiddleware"},
)
# TODO: Async write to assistant_audit table via REST call to FastAPI

View File

@@ -11,9 +11,8 @@
import os
import socket
import httpx
import logging
logger = logging.getLogger("cot")
from src.core.logger import logger
FASTAPI_URL = os.getenv("FASTAPI_URL", "http://localhost:8000")
@@ -46,16 +45,37 @@ def _fetch_llm_config() -> dict | None:
resp.raise_for_status()
config = resp.json()
if config.get("configured"):
logger.info("LLM config fetched from FastAPI: %s (%s)", config.get("provider_type"), config.get("default_model"))
logger.reason(
"LLM config fetched from FastAPI",
payload={"provider_type": config.get("provider_type"), "model": config.get("default_model")},
extra={"src": "AgentChat.Run.FetchLlmConfig"},
)
return config
logger.warning("FastAPI returned no active LLM provider: %s", config.get("reason"))
logger.explore(
"FastAPI returned no active LLM provider",
payload={"reason": config.get("reason")},
error="No configured LLM provider",
extra={"src": "AgentChat.Run.FetchLlmConfig"},
)
except Exception as e:
if attempt < 5:
logger.info("Waiting for FastAPI (attempt %d/6): %s", attempt + 1, e)
logger.reason(
f"Waiting for FastAPI (attempt {attempt + 1}/6)",
payload={"error": str(e)},
extra={"src": "AgentChat.Run.FetchLlmConfig"},
)
time.sleep(5)
else:
logger.warning("Failed to fetch LLM config from FastAPI after 6 attempts: %s", e)
logger.info("Falling back to env vars for LLM config")
logger.explore(
"Failed to fetch LLM config after 6 attempts",
error=str(e),
extra={"src": "AgentChat.Run.FetchLlmConfig"},
)
logger.explore(
"Falling back to env vars for LLM config",
error="FastAPI unreachable",
extra={"src": "AgentChat.Run.FetchLlmConfig"},
)
return None
@@ -85,9 +105,18 @@ if __name__ == "__main__":
try:
port = _find_free_port(configured_port)
if port != configured_port:
logger.warning("Port %d is in use, falling back to port %d", configured_port, port)
logger.explore(
"Port in use, falling back",
payload={"configured_port": configured_port, "actual_port": port},
error=f"Port {configured_port} is in use",
extra={"src": "AgentChat.Run.PortBinding"},
)
except OSError as e:
logger.error("Failed to find a free port: %s", e)
logger.explore(
"Failed to find a free port",
error=str(e),
extra={"src": "AgentChat.Run.PortBinding"},
)
raise
else:
port = configured_port

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

View File

@@ -170,6 +170,9 @@ def log(
# #region MarkerLogger [C:2] [TYPE Class] [SEMANTICS logger, proxy, marker, syntactic-sugar]
# @ingroup Core
# @BRIEF Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.
# @DEPRECATED Use logger.reason/reflect/explore from src.core.logger instead — MarkerLogger has
# zero usages in the codebase and adds a redundant abstraction layer.
# @REPLACED_BY logger.reason/reflect/explore (monkey-patched on superset_tools_app logger)
class MarkerLogger:
"""Thin proxy over the cot_logger.log() function.