mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 22:41:12 +02:00
feat(agent): add comprehensive logging to LLM and tool execution nodes
- Add info and debug logging to llm_call node for tracking LLM invocations and configuration - Add error handling and logging to tool_node for tool execution failures - Add startup logging to stream_chat method with session details - Improve observability of agent workflow and debugging capabilities
This commit is contained in:
1 parent
1e9cf3fcbb
commit
a278a6091c
4 files changed
+94
-10
No files matched your search
@@ -128,6 +128,8 @@ class MessagesState(TypedDict):
|
||||
def llm_call(state: dict, config: RunnableConfig | None = None):
|
||||
"""LLM decides whether to call a tool or not"""
|
||||
|
||||
logger.info("LLM call node invoked")
|
||||
|
||||
# Get llm_config from request-scoped context variable
|
||||
from gns3server.agent.gns3_copilot.gns3_client import get_current_llm_config
|
||||
llm_config = get_current_llm_config()
|
||||
@@ -140,9 +142,13 @@ def llm_call(state: dict, config: RunnableConfig | None = None):
|
||||
"topology_info": None,
|
||||
}
|
||||
|
||||
logger.debug("LLM config retrieved from context: provider=%s, model=%s",
|
||||
llm_config.get("provider"), llm_config.get("model"))
|
||||
|
||||
# Defensive check: skip LLM call if no user messages
|
||||
messages = state.get("messages", [])
|
||||
if not messages or len(messages) == 0:
|
||||
logger.warning("No messages in state, skipping LLM call")
|
||||
return {
|
||||
"messages": [],
|
||||
"llm_calls": state.get("llm_calls", 0),
|
||||
@@ -216,13 +222,21 @@ def llm_call(state: dict, config: RunnableConfig | None = None):
|
||||
# print(full_messages)
|
||||
|
||||
# Create fresh model with tools for each LLM call
|
||||
logger.debug("Creating model with tools: provider=%s, model=%s",
|
||||
llm_config.get("provider"), llm_config.get("model"))
|
||||
model_with_tools = create_base_model_with_tools(
|
||||
tools,
|
||||
llm_config=llm_config
|
||||
)
|
||||
|
||||
logger.info("Invoking LLM with %d messages", len(full_messages))
|
||||
response = model_with_tools.invoke(full_messages)
|
||||
|
||||
logger.info("LLM call completed: tool_calls=%d",
|
||||
len(response.tool_calls) if hasattr(response, 'tool_calls') else 0)
|
||||
|
||||
return {
|
||||
"messages": [model_with_tools.invoke(full_messages)],
|
||||
"messages": [response],
|
||||
"llm_calls": state.get("llm_calls", 0) + 1,
|
||||
"topology_info": topology_info,
|
||||
}
|
||||
@@ -317,10 +331,21 @@ def generate_title(state: MessagesState, config: RunnableConfig | None = None) -
|
||||
def tool_node(state: dict, config: RunnableConfig | None = None):
|
||||
"""Performs the tool call"""
|
||||
|
||||
tool_calls = state["messages"][-1].tool_calls
|
||||
logger.info("Tool node invoked: tool_calls=%d", len(tool_calls))
|
||||
|
||||
result = []
|
||||
for tool_call in state["messages"][-1].tool_calls:
|
||||
tool = tools_by_name[tool_call["name"]]
|
||||
observation = tool.invoke(tool_call["args"])
|
||||
for tool_call in tool_calls:
|
||||
tool_name = tool_call["name"]
|
||||
logger.debug("Executing tool: %s with args: %s", tool_name, tool_call["args"])
|
||||
tool = tools_by_name[tool_name]
|
||||
try:
|
||||
observation = tool.invoke(tool_call["args"])
|
||||
logger.debug("Tool %s completed: output_length=%d",
|
||||
tool_name, len(str(observation)) if observation else 0)
|
||||
except Exception as e:
|
||||
logger.error("Tool %s failed: %s", tool_name, e, exc_info=True)
|
||||
observation = f"Error: {str(e)}"
|
||||
result.append(ToolMessage(
|
||||
content=observation,
|
||||
tool_call_id=tool_call["id"],
|
||||
|
||||
@@ -123,13 +123,24 @@ class AgentService:
|
||||
Yields:
|
||||
Dict containing SSE-compatible response chunks
|
||||
"""
|
||||
log.info(
|
||||
"Stream chat started: project_id=%s, user_id=%s, session_id=%s, mode=%s",
|
||||
project_id,
|
||||
user_id,
|
||||
session_id,
|
||||
mode,
|
||||
)
|
||||
|
||||
# Set request-scoped context variables (memory-only, not persisted)
|
||||
if jwt_token:
|
||||
from gns3server.agent.gns3_copilot.gns3_client import set_current_jwt_token
|
||||
set_current_jwt_token(jwt_token)
|
||||
log.debug("JWT token set in context")
|
||||
if llm_config:
|
||||
from gns3server.agent.gns3_copilot.gns3_client import set_current_llm_config
|
||||
set_current_llm_config(llm_config)
|
||||
log.debug("LLM config set in context: provider=%s, model=%s",
|
||||
llm_config.get("provider"), llm_config.get("model"))
|
||||
|
||||
# Build config - only thread-safe identifiers
|
||||
config = {
|
||||
@@ -152,12 +163,14 @@ class AgentService:
|
||||
|
||||
# Get the compiled graph
|
||||
graph = await self._get_graph()
|
||||
log.debug("LangGraph graph obtained, starting stream")
|
||||
|
||||
# Stream events
|
||||
try:
|
||||
async for event in graph.astream_events(inputs, config=config, version="v2"):
|
||||
chunk = self._convert_event_to_chunk(event, session_id)
|
||||
if chunk:
|
||||
log.debug("Yielding chunk: type=%s", chunk.get("type"))
|
||||
yield chunk
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -30,6 +30,8 @@ import logging
|
||||
from typing import Optional
|
||||
from contextvars import ContextVar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Context variables for request-scoped data
|
||||
# Automatically cleaned up when request context ends
|
||||
_jwt_token_context: ContextVar[Optional[str]] = ContextVar("_jwt_token_context", default=None)
|
||||
@@ -39,27 +41,39 @@ _llm_config_context: ContextVar[Optional[dict]] = ContextVar("_llm_config_contex
|
||||
def set_current_jwt_token(token: str) -> None:
|
||||
"""Set the JWT token for the current request context."""
|
||||
_jwt_token_context.set(token)
|
||||
logger.debug("JWT token set in context")
|
||||
|
||||
|
||||
def get_current_jwt_token() -> Optional[str]:
|
||||
"""Get the JWT token for the current request context."""
|
||||
return _jwt_token_context.get()
|
||||
token = _jwt_token_context.get()
|
||||
if token:
|
||||
logger.debug("JWT token retrieved from context")
|
||||
else:
|
||||
logger.warning("JWT token not found in context")
|
||||
return token
|
||||
|
||||
|
||||
def set_current_llm_config(config: dict) -> None:
|
||||
"""Set the LLM config for the current request context."""
|
||||
_llm_config_context.set(config)
|
||||
logger.debug("LLM config set in context: provider=%s, model=%s",
|
||||
config.get("provider"), config.get("model"))
|
||||
|
||||
|
||||
def get_current_llm_config() -> Optional[dict]:
|
||||
"""Get the LLM config for the current request context."""
|
||||
return _llm_config_context.get()
|
||||
config = _llm_config_context.get()
|
||||
if config:
|
||||
logger.debug("LLM config retrieved from context: provider=%s, model=%s",
|
||||
config.get("provider"), config.get("model"))
|
||||
else:
|
||||
logger.warning("LLM config not found in context")
|
||||
return config
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Gns3Connector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Fallback default URL
|
||||
DEFAULT_GNS3_URL = "http://127.0.0.1:3080"
|
||||
|
||||
@@ -316,17 +330,22 @@ def _detect_url_for_api() -> Optional[str]:
|
||||
Returns:
|
||||
URL string, or None if detection failed
|
||||
"""
|
||||
logger.debug("Detecting GNS3 server URL for API calls")
|
||||
|
||||
# Try Controller first
|
||||
url = _get_url_from_controller()
|
||||
if url:
|
||||
logger.debug("Using URL from Controller for API call: %s", url)
|
||||
return url
|
||||
|
||||
# Try Config
|
||||
url = _get_url_from_config()
|
||||
if url:
|
||||
logger.debug("Using URL from Config for API call: %s", url)
|
||||
return url
|
||||
|
||||
# Fallback
|
||||
logger.debug("Using fallback URL for API call: %s", DEFAULT_GNS3_URL)
|
||||
return DEFAULT_GNS3_URL
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user