feat(copilot): replace LangGraph config with request-scoped context variables

- Refactor `llm_call` and `generate_title` nodes to retrieve `llm_config` from request-scoped context variable instead of LangGraph config
- Remove `jwt_token` and `llm_config` from LangGraph configurable parameters in `AgentService.stream`
- Add `set_current_llm_config` and `get_current_llm_config` functions to `connector_factory` and export them in `__init__.py`
- Update `tool_node` to no longer extract `jwt_token` from config as it is now handled via context variable
- Improves thread safety and decouples configuration from LangGraph's state management
This commit is contained in:
YueGuobin 2026-03-04 13:49:17 +08:00
parent b05e6a71b4
commit eec4ebe3fd
4 changed files with 43 additions and 20 deletions

View File

@ -128,10 +128,17 @@ class MessagesState(TypedDict):
def llm_call(state: dict, config: RunnableConfig | None = None): def llm_call(state: dict, config: RunnableConfig | None = None):
"""LLM decides whether to call a tool or not""" """LLM decides whether to call a tool or not"""
# Extract config from LangGraph config # Get llm_config from request-scoped context variable
configurable = config.get("configurable", {}) if config else {} from gns3server.agent.gns3_copilot.gns3_client import get_current_llm_config
jwt_token = configurable.get("jwt_token") llm_config = get_current_llm_config()
llm_config = configurable.get("llm_config")
if not llm_config:
logger.error("LLM config not found in context")
return {
"messages": [],
"llm_calls": state.get("llm_calls", 0),
"topology_info": None,
}
# Defensive check: skip LLM call if no user messages # Defensive check: skip LLM call if no user messages
messages = state.get("messages", []) messages = state.get("messages", [])
@ -228,9 +235,13 @@ def generate_title(state: MessagesState, config: RunnableConfig | None = None) -
This node is only executed when no title has been set yet (first round only). This node is only executed when no title has been set yet (first round only).
""" """
# Extract config from LangGraph config # Get llm_config from request-scoped context variable
configurable = config.get("configurable", {}) if config else {} from gns3server.agent.gns3_copilot.gns3_client import get_current_llm_config
llm_config = configurable.get("llm_config") llm_config = get_current_llm_config()
if not llm_config:
logger.error("LLM config not found in context, cannot generate title")
return {"conversation_title": UNTITLED_SESSION_FALLBACK}
# Only generate a title if it hasn't been set yet # Only generate a title if it hasn't been set yet
current_title = state.get("conversation_title") current_title = state.get("conversation_title")
@ -306,15 +317,6 @@ def generate_title(state: MessagesState, config: RunnableConfig | None = None) -
def tool_node(state: dict, config: RunnableConfig | None = None): def tool_node(state: dict, config: RunnableConfig | None = None):
"""Performs the tool call""" """Performs the tool call"""
# Extract jwt_token from config for tools to use
configurable = config.get("configurable", {}) if config else {}
jwt_token = configurable.get("jwt_token")
# Set jwt_token for connector_factory to access
if jwt_token:
from gns3server.agent.gns3_copilot.gns3_client import set_current_jwt_token
set_current_jwt_token(jwt_token)
result = [] result = []
for tool_call in state["messages"][-1].tool_calls: for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]] tool = tools_by_name[tool_call["name"]]

View File

@ -123,12 +123,18 @@ class AgentService:
Yields: Yields:
Dict containing SSE-compatible response chunks Dict containing SSE-compatible response chunks
""" """
# Build config with LLM configuration and metadata # 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)
if llm_config:
from gns3server.agent.gns3_copilot.gns3_client import set_current_llm_config
set_current_llm_config(llm_config)
# Build config - only thread-safe identifiers
config = { config = {
"configurable": { "configurable": {
"thread_id": session_id, "thread_id": session_id,
"jwt_token": jwt_token,
"llm_config": llm_config,
}, },
"metadata": { "metadata": {
"user_id": user_id, "user_id": user_id,

View File

@ -27,6 +27,8 @@ from .connector_factory import (
get_llm_config, get_llm_config,
set_current_jwt_token, set_current_jwt_token,
get_current_jwt_token, get_current_jwt_token,
set_current_llm_config,
get_current_llm_config,
) )
from .custom_gns3fy import ( from .custom_gns3fy import (
CONSOLE_TYPES, CONSOLE_TYPES,
@ -66,4 +68,6 @@ __all__ = [
"get_llm_config", "get_llm_config",
"set_current_jwt_token", "set_current_jwt_token",
"get_current_jwt_token", "get_current_jwt_token",
"set_current_llm_config",
"get_current_llm_config",
] ]

View File

@ -30,9 +30,10 @@ import logging
from typing import Optional from typing import Optional
from contextvars import ContextVar from contextvars import ContextVar
# Context variable for request-scoped jwt_token # Context variables for request-scoped data
# Automatically cleaned up when request context ends # Automatically cleaned up when request context ends
_jwt_token_context: ContextVar[Optional[str]] = ContextVar("_jwt_token_context", default=None) _jwt_token_context: ContextVar[Optional[str]] = ContextVar("_jwt_token_context", default=None)
_llm_config_context: ContextVar[Optional[dict]] = ContextVar("_llm_config_context", default=None)
def set_current_jwt_token(token: str) -> None: def set_current_jwt_token(token: str) -> None:
@ -43,6 +44,16 @@ def set_current_jwt_token(token: str) -> None:
def get_current_jwt_token() -> Optional[str]: def get_current_jwt_token() -> Optional[str]:
"""Get the JWT token for the current request context.""" """Get the JWT token for the current request context."""
return _jwt_token_context.get() return _jwt_token_context.get()
def set_current_llm_config(config: dict) -> None:
"""Set the LLM config for the current request context."""
_llm_config_context.set(config)
def get_current_llm_config() -> Optional[dict]:
"""Get the LLM config for the current request context."""
return _llm_config_context.get()
from uuid import UUID from uuid import UUID
from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Gns3Connector from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Gns3Connector