diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py index c5839b8f2..f85ea3dfd 100644 --- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py +++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py @@ -214,12 +214,10 @@ def llm_call(state: dict, config: RunnableConfig | None = None): llm_config=llm_config ) - # Store jwt_token in state for Tools to use when calling GNS3 API return { "messages": [model_with_tools.invoke(full_messages)], "llm_calls": state.get("llm_calls", 0) + 1, "topology_info": topology_info, - "jwt_token": jwt_token, } @@ -305,9 +303,18 @@ def generate_title(state: MessagesState, config: RunnableConfig | None = None) - # Define tool node -def tool_node(state: dict): +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 a3a9cda56..1a409931a 100644 --- a/gns3server/agent/gns3_copilot/agent_service.py +++ b/gns3server/agent/gns3_copilot/agent_service.py @@ -46,7 +46,7 @@ class AgentService: def _get_checkpoint_dir(self) -> str: """Get or create the checkpoint directory for this project.""" - checkpoint_dir = os.path.join(self.project_path, ".gns3-copilot") + checkpoint_dir = os.path.join(self.project_path, "gns3-copilot") os.makedirs(checkpoint_dir, exist_ok=True) return checkpoint_dir @@ -103,6 +103,7 @@ class AgentService: message: str, session_id: str, project_id: Optional[str] = None, + user_id: Optional[str] = None, jwt_token: Optional[str] = None, mode: str = "text", llm_config: Optional[Dict[str, Any]] = None @@ -114,6 +115,7 @@ class AgentService: message: User message session_id: Session/thread ID for conversation continuity project_id: GNS3 project ID (optional, for context) + user_id: User ID for metadata tracking jwt_token: JWT token for API authentication (optional) mode: Interaction mode (default: "text") llm_config: LLM configuration dict (provider, model, api_key, etc.) @@ -121,12 +123,16 @@ class AgentService: Yields: Dict containing SSE-compatible response chunks """ - # Build config with LLM configuration + # Build config with LLM configuration and metadata config = { "configurable": { "thread_id": session_id, "jwt_token": jwt_token, "llm_config": llm_config, + }, + "metadata": { + "user_id": user_id, + "project_id": project_id, } } diff --git a/gns3server/agent/gns3_copilot/gns3_client/__init__.py b/gns3server/agent/gns3_copilot/gns3_client/__init__.py index 309dd47ef..2bc89f76b 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/__init__.py +++ b/gns3server/agent/gns3_copilot/gns3_client/__init__.py @@ -25,6 +25,8 @@ from .connector_factory import ( get_gns3_connector_with_llm_config, get_gns3_server_host, get_llm_config, + set_current_jwt_token, + get_current_jwt_token, ) from .custom_gns3fy import ( CONSOLE_TYPES, @@ -62,4 +64,6 @@ __all__ = [ "get_gns3_connector_with_llm_config", "get_gns3_server_host", "get_llm_config", + "set_current_jwt_token", + "get_current_jwt_token", ] diff --git a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py index 70b809c1b..0ad8b0d03 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py +++ b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py @@ -28,6 +28,21 @@ Authentication: import logging from typing import Optional +from contextvars import ContextVar + +# Context variable for request-scoped jwt_token +# Automatically cleaned up when request context ends +_jwt_token_context: ContextVar[Optional[str]] = ContextVar("_jwt_token_context", default=None) + + +def set_current_jwt_token(token: str) -> None: + """Set the JWT token for the current request context.""" + _jwt_token_context.set(token) + + +def get_current_jwt_token() -> Optional[str]: + """Get the JWT token for the current request context.""" + return _jwt_token_context.get() from uuid import UUID from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Gns3Connector @@ -136,9 +151,12 @@ def get_gns3_connector(jwt_token: str, url: Optional[str] = None) -> Optional[Gn """ try: # Validate JWT token + # If not provided, try to get from current request context if not jwt_token: - logger.error("JWT token parameter is required") - return None + jwt_token = get_current_jwt_token() + if not jwt_token: + logger.error("JWT token is required") + return None # Resolve URL with fallback strategy if url is None: diff --git a/gns3server/api/routes/controller/chat.py b/gns3server/api/routes/controller/chat.py index 56a329f0e..1e8030aa5 100644 --- a/gns3server/api/routes/controller/chat.py +++ b/gns3server/api/routes/controller/chat.py @@ -117,6 +117,7 @@ async def stream_chat( message=request.message, session_id=session_id, project_id=str(project.id), + user_id=user_id, jwt_token=jwt_token, mode=request.mode, llm_config=llm_config