From eec4ebe3fd6f4e2939d5e2cc582b23e82f28c174 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 4 Mar 2026 13:49:17 +0800 Subject: [PATCH] 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 --- .../agent/gns3_copilot/agent/gns3_copilot.py | 34 ++++++++++--------- .../agent/gns3_copilot/agent_service.py | 12 +++++-- .../gns3_copilot/gns3_client/__init__.py | 4 +++ .../gns3_client/connector_factory.py | 13 ++++++- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py index f85ea3dfd..efd759a6c 100644 --- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py +++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py @@ -128,10 +128,17 @@ class MessagesState(TypedDict): def llm_call(state: dict, config: RunnableConfig | None = None): """LLM decides whether to call a tool or not""" - # Extract config from LangGraph config - configurable = config.get("configurable", {}) if config else {} - jwt_token = configurable.get("jwt_token") - llm_config = configurable.get("llm_config") + # 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() + + 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 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). """ - # Extract config from LangGraph config - configurable = config.get("configurable", {}) if config else {} - llm_config = configurable.get("llm_config") + # 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() + + 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 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): """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 = [] for tool_call in state["messages"][-1].tool_calls: tool = tools_by_name[tool_call["name"]] diff --git a/gns3server/agent/gns3_copilot/agent_service.py b/gns3server/agent/gns3_copilot/agent_service.py index 1a409931a..3710ae5b0 100644 --- a/gns3server/agent/gns3_copilot/agent_service.py +++ b/gns3server/agent/gns3_copilot/agent_service.py @@ -123,12 +123,18 @@ class AgentService: Yields: 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 = { "configurable": { "thread_id": session_id, - "jwt_token": jwt_token, - "llm_config": llm_config, }, "metadata": { "user_id": user_id, diff --git a/gns3server/agent/gns3_copilot/gns3_client/__init__.py b/gns3server/agent/gns3_copilot/gns3_client/__init__.py index 2bc89f76b..0ffa69c1d 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/__init__.py +++ b/gns3server/agent/gns3_copilot/gns3_client/__init__.py @@ -27,6 +27,8 @@ from .connector_factory import ( get_llm_config, set_current_jwt_token, get_current_jwt_token, + set_current_llm_config, + get_current_llm_config, ) from .custom_gns3fy import ( CONSOLE_TYPES, @@ -66,4 +68,6 @@ __all__ = [ "get_llm_config", "set_current_jwt_token", "get_current_jwt_token", + "set_current_llm_config", + "get_current_llm_config", ] diff --git a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py index 0ad8b0d03..7baf0fe16 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py +++ b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py @@ -30,9 +30,10 @@ import logging from typing import Optional from contextvars import ContextVar -# Context variable for request-scoped jwt_token +# 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) +_llm_config_context: ContextVar[Optional[dict]] = ContextVar("_llm_config_context", default=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]: """Get the JWT token for the current request context.""" 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 gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Gns3Connector