Skip to content

Commit 942aef9

Browse files
committed
feat: Add tool invocation logging with latency tracking in WorkbenchService
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 547fded commit 942aef9

1 file changed

Lines changed: 50 additions & 1 deletion

File tree

backend/agent_workbench/service.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
The host project injects tools and LLM configuration at startup.
1010
"""
1111

12+
import logging
1213
import os
1314
from datetime import datetime
1415
from pathlib import Path
16+
from time import perf_counter
1517
from typing import Any, Optional
1618

1719
from sqlmodel import Session, select
@@ -33,6 +35,41 @@
3335
)
3436
from .tool_registry import ToolRegistry
3537

38+
logger = logging.getLogger(__name__)
39+
40+
41+
# ============================================================================
42+
# TOOL CALL LOGGING CALLBACK
43+
# ============================================================================
44+
45+
def _make_tool_logging_callback() -> Any:
46+
"""Create a callback handler that logs tool invocations with latency."""
47+
from langchain_core.callbacks import BaseCallbackHandler
48+
49+
class ToolCallLoggingCallback(BaseCallbackHandler):
50+
def __init__(self) -> None:
51+
super().__init__()
52+
self._start_times: dict[Any, float] = {}
53+
54+
def on_tool_start(self, serialized: dict[str, Any], input_str: str, *, run_id: Any, **kwargs: Any) -> None:
55+
self._start_times[run_id] = perf_counter()
56+
name = serialized.get("name", "?")
57+
preview = input_str[:200] if isinstance(input_str, str) else str(input_str)[:200]
58+
logger.info("🔧 Tool START name=%s run_id=%s input=%s", name, run_id, preview)
59+
60+
def on_tool_end(self, output: str, *, run_id: Any, **kwargs: Any) -> None:
61+
started = self._start_times.pop(run_id, None)
62+
ms = int((perf_counter() - started) * 1000) if started is not None else None
63+
preview = output[:300] if isinstance(output, str) else str(output)[:300]
64+
logger.info("✅ Tool END run_id=%s duration_ms=%s output=%s", run_id, ms, preview)
65+
66+
def on_tool_error(self, error: BaseException, *, run_id: Any, **kwargs: Any) -> None:
67+
started = self._start_times.pop(run_id, None)
68+
ms = int((perf_counter() - started) * 1000) if started is not None else None
69+
logger.error("❌ Tool ERROR run_id=%s duration_ms=%s error=%s", run_id, ms, error)
70+
71+
return ToolCallLoggingCallback()
72+
3673
# ============================================================================
3774
# LLM HELPER - isolated so it stays optional at import time
3875
# ============================================================================
@@ -381,11 +418,20 @@ async def run_agent(
381418
runtime_system_prompt = _append_markdown_output_instruction(agent_def.system_prompt)
382419
react = _build_react_agent(self.llm, tools, runtime_system_prompt)
383420

421+
logger.info("▶️ Agent run_id=%s agent=%s tools=%s prompt=%s",
422+
run_id, agent_id, validated_tool_names, user_message[:120])
423+
t0 = perf_counter()
424+
384425
result = await react.ainvoke(
385426
{"messages": [("user", user_message)]},
386-
config={"recursion_limit": self._recursion_limit},
427+
config={
428+
"recursion_limit": self._recursion_limit,
429+
"callbacks": [_make_tool_logging_callback()],
430+
},
387431
)
388432

433+
total_ms = int((perf_counter() - t0) * 1000)
434+
389435
final_msg = result["messages"][-1]
390436
output = final_msg.content if hasattr(final_msg, "content") else str(final_msg)
391437

@@ -398,6 +444,9 @@ async def run_agent(
398444
if name:
399445
tools_used.append(name)
400446

447+
logger.info("⏹️ Agent done run_id=%s total_ms=%s tools_used=%s messages=%d",
448+
run_id, total_ms, tools_used, len(result["messages"]))
449+
401450
# -- Persist completion --
402451
with Session(self._engine) as session:
403452
db_run = session.get(AgentRun, run_id)

0 commit comments

Comments
 (0)