From 425e6e50a3b7a73e9fd4cc19eef06a2859eef41a Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 4 Jun 2026 20:02:19 +0200 Subject: [PATCH] refactor(agents): colocate luma connector tools into subagent slice The luma subagent already ran its own local tool impls (tools/index.py imports the local create_event/list_events/read_event). The shared/tools/luma copies were a dead twin, only referenced by the subagent's unused tools/__init__ shim. Repoint that shim at the local modules and delete the dead shared copies. No runtime behavior change. --- .../connectors/luma/tools/__init__.py | 12 +- .../app/agents/shared/tools/luma/__init__.py | 15 -- .../app/agents/shared/tools/luma/_auth.py | 39 ----- .../agents/shared/tools/luma/create_event.py | 150 ------------------ .../agents/shared/tools/luma/list_events.py | 133 ---------------- .../agents/shared/tools/luma/read_event.py | 114 ------------- 6 files changed, 3 insertions(+), 460 deletions(-) delete mode 100644 surfsense_backend/app/agents/shared/tools/luma/__init__.py delete mode 100644 surfsense_backend/app/agents/shared/tools/luma/_auth.py delete mode 100644 surfsense_backend/app/agents/shared/tools/luma/create_event.py delete mode 100644 surfsense_backend/app/agents/shared/tools/luma/list_events.py delete mode 100644 surfsense_backend/app/agents/shared/tools/luma/read_event.py diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py index 83af8c8c5..c089eab4b 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py @@ -1,12 +1,6 @@ -from app.agents.shared.tools.luma.create_event import ( - create_create_luma_event_tool, -) -from app.agents.shared.tools.luma.list_events import ( - create_list_luma_events_tool, -) -from app.agents.shared.tools.luma.read_event import ( - create_read_luma_event_tool, -) +from .create_event import create_create_luma_event_tool +from .list_events import create_list_luma_events_tool +from .read_event import create_read_luma_event_tool __all__ = [ "create_create_luma_event_tool", diff --git a/surfsense_backend/app/agents/shared/tools/luma/__init__.py b/surfsense_backend/app/agents/shared/tools/luma/__init__.py deleted file mode 100644 index 83af8c8c5..000000000 --- a/surfsense_backend/app/agents/shared/tools/luma/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from app.agents.shared.tools.luma.create_event import ( - create_create_luma_event_tool, -) -from app.agents.shared.tools.luma.list_events import ( - create_list_luma_events_tool, -) -from app.agents.shared.tools.luma.read_event import ( - create_read_luma_event_tool, -) - -__all__ = [ - "create_create_luma_event_tool", - "create_list_luma_events_tool", - "create_read_luma_event_tool", -] diff --git a/surfsense_backend/app/agents/shared/tools/luma/_auth.py b/surfsense_backend/app/agents/shared/tools/luma/_auth.py deleted file mode 100644 index 37deb1525..000000000 --- a/surfsense_backend/app/agents/shared/tools/luma/_auth.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Shared auth helper for Luma agent tools.""" - -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.future import select - -from app.db import SearchSourceConnector, SearchSourceConnectorType - -LUMA_API = "https://public-api.luma.com/v1" - - -async def get_luma_connector( - db_session: AsyncSession, - search_space_id: int, - user_id: str, -) -> SearchSourceConnector | None: - result = await db_session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, - SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type - == SearchSourceConnectorType.LUMA_CONNECTOR, - ) - ) - return result.scalars().first() - - -def get_api_key(connector: SearchSourceConnector) -> str: - """Extract the API key from connector config (handles both key names).""" - key = connector.config.get("api_key") or connector.config.get("LUMA_API_KEY") - if not key: - raise ValueError("Luma API key not found in connector config.") - return key - - -def luma_headers(api_key: str) -> dict[str, str]: - return { - "Content-Type": "application/json", - "x-luma-api-key": api_key, - } diff --git a/surfsense_backend/app/agents/shared/tools/luma/create_event.py b/surfsense_backend/app/agents/shared/tools/luma/create_event.py deleted file mode 100644 index d4c47535e..000000000 --- a/surfsense_backend/app/agents/shared/tools/luma/create_event.py +++ /dev/null @@ -1,150 +0,0 @@ -import logging -from typing import Any - -import httpx -from langchain_core.tools import tool -from sqlalchemy.ext.asyncio import AsyncSession - -from app.agents.shared.tools.hitl import request_approval -from app.db import async_session_maker - -from ._auth import LUMA_API, get_api_key, get_luma_connector, luma_headers - -logger = logging.getLogger(__name__) - - -def create_create_luma_event_tool( - db_session: AsyncSession | None = None, - search_space_id: int | None = None, - user_id: str | None = None, -): - """ - Factory function to create the create_luma_event tool. - - The tool acquires its own short-lived ``AsyncSession`` per call via - :data:`async_session_maker` so the closure is safe to share across - HTTP requests by the compiled-agent cache. Capturing a per-request - session here would surface stale/closed sessions on cache hits. - - Args: - db_session: Reserved for registry compatibility. Per-call sessions - are opened via :data:`async_session_maker` inside the tool body. - - Returns: - Configured create_luma_event tool - """ - del db_session # per-call session — see docstring - - @tool - async def create_luma_event( - name: str, - start_at: str, - end_at: str, - description: str | None = None, - timezone: str = "UTC", - ) -> dict[str, Any]: - """Create a new event on Luma. - - Args: - name: The event title. - start_at: Start time in ISO 8601 format (e.g. "2026-05-01T18:00:00"). - end_at: End time in ISO 8601 format (e.g. "2026-05-01T20:00:00"). - description: Optional event description (markdown supported). - timezone: Timezone string (default "UTC", e.g. "America/New_York"). - - Returns: - Dictionary with status, event_id on success. - - IMPORTANT: - - If status is "rejected", the user explicitly declined. Do NOT retry. - """ - if search_space_id is None or user_id is None: - return {"status": "error", "message": "Luma tool not properly configured."} - - try: - async with async_session_maker() as db_session: - connector = await get_luma_connector( - db_session, search_space_id, user_id - ) - if not connector: - return {"status": "error", "message": "No Luma connector found."} - - result = request_approval( - action_type="luma_create_event", - tool_name="create_luma_event", - params={ - "name": name, - "start_at": start_at, - "end_at": end_at, - "description": description, - "timezone": timezone, - }, - context={"connector_id": connector.id}, - ) - - if result.rejected: - return { - "status": "rejected", - "message": "User declined. Event was not created.", - } - - final_name = result.params.get("name", name) - final_start = result.params.get("start_at", start_at) - final_end = result.params.get("end_at", end_at) - final_desc = result.params.get("description", description) - final_tz = result.params.get("timezone", timezone) - - api_key = get_api_key(connector) - headers = luma_headers(api_key) - - body: dict[str, Any] = { - "name": final_name, - "start_at": final_start, - "end_at": final_end, - "timezone": final_tz, - } - if final_desc: - body["description_md"] = final_desc - - async with httpx.AsyncClient(timeout=20.0) as client: - resp = await client.post( - f"{LUMA_API}/event/create", - headers=headers, - json=body, - ) - - if resp.status_code == 401: - return { - "status": "auth_error", - "message": "Luma API key is invalid.", - "connector_type": "luma", - } - if resp.status_code == 403: - return { - "status": "error", - "message": "Luma Plus subscription required to create events via API.", - } - if resp.status_code not in (200, 201): - return { - "status": "error", - "message": f"Luma API error: {resp.status_code} — {resp.text[:200]}", - } - - data = resp.json() - event_id = data.get("api_id") or data.get("event", {}).get("api_id") - - return { - "status": "success", - "event_id": event_id, - "message": f"Event '{final_name}' created on Luma.", - } - - except Exception as e: - from langgraph.errors import GraphInterrupt - - if isinstance(e, GraphInterrupt): - raise - logger.error("Error creating Luma event: %s", e, exc_info=True) - return {"status": "error", "message": "Failed to create Luma event."} - - return create_luma_event diff --git a/surfsense_backend/app/agents/shared/tools/luma/list_events.py b/surfsense_backend/app/agents/shared/tools/luma/list_events.py deleted file mode 100644 index 6885c2049..000000000 --- a/surfsense_backend/app/agents/shared/tools/luma/list_events.py +++ /dev/null @@ -1,133 +0,0 @@ -import logging -from typing import Any - -import httpx -from langchain_core.tools import tool -from sqlalchemy.ext.asyncio import AsyncSession - -from app.db import async_session_maker - -from ._auth import LUMA_API, get_api_key, get_luma_connector, luma_headers - -logger = logging.getLogger(__name__) - - -def create_list_luma_events_tool( - db_session: AsyncSession | None = None, - search_space_id: int | None = None, - user_id: str | None = None, -): - """ - Factory function to create the list_luma_events tool. - - The tool acquires its own short-lived ``AsyncSession`` per call via - :data:`async_session_maker` so the closure is safe to share across - HTTP requests by the compiled-agent cache. Capturing a per-request - session here would surface stale/closed sessions on cache hits. - - Args: - db_session: Reserved for registry compatibility. Per-call sessions - are opened via :data:`async_session_maker` inside the tool body. - - Returns: - Configured list_luma_events tool - """ - del db_session # per-call session — see docstring - - @tool - async def list_luma_events( - max_results: int = 25, - ) -> dict[str, Any]: - """List upcoming and recent Luma events. - - Args: - max_results: Maximum events to return (default 25, max 50). - - Returns: - Dictionary with status and a list of events including - event_id, name, start_at, end_at, location, url. - """ - if search_space_id is None or user_id is None: - return {"status": "error", "message": "Luma tool not properly configured."} - - max_results = min(max_results, 50) - - try: - async with async_session_maker() as db_session: - connector = await get_luma_connector( - db_session, search_space_id, user_id - ) - if not connector: - return {"status": "error", "message": "No Luma connector found."} - - api_key = get_api_key(connector) - headers = luma_headers(api_key) - - all_entries: list[dict] = [] - cursor = None - - async with httpx.AsyncClient(timeout=20.0) as client: - while len(all_entries) < max_results: - params: dict[str, Any] = { - "limit": min(100, max_results - len(all_entries)) - } - if cursor: - params["cursor"] = cursor - - resp = await client.get( - f"{LUMA_API}/calendar/list-events", - headers=headers, - params=params, - ) - - if resp.status_code == 401: - return { - "status": "auth_error", - "message": "Luma API key is invalid.", - "connector_type": "luma", - } - if resp.status_code != 200: - return { - "status": "error", - "message": f"Luma API error: {resp.status_code}", - } - - data = resp.json() - entries = data.get("entries", []) - if not entries: - break - all_entries.extend(entries) - - next_cursor = data.get("next_cursor") - if not next_cursor: - break - cursor = next_cursor - - events = [] - for entry in all_entries[:max_results]: - ev = entry.get("event", {}) - geo = ev.get("geo_info", {}) - events.append( - { - "event_id": entry.get("api_id"), - "name": ev.get("name", "Untitled"), - "start_at": ev.get("start_at", ""), - "end_at": ev.get("end_at", ""), - "timezone": ev.get("timezone", ""), - "location": geo.get("name", ""), - "url": ev.get("url", ""), - "visibility": ev.get("visibility", ""), - } - ) - - return {"status": "success", "events": events, "total": len(events)} - - except Exception as e: - from langgraph.errors import GraphInterrupt - - if isinstance(e, GraphInterrupt): - raise - logger.error("Error listing Luma events: %s", e, exc_info=True) - return {"status": "error", "message": "Failed to list Luma events."} - - return list_luma_events diff --git a/surfsense_backend/app/agents/shared/tools/luma/read_event.py b/surfsense_backend/app/agents/shared/tools/luma/read_event.py deleted file mode 100644 index a8484e9c0..000000000 --- a/surfsense_backend/app/agents/shared/tools/luma/read_event.py +++ /dev/null @@ -1,114 +0,0 @@ -import logging -from typing import Any - -import httpx -from langchain_core.tools import tool -from sqlalchemy.ext.asyncio import AsyncSession - -from app.db import async_session_maker - -from ._auth import LUMA_API, get_api_key, get_luma_connector, luma_headers - -logger = logging.getLogger(__name__) - - -def create_read_luma_event_tool( - db_session: AsyncSession | None = None, - search_space_id: int | None = None, - user_id: str | None = None, -): - """ - Factory function to create the read_luma_event tool. - - The tool acquires its own short-lived ``AsyncSession`` per call via - :data:`async_session_maker` so the closure is safe to share across - HTTP requests by the compiled-agent cache. Capturing a per-request - session here would surface stale/closed sessions on cache hits. - - Args: - db_session: Reserved for registry compatibility. Per-call sessions - are opened via :data:`async_session_maker` inside the tool body. - - Returns: - Configured read_luma_event tool - """ - del db_session # per-call session — see docstring - - @tool - async def read_luma_event(event_id: str) -> dict[str, Any]: - """Read detailed information about a specific Luma event. - - Args: - event_id: The Luma event API ID (from list_luma_events). - - Returns: - Dictionary with status and full event details including - description, attendees count, meeting URL. - """ - if search_space_id is None or user_id is None: - return {"status": "error", "message": "Luma tool not properly configured."} - - try: - async with async_session_maker() as db_session: - connector = await get_luma_connector( - db_session, search_space_id, user_id - ) - if not connector: - return {"status": "error", "message": "No Luma connector found."} - - api_key = get_api_key(connector) - headers = luma_headers(api_key) - - async with httpx.AsyncClient(timeout=15.0) as client: - resp = await client.get( - f"{LUMA_API}/events/{event_id}", - headers=headers, - ) - - if resp.status_code == 401: - return { - "status": "auth_error", - "message": "Luma API key is invalid.", - "connector_type": "luma", - } - if resp.status_code == 404: - return { - "status": "not_found", - "message": f"Event '{event_id}' not found.", - } - if resp.status_code != 200: - return { - "status": "error", - "message": f"Luma API error: {resp.status_code}", - } - - data = resp.json() - ev = data.get("event", data) - geo = ev.get("geo_info", {}) - - event_detail = { - "event_id": event_id, - "name": ev.get("name", ""), - "description": ev.get("description", ""), - "start_at": ev.get("start_at", ""), - "end_at": ev.get("end_at", ""), - "timezone": ev.get("timezone", ""), - "location_name": geo.get("name", ""), - "address": geo.get("address", ""), - "url": ev.get("url", ""), - "meeting_url": ev.get("meeting_url", ""), - "visibility": ev.get("visibility", ""), - "cover_url": ev.get("cover_url", ""), - } - - return {"status": "success", "event": event_detail} - - except Exception as e: - from langgraph.errors import GraphInterrupt - - if isinstance(e, GraphInterrupt): - raise - logger.error("Error reading Luma event: %s", e, exc_info=True) - return {"status": "error", "message": "Failed to read Luma event."} - - return read_luma_event