mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-01 23:55:15 +03: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:
parent
1e9cf3fcbb
commit
a278a6091c
@ -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
|
||||
|
||||
|
||||
|
||||
@ -86,15 +86,29 @@ async def stream_chat(
|
||||
The project must be opened to use chat functionality.
|
||||
"""
|
||||
|
||||
# Get user authentication info
|
||||
user_id = str(current_user.user_id)
|
||||
|
||||
# Check if project is opened
|
||||
if project.status != "opened":
|
||||
log.warning(
|
||||
"Chat rejected: project not opened. user_id=%s, project_id=%s, status=%s",
|
||||
user_id,
|
||||
project.id,
|
||||
project.status
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Project must be opened to use chat. Current status: {project.status}"
|
||||
)
|
||||
|
||||
# Get user authentication info
|
||||
user_id = str(current_user.user_id)
|
||||
log.info(
|
||||
"Chat request started: user_id=%s, project_id=%s, project_name=%s, session_id=%s",
|
||||
user_id,
|
||||
project.id,
|
||||
project.name,
|
||||
request.session_id or "(new)",
|
||||
)
|
||||
|
||||
# Get JWT token from Authorization header
|
||||
auth_header = http_request.headers.get("Authorization", "")
|
||||
@ -107,21 +121,33 @@ async def stream_chat(
|
||||
from gns3server.db.tasks import get_user_llm_config_full
|
||||
llm_config = await get_user_llm_config_full(user_id, app)
|
||||
if not llm_config:
|
||||
log.warning("LLM config not found for user: %s", user_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="LLM configuration not found. Please configure your LLM settings first."
|
||||
)
|
||||
|
||||
log.debug(
|
||||
"LLM config loaded: user_id=%s, provider=%s, model=%s",
|
||||
user_id,
|
||||
llm_config.get("provider"),
|
||||
llm_config.get("model"),
|
||||
)
|
||||
|
||||
# Get or create AgentService for this project
|
||||
agent_manager = await get_project_agent_manager()
|
||||
agent_service = await agent_manager.get_agent(str(project.id), project.path)
|
||||
log.debug("AgentService obtained for project: %s", project.id)
|
||||
|
||||
# Generate session_id if not provided
|
||||
session_id = request.session_id or str(uuid.uuid4())
|
||||
if not request.session_id:
|
||||
log.debug("New session created: %s", session_id)
|
||||
|
||||
async def generate():
|
||||
"""Generator for SSE streaming."""
|
||||
try:
|
||||
log.debug("Starting stream: session_id=%s", session_id)
|
||||
async for chunk in agent_service.stream_chat(
|
||||
message=request.message,
|
||||
session_id=session_id,
|
||||
@ -141,6 +167,7 @@ async def stream_chat(
|
||||
continue
|
||||
|
||||
# Final done message
|
||||
log.debug("Stream completed: session_id=%s", session_id)
|
||||
yield f"data: {json.dumps({'type': 'done', 'session_id': session_id})}\n\n"
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user