diff --git a/surfsense_backend/app/agents/chat/anonymous_chat/agent.py b/surfsense_backend/app/agents/chat/anonymous_chat/agent.py index 250b4c158..eb0f46037 100644 --- a/surfsense_backend/app/agents/chat/anonymous_chat/agent.py +++ b/surfsense_backend/app/agents/chat/anonymous_chat/agent.py @@ -124,7 +124,7 @@ async def create_anonymous_chat_agent( tools (used when the user toggles web search off). """ tools = ( - [create_web_search_tool(search_space_id=None, available_connectors=None)] + [create_web_search_tool(workspace_id=None, available_connectors=None)] if enable_web_search else [] ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/graph/compile_graph_sync.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/graph/compile_graph_sync.py index e3ab50e8c..2bf393d1f 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/graph/compile_graph_sync.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/graph/compile_graph_sync.py @@ -31,7 +31,7 @@ def build_compiled_agent_graph_sync( final_system_prompt: str, backend_resolver: Any, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, user_id: str | None, thread_id: int | None, visibility: ChatVisibility, @@ -53,7 +53,7 @@ def build_compiled_agent_graph_sync( tools=tools, backend_resolver=backend_resolver, filesystem_mode=filesystem_mode, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, thread_id=thread_id, visibility=visibility, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/builder.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/builder.py index 9213f1339..6a2c737d8 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/builder.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/builder.py @@ -14,7 +14,7 @@ def build_action_log_mw( *, flags: AgentFeatureFlags, thread_id: int | None, - search_space_id: int, + workspace_id: int, user_id: str | None, ) -> ActionLogMiddleware | None: if not enabled(flags, "enable_action_log") or thread_id is None: @@ -25,7 +25,7 @@ def build_action_log_mw( # tool via ``ToolDefinition.reverse`` and can be wired here when used. return ActionLogMiddleware( thread_id=thread_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) except Exception: # pragma: no cover - defensive diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/middleware.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/middleware.py index 2cce7eb53..918d6c784 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/middleware.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/action_log/middleware.py @@ -84,7 +84,7 @@ class ActionLogMiddleware(AgentMiddleware): Args: thread_id: The current chat thread's primary-key id. Required to persist a row; if ``None`` the middleware silently no-ops. - search_space_id: Search-space id for cascade-on-delete safety. + workspace_id: Workspace id for cascade-on-delete safety. user_id: UUID string of the user driving this turn (nullable in anonymous mode). tool_definitions: Optional mapping of tool name -> :class:`ToolDefinition` @@ -98,13 +98,13 @@ class ActionLogMiddleware(AgentMiddleware): self, *, thread_id: int | None, - search_space_id: int, + workspace_id: int, user_id: str | None, tool_definitions: dict[str, ToolDefinition] | None = None, ) -> None: super().__init__() self._thread_id = thread_id - self._search_space_id = search_space_id + self._workspace_id = workspace_id self._user_id = user_id self._tool_definitions = dict(tool_definitions or {}) @@ -187,7 +187,7 @@ class ActionLogMiddleware(AgentMiddleware): row = AgentActionLog( thread_id=thread_id, user_id=self._user_id, - search_space_id=self._search_space_id, + workspace_id=self._workspace_id, # ``turn_id`` is the deprecated alias of ``tool_call_id`` # kept for one release for safe rollback. New consumers # should read ``tool_call_id`` directly. diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/middleware.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/middleware.py index ab6c3a1f5..8c4117bf9 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/middleware.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/middleware.py @@ -40,7 +40,7 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware): subagents: list[SubAgent | CompiledSubAgent], system_prompt: str | None = TASK_SYSTEM_PROMPT, task_description: str | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, ) -> None: self._surf_checkpointer = checkpointer super(SubAgentMiddleware, self).__init__() @@ -50,11 +50,11 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware): ) self._backend = backend self._subagents = subagents - # Search-space id is captured at build time (the orchestrator runs in - # exactly one search space for its lifetime). The spawn-paused kill + # Workspace id is captured at build time (the orchestrator runs in + # exactly one workspace for its lifetime). The spawn-paused kill # switch keys on it so an operator can quarantine one workspace # without affecting the rest of the deployment. - self._search_space_id = search_space_id + self._workspace_id = workspace_id # Lazy subagent compilation. Compiling a subagent graph via # ``create_agent`` is expensive (~250-400ms each) and there can be up @@ -75,7 +75,7 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware): task_tool = build_task_tool_with_parent_config( descriptors, task_description, - search_space_id=search_space_id, + workspace_id=workspace_id, resolve_subagent=self._resolve_subagent, ) if system_prompt and descriptors: diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/spawn_paused.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/spawn_paused.py index 2c9e114e0..50f91dfc4 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/spawn_paused.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/spawn_paused.py @@ -1,12 +1,12 @@ -"""Per-search-space spawn-paused kill switch for the ``task`` boundary. +"""Per-workspace spawn-paused kill switch for the ``task`` boundary. When operators see a runaway loop, a vendor outage, or a billing event that requires immediate cessation of subagent traffic for a specific workspace, they flip a Redis flag and the ``task`` tool short-circuits -without touching downstream services. The flag is **per-search-space** +without touching downstream services. The flag is **per-workspace** so one tenant's incident never silences the rest of the deployment. -Flag key: ``surfsense:spawn_paused:{search_space_id}`` +Flag key: ``surfsense:spawn_paused:{workspace_id}`` Flag value: any string-truthy value (we read presence, not contents). TTL: set by whoever toggles the flag — this module never expires keys on its own, since "the flag is on" is itself the signal @@ -43,18 +43,18 @@ _DISABLED = os.environ.get( } -def _flag_key(search_space_id: int) -> str: - return f"surfsense:spawn_paused:{search_space_id}" +def _flag_key(workspace_id: int) -> str: + return f"surfsense:spawn_paused:{workspace_id}" -async def is_spawn_paused(search_space_id: int | None) -> bool: +async def is_spawn_paused(workspace_id: int | None) -> bool: """Return ``True`` iff the workspace's spawn-paused flag is set in Redis. - A ``None`` search-space (e.g. dev paths that did not plumb the id + A ``None`` workspace (e.g. dev paths that did not plumb the id through yet) bypasses the check. So does a Redis outage — see module docstring for the fail-open rationale. """ - if _DISABLED or search_space_id is None: + if _DISABLED or workspace_id is None: return False try: # Local import keeps the cold-path import cheap and lets routes @@ -63,7 +63,7 @@ async def is_spawn_paused(search_space_id: int | None) -> bool: client = aioredis.from_url(config.REDIS_APP_URL, decode_responses=True) try: - raw = await client.get(_flag_key(search_space_id)) + raw = await client.get(_flag_key(workspace_id)) finally: # ``aclose()`` is the async-safe variant on redis-py >=5; fall back # to ``close()`` for older clients pinned in tests. @@ -74,8 +74,8 @@ async def is_spawn_paused(search_space_id: int | None) -> bool: return bool(raw) except Exception: logger.warning( - "spawn_paused check failed for search_space_id=%s; failing open.", - search_space_id, + "spawn_paused check failed for workspace_id=%s; failing open.", + workspace_id, exc_info=True, ) return False diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/task_tool.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/task_tool.py index 6698211f7..a08e6cda1 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/task_tool.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/checkpointed_subagent_middleware/task_tool.py @@ -142,7 +142,7 @@ def build_task_tool_with_parent_config( subagents: list[dict[str, Any]], task_description: str | None = None, *, - search_space_id: int | None = None, + workspace_id: int | None = None, resolve_subagent: Callable[[str], Runnable] | None = None, ) -> BaseTool: """Upstream ``_build_task_tool`` + parent ``runtime.config`` propagation + resume bridging. @@ -829,10 +829,10 @@ def build_task_tool_with_parent_config( atask_start = time.perf_counter() # Ops kill switch: short-circuit every task() call for this workspace # so the orchestrator stops hammering downstream APIs. - if await is_spawn_paused(search_space_id): + if await is_spawn_paused(workspace_id): logger.warning( - "[hitl_route] atask SPAWN_PAUSED: search_space_id=%s tool_call_id=%s", - search_space_id, + "[hitl_route] atask SPAWN_PAUSED: workspace_id=%s tool_call_id=%s", + workspace_id, runtime.tool_call_id, ) return ( diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/builder.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/builder.py index 7e8e06570..eea2a1073 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/builder.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/builder.py @@ -12,14 +12,14 @@ from .middleware import ( def build_kb_persistence_mw( *, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, user_id: str | None, thread_id: int | None, ) -> KnowledgeBasePersistenceMiddleware | None: if filesystem_mode != FilesystemMode.CLOUD: return None return KnowledgeBasePersistenceMiddleware( - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, filesystem_mode=filesystem_mode, thread_id=thread_id, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/middleware.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/middleware.py index a6c83a7d4..0554dc747 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/middleware.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/middleware.py @@ -83,11 +83,11 @@ def _basename(path: str) -> str: async def _ensure_folder_hierarchy( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, created_by_id: str | None, folder_parts: list[str], ) -> int | None: - """Ensure a chain of folder names exists under the search space. + """Ensure a chain of folder names exists under the workspace. Returns the leaf folder id, or ``None`` if ``folder_parts`` is empty (i.e. a document directly under ``/documents/``). @@ -98,7 +98,7 @@ async def _ensure_folder_hierarchy( for raw in folder_parts: name = safe_folder_segment(str(raw)) query = select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.name == name, ) if parent_id is None: @@ -112,7 +112,7 @@ async def _ensure_folder_hierarchy( select(Folder.position).order_by(Folder.position.desc()).limit(1) ) sibling_query = sibling_query.where( - Folder.search_space_id == search_space_id + Folder.workspace_id == workspace_id ) if parent_id is None: sibling_query = sibling_query.where(Folder.parent_id.is_(None)) @@ -124,7 +124,7 @@ async def _ensure_folder_hierarchy( name=name, position=generate_key_between(last_position, None), parent_id=parent_id, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=created_by_id, updated_at=datetime.now(UTC), ) @@ -137,7 +137,7 @@ async def _ensure_folder_hierarchy( async def _resolve_folder_id( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, folder_parts: list[str], ) -> int | None: """Look up an existing folder chain without creating anything. @@ -151,7 +151,7 @@ async def _resolve_folder_id( for raw in folder_parts: name = safe_folder_segment(str(raw)) query = select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.name == name, ) query = ( @@ -185,7 +185,7 @@ async def _create_document( *, virtual_path: str, content: str, - search_space_id: int, + workspace_id: int, created_by_id: str | None, ) -> Document: """Create a NOTE Document + Chunks for ``virtual_path``.""" @@ -194,21 +194,21 @@ async def _create_document( raise ValueError(f"invalid /documents path '{virtual_path}'") folder_id = await _ensure_folder_hierarchy( session, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=created_by_id, folder_parts=folder_parts, ) unique_identifier_hash = generate_unique_identifier_hash( DocumentType.NOTE, virtual_path, - search_space_id, + workspace_id, ) # Pre-check the path-derived unique_identifier_hash so a duplicate path # surfaces as a clean ValueError instead of an INSERT IntegrityError that # poisons the session. Content is intentionally not unique (cp a b). path_collision = await session.execute( select(Document.id).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.unique_identifier_hash == unique_identifier_hash, ) ) @@ -217,7 +217,7 @@ async def _create_document( f"a document already exists at path '{virtual_path}' " "(unique_identifier_hash collision)" ) - content_hash = generate_content_hash(content, search_space_id) + content_hash = generate_content_hash(content, workspace_id) doc = Document( title=title, document_type=DocumentType.NOTE, @@ -226,7 +226,7 @@ async def _create_document( content_hash=content_hash, unique_identifier_hash=unique_identifier_hash, source_markdown=content, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_id=folder_id, created_by_id=created_by_id, updated_at=datetime.now(UTC), @@ -261,13 +261,13 @@ async def _update_document( doc_id: int, content: str, virtual_path: str, - search_space_id: int, + workspace_id: int, ) -> Document | None: """Update an existing Document's content + chunks.""" result = await session.execute( select(Document).where( Document.id == doc_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document = result.scalar_one_or_none() @@ -276,7 +276,7 @@ async def _update_document( document.content = content document.source_markdown = content - document.content_hash = generate_content_hash(content, search_space_id) + document.content_hash = generate_content_hash(content, workspace_id) document.updated_at = datetime.now(UTC) metadata = dict(document.document_metadata or {}) metadata["virtual_path"] = virtual_path @@ -284,7 +284,7 @@ async def _update_document( document.unique_identifier_hash = generate_unique_identifier_hash( DocumentType.NOTE, virtual_path, - search_space_id, + workspace_id, ) summary_embedding = (await asyncio.to_thread(embed_texts, [content]))[0] @@ -318,7 +318,7 @@ async def _update_document( async def _apply_move( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, created_by_id: str | None, move: dict[str, Any], doc_id_by_path: dict[str, int], @@ -341,14 +341,14 @@ async def _apply_move( result = await session.execute( select(Document).where( Document.id == doc_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document = result.scalar_one_or_none() if document is None: document = await virtual_path_to_doc( session, - search_space_id=search_space_id, + workspace_id=workspace_id, virtual_path=source, ) if document is None: @@ -364,7 +364,7 @@ async def _apply_move( return None folder_id = await _ensure_folder_hierarchy( session, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=created_by_id, folder_parts=folder_parts, ) @@ -377,7 +377,7 @@ async def _apply_move( document.unique_identifier_hash = generate_unique_identifier_hash( DocumentType.NOTE, dest, - search_space_id, + workspace_id, ) document.updated_at = datetime.now(UTC) @@ -501,7 +501,7 @@ async def _snapshot_document_pre_write( *, doc: Document, action_id: int | None, - search_space_id: int, + workspace_id: int, turn_id: str | None = None, deferred_dispatches: list[int] | None = None, ) -> int | None: @@ -517,7 +517,7 @@ async def _snapshot_document_pre_write( payload = _doc_revision_payload(doc, chunks_before=chunks) rev = DocumentRevision( document_id=doc.id, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_turn_id=turn_id, agent_action_id=action_id, **payload, @@ -544,7 +544,7 @@ async def _snapshot_document_pre_create( session: AsyncSession, *, action_id: int | None, - search_space_id: int, + workspace_id: int, turn_id: str | None = None, deferred_dispatches: list[int] | None = None, ) -> int | None: @@ -558,7 +558,7 @@ async def _snapshot_document_pre_create( async with session.begin_nested(): rev = DocumentRevision( document_id=None, - search_space_id=search_space_id, + workspace_id=workspace_id, content_before=None, title_before=None, folder_id_before=None, @@ -586,7 +586,7 @@ async def _snapshot_document_pre_move( *, doc: Document, action_id: int | None, - search_space_id: int, + workspace_id: int, turn_id: str | None = None, deferred_dispatches: list[int] | None = None, ) -> int | None: @@ -596,7 +596,7 @@ async def _snapshot_document_pre_move( payload = _doc_revision_payload(doc, chunks_before=None) rev = DocumentRevision( document_id=doc.id, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_turn_id=turn_id, agent_action_id=action_id, **payload, @@ -624,7 +624,7 @@ async def _snapshot_folder_pre_mkdir( *, folder: Folder, action_id: int | None, - search_space_id: int, + workspace_id: int, turn_id: str | None = None, deferred_dispatches: list[int] | None = None, ) -> int | None: @@ -637,7 +637,7 @@ async def _snapshot_folder_pre_mkdir( async with session.begin_nested(): rev = FolderRevision( folder_id=folder.id, - search_space_id=search_space_id, + workspace_id=workspace_id, name_before=None, parent_id_before=None, position_before=None, @@ -670,7 +670,7 @@ async def _snapshot_folder_pre_mkdir( async def commit_staged_filesystem_state( state: dict[str, Any] | AgentState, *, - search_space_id: int, + workspace_id: int, created_by_id: str | None, filesystem_mode: FilesystemMode = FilesystemMode.CLOUD, thread_id: int | None = None, @@ -814,7 +814,7 @@ async def commit_staged_filesystem_state( continue folder_id = await _ensure_folder_hierarchy( session, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=created_by_id, folder_parts=folder_parts_full, ) @@ -833,7 +833,7 @@ async def commit_staged_filesystem_state( session, folder=folder_row, action_id=action_id, - search_space_id=search_space_id, + workspace_id=workspace_id, turn_id=tcid, deferred_dispatches=deferred_dispatches, ) @@ -851,14 +851,14 @@ async def commit_staged_filesystem_state( res_pre = await session.execute( select(Document).where( Document.id == doc_id_pre, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document_pre = res_pre.scalar_one_or_none() if document_pre is None: document_pre = await virtual_path_to_doc( session, - search_space_id=search_space_id, + workspace_id=workspace_id, virtual_path=source, ) if document_pre is not None: @@ -866,14 +866,14 @@ async def commit_staged_filesystem_state( session, doc=document_pre, action_id=action_id, - search_space_id=search_space_id, + workspace_id=workspace_id, turn_id=tcid, deferred_dispatches=deferred_dispatches, ) applied = await _apply_move( session, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=created_by_id, move=move, doc_id_by_path=doc_id_by_path, @@ -937,7 +937,7 @@ async def commit_staged_filesystem_state( # INSERT (which would hit the path-derived unique hash). existing = await virtual_path_to_doc( session, - search_space_id=search_space_id, + workspace_id=workspace_id, virtual_path=path, ) if existing is not None: @@ -948,7 +948,7 @@ async def commit_staged_filesystem_state( result_doc = await session.execute( select(Document).where( Document.id == doc_id, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) existing_doc = result_doc.scalar_one_or_none() @@ -957,7 +957,7 @@ async def commit_staged_filesystem_state( session, doc=existing_doc, action_id=action_id, - search_space_id=search_space_id, + workspace_id=workspace_id, turn_id=tcid, deferred_dispatches=deferred_dispatches, ) @@ -966,7 +966,7 @@ async def commit_staged_filesystem_state( doc_id=doc_id, content=content, virtual_path=path, - search_space_id=search_space_id, + workspace_id=workspace_id, ) if updated is not None: committed_updates.append( @@ -974,7 +974,7 @@ async def commit_staged_filesystem_state( "id": updated.id, "title": updated.title, "documentType": DocumentType.NOTE.value, - "searchSpaceId": search_space_id, + "workspaceId": workspace_id, "folderId": updated.folder_id, "createdById": str(created_by_id) if created_by_id @@ -991,7 +991,7 @@ async def commit_staged_filesystem_state( placeholder_revision_id = await _snapshot_document_pre_create( session, action_id=action_id, - search_space_id=search_space_id, + workspace_id=workspace_id, turn_id=tcid, deferred_dispatches=deferred_dispatches, ) @@ -1001,7 +1001,7 @@ async def commit_staged_filesystem_state( session, virtual_path=path, content=content, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=created_by_id, ) except ValueError as exc: @@ -1045,7 +1045,7 @@ async def commit_staged_filesystem_state( "id": new_doc.id, "title": new_doc.title, "documentType": DocumentType.NOTE.value, - "searchSpaceId": search_space_id, + "workspaceId": workspace_id, "folderId": new_doc.folder_id, "createdById": str(created_by_id) if created_by_id @@ -1069,14 +1069,14 @@ async def commit_staged_filesystem_state( result = await session.execute( select(Document).where( Document.id == doc_id_for_delete, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) document_to_delete = result.scalar_one_or_none() if document_to_delete is None: document_to_delete = await virtual_path_to_doc( session, - search_space_id=search_space_id, + workspace_id=workspace_id, virtual_path=final, ) if document_to_delete is None: @@ -1100,7 +1100,7 @@ async def commit_staged_filesystem_state( ) rev = DocumentRevision( document_id=doc_pk, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_turn_id=tcid, agent_action_id=action_id, **payload, @@ -1130,7 +1130,7 @@ async def commit_staged_filesystem_state( "id": doc_pk, "title": doc_title, "documentType": DocumentType.NOTE.value, - "searchSpaceId": search_space_id, + "workspaceId": workspace_id, "folderId": doc_folder_id, "createdById": str(created_by_id) if created_by_id else None, "virtualPath": final, @@ -1151,7 +1151,7 @@ async def commit_staged_filesystem_state( continue folder_id = await _resolve_folder_id( session, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_parts=folder_parts, ) if folder_id is None: @@ -1163,7 +1163,7 @@ async def commit_staged_filesystem_state( docs_in_folder = await session.execute( select(Document.id) .where(Document.folder_id == folder_id) - .where(Document.search_space_id == search_space_id) + .where(Document.workspace_id == workspace_id) .limit(1) ) if docs_in_folder.scalar_one_or_none() is not None: @@ -1175,7 +1175,7 @@ async def commit_staged_filesystem_state( child_folders = await session.execute( select(Folder.id) .where(Folder.parent_id == folder_id) - .where(Folder.search_space_id == search_space_id) + .where(Folder.workspace_id == workspace_id) .limit(1) ) if child_folders.scalar_one_or_none() is not None: @@ -1203,7 +1203,7 @@ async def commit_staged_filesystem_state( if snapshot_enabled and action_id is not None: rev = FolderRevision( folder_id=folder_pk, - search_space_id=search_space_id, + workspace_id=workspace_id, name_before=folder_name, parent_id_before=folder_parent_id, position_before=folder_position, @@ -1232,7 +1232,7 @@ async def commit_staged_filesystem_state( { "id": folder_pk, "name": folder_name, - "searchSpaceId": search_space_id, + "workspaceId": workspace_id, "parentId": folder_parent_id, "virtualPath": final, } @@ -1242,7 +1242,7 @@ async def commit_staged_filesystem_state( await session.commit() except Exception: # pragma: no cover - rollback safety net logger.exception( - "kb_persistence: commit failed (search_space=%s)", search_space_id + "kb_persistence: commit failed (workspace=%s)", workspace_id ) # Outer commit raised: everything above rolled back, so drop the # deferred dispatches. @@ -1402,9 +1402,9 @@ async def commit_staged_filesystem_state( _ = turn_id_for_revision # diagnostic-only; silence unused lint logger.info( - "kb_persistence: commit (search_space=%s) creates=%d updates=%d " + "kb_persistence: commit (workspace=%s) creates=%d updates=%d " "moves=%d staged_dirs=%d deletes=%d folder_deletes=%d discarded=%d", - search_space_id, + workspace_id, len(committed_creates), len(committed_updates), len(applied_moves), @@ -1430,12 +1430,12 @@ class KnowledgeBasePersistenceMiddleware(AgentMiddleware): # type: ignore[type- def __init__( self, *, - search_space_id: int, + workspace_id: int, created_by_id: str | None, filesystem_mode: FilesystemMode, thread_id: int | None = None, ) -> None: - self.search_space_id = search_space_id + self.workspace_id = workspace_id self.created_by_id = created_by_id self.filesystem_mode = filesystem_mode self.thread_id = thread_id @@ -1450,7 +1450,7 @@ class KnowledgeBasePersistenceMiddleware(AgentMiddleware): # type: ignore[type- return None return await commit_staged_filesystem_state( state, - search_space_id=self.search_space_id, + workspace_id=self.workspace_id, created_by_id=self.created_by_id, filesystem_mode=self.filesystem_mode, thread_id=self._resolve_thread_id(), diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/builder.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/builder.py index 644d1e55a..af5188237 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/builder.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/builder.py @@ -12,13 +12,13 @@ from .middleware import KnowledgeTreeMiddleware def build_knowledge_tree_mw( *, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, llm: BaseChatModel, ) -> KnowledgeTreeMiddleware | None: if filesystem_mode != FilesystemMode.CLOUD: return None return KnowledgeTreeMiddleware( - search_space_id=search_space_id, + workspace_id=workspace_id, filesystem_mode=filesystem_mode, llm=llm, inject_system_message=False, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/middleware.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/middleware.py index a0c62834a..27c271c15 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/middleware.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/middleware.py @@ -1,7 +1,7 @@ """Workspace-tree middleware for the SurfSense agent. Renders the full ``Folder``+``Document`` tree under ``/documents/`` once per -turn (cloud only), caches it by ``(search_space_id, tree_version)``, and +turn (cloud only), caches it by ``(workspace_id, tree_version)``, and injects the result as a ```` system message immediately before the latest human turn. @@ -106,14 +106,14 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg] def __init__( self, *, - search_space_id: int, + workspace_id: int, filesystem_mode: FilesystemMode, llm: BaseChatModel | None = None, max_entries: int = MAX_TREE_ENTRIES, max_tokens: int = MAX_TREE_TOKENS, inject_system_message: bool = True, # For backwards compatibility ) -> None: - self.search_space_id = search_space_id + self.workspace_id = workspace_id self.filesystem_mode = filesystem_mode self.llm = llm self.max_entries = max_entries @@ -141,7 +141,7 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg] cache_outcome = "anon" else: version = int(state.get("tree_version") or 0) - cache_key = (self.search_space_id, version, False) + cache_key = (self.workspace_id, version, False) cache_outcome = "hit" if cache_key in self._cache else "miss" tree_msg = await self._render_kb_tree(state) @@ -158,7 +158,7 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg] cache_outcome, len(tree_msg), time.perf_counter() - start, - self.search_space_id, + self.workspace_id, ) return update @@ -190,17 +190,17 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg] async def _render_kb_tree(self, state: AgentState) -> str: version = int(state.get("tree_version") or 0) - cache_key = (self.search_space_id, version, False) + cache_key = (self.workspace_id, version, False) cached = self._cache.get(cache_key) if cached is not None: return cached try: async with shielded_async_session() as session: - index = await build_path_index(session, self.search_space_id) + index = await build_path_index(session, self.workspace_id) doc_rows = await session.execute( select(Document.id, Document.title, Document.folder_id).where( - Document.search_space_id == self.search_space_id + Document.workspace_id == self.workspace_id ) ) docs = list(doc_rows.all()) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/builder.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/builder.py index 4ea171e13..1b0535b85 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/builder.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/builder.py @@ -10,11 +10,11 @@ from .middleware import MemoryInjectionMiddleware def build_memory_mw( *, user_id: str | None, - search_space_id: int, + workspace_id: int, visibility: ChatVisibility, ) -> MemoryInjectionMiddleware: return MemoryInjectionMiddleware( user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, thread_visibility=visibility, ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/middleware.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/middleware.py index 9d0fd26a1..98ead2dd7 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/middleware.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/memory/middleware.py @@ -18,7 +18,7 @@ from langgraph.runtime import Runtime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.db import ChatVisibility, SearchSpace, User, shielded_async_session +from app.db import ChatVisibility, Workspace, User, shielded_async_session from app.services.memory import MEMORY_HARD_LIMIT, MEMORY_SOFT_LIMIT from app.utils.perf import get_perf_logger @@ -35,11 +35,11 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg] self, *, user_id: str | UUID | None, - search_space_id: int, + workspace_id: int, thread_visibility: ChatVisibility | None = None, ) -> None: self.user_id = UUID(user_id) if isinstance(user_id, str) else user_id - self.search_space_id = search_space_id + self.workspace_id = workspace_id self.visibility = thread_visibility or ChatVisibility.PRIVATE async def abefore_agent( # type: ignore[override] @@ -149,8 +149,8 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg] async def _load_team_memory(self, session: AsyncSession) -> str | None: try: result = await session.execute( - select(SearchSpace.shared_memory_md).where( - SearchSpace.id == self.search_space_id + select(Workspace.shared_memory_md).where( + Workspace.id == self.workspace_id ) ) row = result.scalar_one_or_none() diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/plugins.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/plugins.py index 43f4136ec..da8f92a53 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/plugins.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/plugins.py @@ -21,7 +21,7 @@ from ..plugins.loader import ( def build_plugin_middlewares( *, flags: AgentFeatureFlags, - search_space_id: int, + workspace_id: int, user_id: str | None, visibility: ChatVisibility, llm: BaseChatModel, @@ -34,7 +34,7 @@ def build_plugin_middlewares( return [] return load_plugin_middlewares( PluginContext.build( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, thread_visibility=visibility, llm=llm, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/skills.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/skills.py index 13c62e817..38fb50a24 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/skills.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/skills.py @@ -17,13 +17,13 @@ def build_skills_mw( *, flags: AgentFeatureFlags, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, ) -> SkillsMiddleware | None: if not enabled(flags, "enable_skills"): return None try: skills_factory = build_skills_backend_factory( - search_space_id=search_space_id + workspace_id=workspace_id if filesystem_mode == FilesystemMode.CLOUD else None, ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/stack.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/stack.py index 83053954b..143cceaef 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/stack.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/stack.py @@ -99,7 +99,7 @@ def build_main_agent_deepagent_middleware( tools: Sequence[BaseTool], backend_resolver: Any, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, user_id: str | None, thread_id: int | None, visibility: ChatVisibility, @@ -120,7 +120,7 @@ def build_main_agent_deepagent_middleware( memory_mw = build_memory_mw( user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, visibility=visibility, ) @@ -232,19 +232,19 @@ def build_main_agent_deepagent_middleware( ), build_knowledge_tree_mw( filesystem_mode=filesystem_mode, - search_space_id=search_space_id, + workspace_id=workspace_id, llm=llm, ), build_kb_persistence_mw( filesystem_mode=filesystem_mode, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, thread_id=thread_id, ), build_skills_mw( flags=flags, filesystem_mode=filesystem_mode, - search_space_id=search_space_id, + workspace_id=workspace_id, ), SurfSenseCheckpointedSubAgentMiddleware( checkpointer=checkpointer, @@ -252,7 +252,7 @@ def build_main_agent_deepagent_middleware( subagents=subagents, system_prompt=None, task_description=TASK_TOOL_DESCRIPTION, - search_space_id=search_space_id, + workspace_id=workspace_id, ), resilience.model_call_limit, resilience.tool_call_limit, @@ -272,14 +272,14 @@ def build_main_agent_deepagent_middleware( build_action_log_mw( flags=flags, thread_id=thread_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ), build_patch_tool_calls_mw(), build_dedup_hitl_mw(tools), *build_plugin_middlewares( flags=flags, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, visibility=visibility, llm=llm, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/loader.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/loader.py index c52620d40..ee405fb79 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/loader.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/loader.py @@ -57,7 +57,7 @@ class PluginContext(dict): Backed by ``dict`` so plugins can inspect the keys they care about without coupling to a concrete dataclass shape. Required keys: - * ``search_space_id`` (int) + * ``workspace_id`` (int) * ``user_id`` (str | None) * ``thread_visibility`` (:class:`app.db.ChatVisibility`) * ``llm`` (:class:`langchain_core.language_models.BaseChatModel`) @@ -72,13 +72,13 @@ class PluginContext(dict): def build( cls, *, - search_space_id: int, + workspace_id: int, user_id: str | None, thread_visibility: ChatVisibility, llm: BaseChatModel, ) -> PluginContext: return cls( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, thread_visibility=thread_visibility, llm=llm, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/year_substituter.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/year_substituter.py index f6564fe6e..58a59e5ca 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/year_substituter.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/plugins/year_substituter.py @@ -7,7 +7,7 @@ result. This particular plugin is read-only and only transforms the mutation). The plugin is built as a factory function so the entry-point loader can -inject :class:`PluginContext` (containing the agent's LLM, search-space +inject :class:`PluginContext` (containing the agent's LLM, workspace ID, etc.). The factory signature ``Callable[[PluginContext], AgentMiddleware]`` is the only contract -- SurfSense doesn't define a custom plugin protocol on top of LangChain's diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache.py index 2d3599de0..7c8d9d3d2 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache.py @@ -42,7 +42,7 @@ async def build_agent_with_cache( final_system_prompt: str, backend_resolver: Any, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, user_id: str | None, thread_id: int | None, visibility: ChatVisibility, @@ -69,7 +69,7 @@ async def build_agent_with_cache( final_system_prompt=final_system_prompt, backend_resolver=backend_resolver, filesystem_mode=filesystem_mode, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, thread_id=thread_id, visibility=visibility, @@ -104,7 +104,7 @@ async def build_agent_with_cache( config_id, None if cross_thread else thread_id, user_id, - search_space_id, + workspace_id, visibility, filesystem_mode, anon_session_id, @@ -120,7 +120,7 @@ async def build_agent_with_cache( sorted(disabled_tools) if disabled_tools else None, # Bound into the generate_image subagent tool at construction time, so it # must key the compiled-agent cache to avoid leaking one automation's - # image model into another with the same config_id/search_space. + # image model into another with the same config_id/workspace. image_gen_model_id_override, ) return await get_cache().get_or_build(cache_key, builder=_build) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache_store.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache_store.py index 00866adf9..e53483566 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache_store.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/agent_cache_store.py @@ -26,7 +26,7 @@ Why a per-thread key (not a global pool) ---------------------------------------- Most middleware in the SurfSense stack captures per-thread state in -``__init__`` closures (``thread_id``, ``user_id``, ``search_space_id``, +``__init__`` closures (``thread_id``, ``user_id``, ``workspace_id``, ``filesystem_mode``, ``mentioned_document_ids``). Cross-thread reuse would silently leak state across users and threads. Keying the cache on ``(llm_config_id, thread_id, ...)`` gives us safe reuse for repeated @@ -34,7 +34,7 @@ turns on the same thread without changing any middleware's behavior. Phase 2 will move those captured fields onto :class:`SurfSenseContextSchema` (read via ``runtime.context``) so the cache can collapse to a single -``(llm_config_id, search_space_id, ...)`` key shared across threads. Until +``(llm_config_id, workspace_id, ...)`` key shared across threads. Until then, per-thread keying is the only safe option. Cache shape @@ -111,7 +111,7 @@ def tools_signature( * A tool is added or removed from the bound list (built-in toggles, MCP tools loaded for the user changes, gating rules flip, etc.). - * The available connectors / document types for the search space + * The available connectors / document types for the workspace change (new connector added, last connector removed, new document type indexed). Connector gating derives disabled tools from ``available_connectors``, so the tool surface is technically already diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/connector_searchable_types.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/connector_searchable_types.py index be193be04..09fa0756a 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/connector_searchable_types.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/connector_searchable_types.py @@ -1,7 +1,7 @@ """Map configured connectors to the searchable document/connector types. This is agent-agnostic infrastructure shared by every agent factory (single- -and multi-agent). It translates the connectors a search space has enabled into +and multi-agent). It translates the connectors a workspace has enabled into the set of searchable type strings that pre-search middleware and ``web_search`` understand, and always layers in the document types that exist independently of any connector (uploads, notes, extension captures, YouTube). diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/factory.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/factory.py index d823a5a06..5cc27b74b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/factory.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/runtime/factory.py @@ -58,7 +58,7 @@ _perf_log = get_perf_logger() async def create_multi_agent_chat_deep_agent( llm: BaseChatModel, - search_space_id: int, + workspace_id: int, db_session: AsyncSession, connector_service: ConnectorService, checkpointer: Checkpointer, @@ -78,9 +78,9 @@ async def create_multi_agent_chat_deep_agent( ): """Deep agent with SurfSense tools/middleware; registry route subagents behind ``task`` when enabled. - ``image_gen_model_id`` overrides the search space's image model for + ``image_gen_model_id`` overrides the workspace's image model for this invocation (used by automations to run on their captured model). When - ``None``, the ``generate_image`` tool resolves the live search-space pref. + ``None``, the ``generate_image`` tool resolves the live workspace pref. """ _t_agent_total = time.perf_counter() @@ -89,7 +89,7 @@ async def create_multi_agent_chat_deep_agent( filesystem_selection = filesystem_selection or FilesystemSelection() backend_resolver = build_backend_resolver( filesystem_selection, - search_space_id=search_space_id + workspace_id=workspace_id if filesystem_selection.mode == FilesystemMode.CLOUD else None, ) @@ -100,12 +100,12 @@ async def create_multi_agent_chat_deep_agent( _t0 = time.perf_counter() try: connector_types = await connector_service.get_available_connectors( - search_space_id + workspace_id ) available_connectors = map_connectors_to_searchable_types(connector_types) available_document_types = await connector_service.get_available_document_types( - search_space_id + workspace_id ) except Exception as e: @@ -136,7 +136,7 @@ async def create_multi_agent_chat_deep_agent( ) dependencies: dict[str, Any] = { - "search_space_id": search_space_id, + "workspace_id": workspace_id, "db_session": db_session, "connector_service": connector_service, "firecrawl_api_key": firecrawl_api_key, @@ -156,7 +156,7 @@ async def create_multi_agent_chat_deep_agent( _t0 = time.perf_counter() try: mcp_tools_by_agent = await load_mcp_tools_by_connector( - db_session, search_space_id + db_session, workspace_id ) except Exception as e: # Degrade to builtins-only rather than aborting the turn: a transient @@ -193,7 +193,7 @@ async def create_multi_agent_chat_deep_agent( user_allowlist_by_subagent = await fetch_user_allowlist_rulesets( db_session, user_id=user_uuid, - search_space_id=search_space_id, + workspace_id=workspace_id, ) except Exception as e: logging.warning( @@ -291,7 +291,7 @@ async def create_multi_agent_chat_deep_agent( final_system_prompt=final_system_prompt, backend_resolver=backend_resolver, filesystem_mode=filesystem_selection.mode, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, thread_id=thread_id, visibility=visibility, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/skills/backends.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/skills/backends.py index 31620fe9b..ad541563a 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/skills/backends.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/skills/backends.py @@ -17,12 +17,12 @@ Two backends are provided: * :class:`BuiltinSkillsBackend` — disk-backed read of bundled skills from ``app/agents/shared/skills/builtin/``. -* :class:`SearchSpaceSkillsBackend` — a thin read-only wrapper over +* :class:`WorkspaceSkillsBackend` — a thin read-only wrapper over :class:`KBPostgresBackend` that filters notes under the privileged folder ``/documents/_skills/``. Both backends are intentionally read-only: skill authoring happens out of band -(via filesystem or a search-space-admin route), so we never expose +(via filesystem or a workspace-admin route), so we never expose ``write`` / ``edit`` / ``upload_files``. The base class' ``NotImplementedError`` gives a clean failure mode if anything tries. """ @@ -181,12 +181,12 @@ class BuiltinSkillsBackend(BackendProtocol): return responses -class SearchSpaceSkillsBackend(BackendProtocol): - """Read-only view of search-space-authored skills. +class WorkspaceSkillsBackend(BackendProtocol): + """Read-only view of workspace-authored skills. Wraps a :class:`KBPostgresBackend` and only ever reads under the privileged folder ``/documents/_skills/`` (configurable). The folder is intended to be - writable only by search-space admins; this backend never writes. + writable only by workspace admins; this backend never writes. The skills middleware expects a layout like:: @@ -236,14 +236,14 @@ class SearchSpaceSkillsBackend(BackendProtocol): # path falls back to ``asyncio.to_thread(...)`` in the base class. We # keep this stub to satisfy abstract resolution; the middleware calls # ``als_info``. - raise NotImplementedError("SearchSpaceSkillsBackend is async-only") + raise NotImplementedError("WorkspaceSkillsBackend is async-only") async def als_info(self, path: str) -> list[FileInfo]: kb_path = self._to_kb(path) try: infos = await self._kb.als_info(kb_path) except Exception as exc: # pragma: no cover - defensive - logger.warning("SearchSpaceSkillsBackend.als_info failed: %s", exc) + logger.warning("WorkspaceSkillsBackend.als_info failed: %s", exc) return [] remapped: list[FileInfo] = [] for info in infos: @@ -254,7 +254,7 @@ class SearchSpaceSkillsBackend(BackendProtocol): return remapped def download_files(self, paths: list[str]) -> list[FileDownloadResponse]: - raise NotImplementedError("SearchSpaceSkillsBackend is async-only") + raise NotImplementedError("WorkspaceSkillsBackend is async-only") async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]: kb_paths = [self._to_kb(p) for p in paths] @@ -274,16 +274,16 @@ SKILLS_SPACE_PREFIX = "/skills/space/" def build_skills_backend_factory( *, builtin_root: Path | str | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, ) -> Callable[[ToolRuntime], BackendProtocol]: """Return a runtime-aware factory for the skills :class:`CompositeBackend`. - When ``search_space_id`` is provided the composite includes a - :class:`SearchSpaceSkillsBackend` route at ``/skills/space/`` over a fresh + When ``workspace_id`` is provided the composite includes a + :class:`WorkspaceSkillsBackend` route at ``/skills/space/`` over a fresh per-runtime :class:`KBPostgresBackend`, mirroring how :func:`build_backend_resolver` constructs the main filesystem backend. - When ``search_space_id`` is ``None`` (e.g., desktop-local mode or unit + When ``workspace_id`` is ``None`` (e.g., desktop-local mode or unit tests) only the bundled :class:`BuiltinSkillsBackend` is exposed. Returning a factory rather than a fixed instance is intentional: the @@ -293,7 +293,7 @@ def build_skills_backend_factory( """ builtin = BuiltinSkillsBackend(builtin_root) - if search_space_id is None: + if workspace_id is None: def _factory_builtin_only(runtime: ToolRuntime) -> BackendProtocol: # Default StateBackend is intentionally inert: any path outside the @@ -314,8 +314,8 @@ def build_skills_backend_factory( KBPostgresBackend, ) - kb = KBPostgresBackend(search_space_id, runtime) - space = SearchSpaceSkillsBackend(kb) + kb = KBPostgresBackend(workspace_id, runtime) + space = WorkspaceSkillsBackend(kb) return CompositeBackend( default=StateBackend(runtime), routes={ @@ -336,7 +336,7 @@ __all__ = [ "SKILLS_BUILTIN_PREFIX", "SKILLS_SPACE_PREFIX", "BuiltinSkillsBackend", - "SearchSpaceSkillsBackend", + "WorkspaceSkillsBackend", "build_skills_backend_factory", "default_skills_sources", ] diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/system_prompt/prompts/tools/update_memory/team/description.md b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/system_prompt/prompts/tools/update_memory/team/description.md index 8459f9e7a..2d1c5af9b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/system_prompt/prompts/tools/update_memory/team/description.md +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/system_prompt/prompts/tools/update_memory/team/description.md @@ -1,5 +1,5 @@ - `update_memory` — Curate the team's **shared** long-term memory document - for this search space. + for this workspace. - The current memory (if any) appears in `` with usage vs limit. - Call when a team member asks to remember or forget something, or when the conversation surfaces durable team decisions, conventions, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/create.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/create.py index c1122b681..c00692185 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/create.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/create.py @@ -45,14 +45,14 @@ _JSON_FENCE = re.compile(r"```(?:json)?\s*(.*?)\s*```", re.DOTALL) def create_create_automation_tool( *, - search_space_id: int, + workspace_id: int, user_id: str | UUID, llm: Any, auth_context: AuthContext | None = None, ): """Factory for the ``create_automation`` tool. - ``search_space_id`` is injected from the chat session (the model never + ``workspace_id`` is injected from the chat session (the model never has to guess it). ``llm`` is the drafting sub-model — we reuse the main agent's LLM and tag the call so it's identifiable in traces. A fresh ``AsyncSession`` is opened per call to avoid stale sessions on @@ -101,12 +101,12 @@ def create_create_automation_tool( """ # Models are chosen per-automation on the approval card (premium/BYOK # selectors) and validated when persisted by ``AutomationService.create`` - # — so there's no fail-fast search-space eligibility gate here. The - # search space's current chat/role model selection no longer constrains + # — so there's no fail-fast workspace eligibility gate here. The + # workspace's current chat/role model selection no longer constrains # whether an automation can be drafted or saved. # --- 1. Draft via sub-LLM --- - prompt = build_draft_prompt(search_space_id=search_space_id, intent=intent) + prompt = build_draft_prompt(workspace_id=workspace_id, intent=intent) try: response = await llm.ainvoke( [HumanMessage(content=prompt)], @@ -125,8 +125,8 @@ def create_create_automation_tool( "raw": raw_text, } - # search_space_id is injected here so the sub-LLM never has to guess. - draft["search_space_id"] = search_space_id + # workspace_id is injected here so the sub-LLM never has to guess. + draft["workspace_id"] = workspace_id try: validated_draft = AutomationCreate.model_validate(draft) except ValidationError as exc: @@ -139,14 +139,14 @@ def create_create_automation_tool( # --- 2. HITL approval card --- try: card_params = validated_draft.model_dump(mode="json", by_alias=True) - # search_space_id is session-scoped, not user-editable. - card_params.pop("search_space_id", None) + # workspace_id is session-scoped, not user-editable. + card_params.pop("workspace_id", None) result = request_approval( action_type="automation_create", tool_name="create_automation", params=card_params, - context={"search_space_id": search_space_id}, + context={"workspace_id": workspace_id}, tool_call_id=runtime.tool_call_id, ) @@ -157,7 +157,7 @@ def create_create_automation_tool( } # --- 3. Persist (re-validate in case the user edited) --- - final_payload = {**result.params, "search_space_id": search_space_id} + final_payload = {**result.params, "workspace_id": workspace_id} try: final_validated = AutomationCreate.model_validate(final_payload) except ValidationError as exc: diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/prompt.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/prompt.py index 09854aa2e..66ba0183c 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/prompt.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/automation/prompt.py @@ -34,7 +34,7 @@ into a SINGLE JSON object matching the AutomationCreate schema. Output ONLY that JSON object — no prose, no markdown fence, no commentary. Current UTC time (for cron context): {now} -Target search_space_id: {search_space_id} +Target workspace_id: {workspace_id} """ @@ -165,12 +165,12 @@ User intent: """ -def build_draft_prompt(*, search_space_id: int, intent: str) -> str: +def build_draft_prompt(*, workspace_id: int, intent: str) -> str: """Render the drafting sub-LLM system prompt for the given intent.""" return ( _HEADER.format( now=datetime.now(UTC).isoformat(timespec="seconds"), - search_space_id=search_space_id, + workspace_id=workspace_id, ) + _SCHEMA + _FEW_SHOTS diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/registry.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/registry.py index bdfa67c79..23ea8b8d6 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/registry.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/registry.py @@ -37,7 +37,7 @@ def _build_scrape_webpage_tool(deps: dict[str, Any]) -> BaseTool: def _build_web_search_tool(deps: dict[str, Any]) -> BaseTool: return create_web_search_tool( - search_space_id=deps.get("search_space_id"), + workspace_id=deps.get("workspace_id"), available_connectors=deps.get("available_connectors"), ) @@ -49,7 +49,7 @@ def _build_create_automation_tool(deps: dict[str, Any]) -> BaseTool: from .automation import create_create_automation_tool return create_create_automation_tool( - search_space_id=deps["search_space_id"], + workspace_id=deps["workspace_id"], user_id=deps["user_id"], auth_context=deps.get("auth_context"), llm=deps["llm"], @@ -59,7 +59,7 @@ def _build_create_automation_tool(deps: dict[str, Any]) -> BaseTool: def _build_update_memory_tool(deps: dict[str, Any]) -> BaseTool: if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE: return create_update_team_memory_tool( - search_space_id=deps["search_space_id"], + workspace_id=deps["workspace_id"], db_session=deps["db_session"], llm=deps.get("llm"), ) @@ -80,11 +80,11 @@ _MAIN_AGENT_TOOL_FACTORIES: dict[ "web_search": (_build_web_search_tool, ()), "create_automation": ( _build_create_automation_tool, - ("search_space_id", "user_id", "llm"), + ("workspace_id", "user_id", "llm"), ), "update_memory": ( _build_update_memory_tool, - ("user_id", "search_space_id", "db_session", "thread_visibility", "llm"), + ("user_id", "workspace_id", "db_session", "thread_visibility", "llm"), ), } diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/update_memory.py b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/update_memory.py index 78a65201b..333d10e4b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/update_memory.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/tools/update_memory.py @@ -53,7 +53,7 @@ def create_update_memory_tool( def create_update_team_memory_tool( - search_space_id: int, + workspace_id: int, db_session: AsyncSession, llm: Any | None = None, ): @@ -62,7 +62,7 @@ def create_update_team_memory_tool( @tool async def update_memory(updated_memory: str) -> dict[str, Any]: - """Update the team's shared memory document for this search space. + """Update the team's shared memory document for this workspace. The current team memory is shown in . Pass the FULL updated markdown document, not a diff. @@ -71,7 +71,7 @@ def create_update_team_memory_tool( async with async_session_maker() as db_session: result = await save_memory( scope=MemoryScope.TEAM, - target_id=search_space_id, + target_id=workspace_id, content=updated_memory, session=db_session, llm=llm, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/kb_postgres.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/kb_postgres.py index cb0f4cc69..d21ce03c6 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/kb_postgres.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/kb_postgres.py @@ -120,8 +120,8 @@ class KBPostgresBackend(BackendProtocol): _IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp"}) - def __init__(self, search_space_id: int, runtime: ToolRuntime) -> None: - self.search_space_id = search_space_id + def __init__(self, workspace_id: int, runtime: ToolRuntime) -> None: + self.workspace_id = workspace_id self.runtime = runtime @property @@ -405,7 +405,7 @@ class KBPostgresBackend(BackendProtocol): if not normalized_path.startswith(DOCUMENTS_ROOT): return [], set() - index = await build_path_index(session, self.search_space_id) + index = await build_path_index(session, self.workspace_id) target_folder_id: int | None = None if normalized_path != DOCUMENTS_ROOT: target_path = normalized_path @@ -418,7 +418,7 @@ class KBPostgresBackend(BackendProtocol): result = await session.execute( select(Document.id, Document.title, Document.folder_id, Document.updated_at) - .where(Document.search_space_id == self.search_space_id) + .where(Document.workspace_id == self.workspace_id) .where( Document.folder_id == target_folder_id if target_folder_id is not None @@ -519,7 +519,7 @@ class KBPostgresBackend(BackendProtocol): async with shielded_async_session() as session: document_row = await virtual_path_to_doc( session, - search_space_id=self.search_space_id, + workspace_id=self.workspace_id, virtual_path=path, ) if document_row is None: @@ -667,10 +667,10 @@ class KBPostgresBackend(BackendProtocol): if normalized.startswith(DOCUMENTS_ROOT) or normalized == "/": try: async with shielded_async_session() as session: - index = await build_path_index(session, self.search_space_id) + index = await build_path_index(session, self.workspace_id) rows = await session.execute( select(Document.id, Document.title, Document.folder_id).where( - Document.search_space_id == self.search_space_id + Document.workspace_id == self.workspace_id ) ) for row in rows.all(): @@ -747,11 +747,11 @@ class KBPostgresBackend(BackendProtocol): if normalized.startswith(DOCUMENTS_ROOT) or normalized == "/": try: async with shielded_async_session() as session: - index = await build_path_index(session, self.search_space_id) + index = await build_path_index(session, self.workspace_id) sub = ( select(Chunk.document_id, Chunk.id, Chunk.content) .join(Document, Document.id == Chunk.document_id) - .where(Document.search_space_id == self.search_space_id) + .where(Document.workspace_id == self.workspace_id) .where(Chunk.content.ilike(f"%{pattern}%")) .order_by(Chunk.document_id, Chunk.position, Chunk.id) ) @@ -849,14 +849,14 @@ class KBPostgresBackend(BackendProtocol): try: async with shielded_async_session() as session: - index = await build_path_index(session, self.search_space_id) + index = await build_path_index(session, self.workspace_id) doc_rows_raw = await session.execute( select( Document.id, Document.title, Document.folder_id, Document.updated_at, - ).where(Document.search_space_id == self.search_space_id) + ).where(Document.workspace_id == self.workspace_id) ) doc_rows = list(doc_rows_raw.all()) except Exception as exc: # pragma: no cover diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/resolver.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/resolver.py index 4553df7ff..4b8c3dc6f 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/resolver.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/backends/resolver.py @@ -31,14 +31,14 @@ def _cached_multi_root_backend( def build_backend_resolver( selection: FilesystemSelection, *, - search_space_id: int | None = None, + workspace_id: int | None = None, ) -> Callable[[ToolRuntime], BackendProtocol]: """Create deepagents backend resolver for the selected filesystem mode. In cloud mode the resolver returns a fresh :class:`KBPostgresBackend` bound to the current ``runtime`` so the backend can read staging state (``staged_dirs``, ``pending_moves``, ``files`` cache, ``kb_anon_doc``) - for each tool call. When no ``search_space_id`` + for each tool call. When no ``workspace_id`` is provided, the resolver falls back to :class:`StateBackend` (used by sub-agents and tests that don't need DB-backed reads). @@ -55,10 +55,10 @@ def build_backend_resolver( return _resolve_local - if search_space_id is not None: + if workspace_id is not None: def _resolve_kb(runtime: ToolRuntime) -> BackendProtocol: - return KBPostgresBackend(search_space_id, runtime) + return KBPostgresBackend(workspace_id, runtime) return _resolve_kb diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/index.py index 91bc4db7c..e4d2fdf78 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/index.py @@ -13,7 +13,7 @@ def build_filesystem_mw( *, backend_resolver: Any, filesystem_mode: FilesystemMode, - search_space_id: int, + workspace_id: int, user_id: str | None, thread_id: int | None, read_only: bool = False, @@ -21,7 +21,7 @@ def build_filesystem_mw( return SurfSenseFilesystemMiddleware( backend=backend_resolver, filesystem_mode=filesystem_mode, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, thread_id=thread_id, read_only=read_only, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/middleware/middleware.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/middleware/middleware.py index c3b06ff12..95f217df6 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/middleware/middleware.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/middleware/filesystem/middleware/middleware.py @@ -49,14 +49,14 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware): *, backend: Any = None, filesystem_mode: FilesystemMode = FilesystemMode.CLOUD, - search_space_id: int | None = None, + workspace_id: int | None = None, created_by_id: str | None = None, thread_id: int | str | None = None, tool_token_limit_before_evict: int | None = 20000, read_only: bool = False, ) -> None: self._filesystem_mode = filesystem_mode - self._search_space_id = search_space_id + self._workspace_id = workspace_id self._created_by_id = created_by_id self._thread_id = thread_id self._read_only = read_only diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/hybrid_search.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/hybrid_search.py index cc200b3a6..70fe5d47f 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/hybrid_search.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/hybrid_search.py @@ -31,7 +31,7 @@ _SURFACE = "chunks" async def search_chunks( db_session: AsyncSession, *, - search_space_id: int, + workspace_id: int, query: str, scope: SearchScope, top_k: int, @@ -44,14 +44,14 @@ async def search_chunks( """ started = time.perf_counter() with otel.kb_search_span( - search_space_id=search_space_id, + workspace_id=workspace_id, query_chars=len(query), extra={"search.surface": _SURFACE, "search.mode": "hybrid"}, ) as span: try: documents = await _search( db_session, - search_space_id=search_space_id, + workspace_id=workspace_id, query=query, scope=scope, top_k=top_k, @@ -60,14 +60,14 @@ async def search_chunks( finally: elapsed_ms = (time.perf_counter() - started) * 1000 metrics.record_kb_search_duration( - elapsed_ms, search_space_id=search_space_id, surface=_SURFACE + elapsed_ms, workspace_id=workspace_id, surface=_SURFACE ) span.set_attribute("result.count", len(documents)) get_perf_logger().info( "[chunk_search] hybrid in %.3fs docs=%d space=%d", elapsed_ms / 1000, len(documents), - search_space_id, + workspace_id, ) return documents @@ -75,7 +75,7 @@ async def search_chunks( async def _search( db_session: AsyncSession, *, - search_space_id: int, + workspace_id: int, query: str, scope: SearchScope, top_k: int, @@ -91,7 +91,7 @@ async def _search( config.embedding_model_instance.embed, query ) - conditions = _base_conditions(search_space_id, scope, document_types) + conditions = _base_conditions(workspace_id, scope, document_types) rows = await _fused_chunks( db_session, query=query, @@ -116,13 +116,13 @@ def _resolve_document_types( def _base_conditions( - search_space_id: int, + workspace_id: int, scope: SearchScope, document_types: list[DocumentType] | None, ) -> list: """Filters shared by both search legs.""" conditions = [ - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, func.coalesce(Document.status["state"].astext, "ready") != "deleting", ] if document_types: diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/service.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/service.py index e9cfa18dd..e8e39a918 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/service.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/retrieval/service.py @@ -29,7 +29,7 @@ _DEFAULT_TOP_K = 10 async def search_knowledge_base_context( db_session: AsyncSession, *, - search_space_id: int, + workspace_id: int, query: str, registry: CitationRegistry, scope: SearchScope | None = None, @@ -42,7 +42,7 @@ async def search_knowledge_base_context( """ hits = await search_chunks( db_session, - search_space_id=search_space_id, + workspace_id=workspace_id, query=query, scope=scope or SearchScope(), top_k=top_k, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/hitl.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/hitl.py index 9b16e1a4c..16c934187 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/hitl.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/hitl.py @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) # artifact in the user's own workspace with no external visibility (drafts # aren't sent; new files aren't shared). They still call ``request_approval``, # which returns ``decision_type="auto_approved"`` without firing an interrupt. -# Per-search-space ``agent_permission_rules`` can re-enable prompting. +# Per-workspace ``agent_permission_rules`` can re-enable prompting. DEFAULT_AUTO_APPROVED_TOOLS: frozenset[str] = frozenset( { "create_gmail_draft", diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/cache.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/cache.py index d088fac0b..83fdc1fa2 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/cache.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/cache.py @@ -99,11 +99,11 @@ async def write_cached_tools( def refresh_mcp_tools_cache_for_connector( connector_id: int, - search_space_id: int, + workspace_id: int, ) -> None: """Maintain the MCP tool cache after a single-connector lifecycle event. - Synchronously evicts the in-process LRU for the connector's search space + Synchronously evicts the in-process LRU for the connector's workspace (LRU keys are per-space, so eviction cannot be scoped finer), then schedules a background live discovery for this connector alone so its persisted ``cached_tools`` row is refreshed before the next user query. @@ -116,11 +116,11 @@ def refresh_mcp_tools_cache_for_connector( invalidate_mcp_tools_cache, ) - invalidate_mcp_tools_cache(search_space_id) + invalidate_mcp_tools_cache(workspace_id) except Exception: logger.debug( "MCP in-process cache eviction skipped for space %d", - search_space_id, + workspace_id, exc_info=True, ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/tool.py b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/tool.py index a1240391b..2d9d0cf06 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/tool.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/tool.py @@ -56,7 +56,7 @@ _MCP_CACHE_MAX_SIZE = 50 _MCP_DISCOVERY_TIMEOUT_SECONDS = 30 _TOOL_CALL_MAX_RETRIES = 3 _TOOL_CALL_RETRY_DELAY = 1.5 # seconds, doubles per attempt -# Keyed by ``(search_space_id, bypass_internal_hitl)`` so single-agent and +# Keyed by ``(workspace_id, bypass_internal_hitl)`` so single-agent and # multi-agent paths cannot share tool closures with different HITL wiring. _MCPCacheKey = tuple[int, bool] _mcp_tools_cache: dict[_MCPCacheKey, tuple[float, list[StructuredTool]]] = {} @@ -814,7 +814,7 @@ async def _refresh_connector_token( await session.commit() await session.refresh(connector) - invalidate_mcp_tools_cache(connector.search_space_id) + invalidate_mcp_tools_cache(connector.workspace_id) return new_access @@ -990,7 +990,7 @@ async def _mark_connector_auth_expired(connector_id: int) -> None: "Marked MCP connector %s as auth_expired after unrecoverable 401", connector_id, ) - invalidate_mcp_tools_cache(connector.search_space_id) + invalidate_mcp_tools_cache(connector.workspace_id) except Exception: logger.warning( @@ -1000,10 +1000,10 @@ async def _mark_connector_auth_expired(connector_id: int) -> None: ) -def invalidate_mcp_tools_cache(search_space_id: int | None = None) -> None: +def invalidate_mcp_tools_cache(workspace_id: int | None = None) -> None: """Invalidate cached MCP tools (both ``bypass_internal_hitl`` variants together).""" - if search_space_id is not None: - for key in [k for k in _mcp_tools_cache if k[0] == search_space_id]: + if workspace_id is not None: + for key in [k for k in _mcp_tools_cache if k[0] == workspace_id]: _mcp_tools_cache.pop(key, None) else: _mcp_tools_cache.clear() @@ -1099,27 +1099,27 @@ async def discover_single_mcp_connector(connector_id: int) -> None: async def load_mcp_tools( session: AsyncSession, - search_space_id: int, + workspace_id: int, *, bypass_internal_hitl: bool = False, ) -> list[StructuredTool]: """Load all MCP tools from the user's active MCP server connectors. - Results are cached per ``(search_space_id, bypass_internal_hitl)`` for up + Results are cached per ``(workspace_id, bypass_internal_hitl)`` for up to 5 minutes; bypass is keyed because each variant builds a different tool closure (with vs. without the in-wrapper ``request_approval`` gate). """ _evict_expired_mcp_cache() now = time.monotonic() - cache_key: _MCPCacheKey = (search_space_id, bypass_internal_hitl) + cache_key: _MCPCacheKey = (workspace_id, bypass_internal_hitl) cached = _mcp_tools_cache.get(cache_key) if cached is not None: cached_at, cached_tools = cached if now - cached_at < _MCP_CACHE_TTL_SECONDS: logger.info( - "Using cached MCP tools for search space %s (%d tools, age=%.0fs, bypass_hitl=%s)", - search_space_id, + "Using cached MCP tools for workspace %s (%d tools, age=%.0fs, bypass_hitl=%s)", + workspace_id, len(cached_tools), now - cached_at, bypass_internal_hitl, @@ -1132,7 +1132,7 @@ async def load_mcp_tools( # Cast JSON -> JSONB so we can use has_key to filter by the presence of "server_config". result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, cast(SearchSourceConnector.config, JSONB).has_key("server_config"), ), ) @@ -1322,9 +1322,9 @@ async def load_mcp_tools( del _mcp_tools_cache[oldest_key] logger.info( - "Loaded %d MCP tools for search space %d (bypass_hitl=%s)", + "Loaded %d MCP tools for workspace %d (bypass_hitl=%s)", len(tools), - search_space_id, + workspace_id, bypass_internal_hitl, ) return tools diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/generate_image.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/generate_image.py index c481c6c3d..f9158335b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/generate_image.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/generate_image.py @@ -1,4 +1,4 @@ -"""Image generation via litellm; resolves model config from the search space and returns UI-ready payloads.""" +"""Image generation via litellm; resolves model config from the workspace and returns UI-ready payloads.""" import hashlib import logging @@ -18,7 +18,7 @@ from app.config import config from app.db import ( ImageGeneration, Model, - SearchSpace, + Workspace, shielded_async_session, ) from app.services.auto_model_pin_service import ( @@ -48,14 +48,14 @@ def _get_global_connection(connection_id: int) -> dict | None: def create_generate_image_tool( - search_space_id: int, + workspace_id: int, db_session: AsyncSession, image_gen_model_id_override: int | None = None, ): - """Create ``generate_image`` with bound search space; DB work uses a per-call session. + """Create ``generate_image`` with bound workspace; DB work uses a per-call session. ``image_gen_model_id_override``: when set (automations running on a - captured model), use this model id instead of reading the search space's + captured model), use this model id instead of reading the workspace's live ``image_gen_model_id``. """ del db_session # tool uses a fresh per-call session instead @@ -102,22 +102,22 @@ def create_generate_image_tool( # autoflushes from a concurrent writer poison this tool too. async with shielded_async_session() as session: result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - search_space = result.scalars().first() - if not search_space: + workspace = result.scalars().first() + if not workspace: return _failed( - {"error": "Search space not found"}, - error="Search space not found", + {"error": "Workspace not found"}, + error="Workspace not found", ) if image_gen_model_id_override is not None: # Automation run: use the captured image model, insulated from - # later search-space changes. No search-space read needed. + # later workspace changes. No workspace read needed. config_id = image_gen_model_id_override or IMAGE_GEN_AUTO_MODE_ID else: config_id = ( - search_space.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID + workspace.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID ) # size/quality/style are intentionally omitted: valid values @@ -129,8 +129,8 @@ def create_generate_image_tool( if is_image_gen_auto_mode(config_id): candidates = await auto_model_candidates( session, - search_space_id=search_space_id, - user_id=search_space.user_id, + workspace_id=workspace_id, + user_id=workspace.user_id, capability="image_gen", ) if not candidates: @@ -140,7 +140,7 @@ def create_generate_image_tool( ) return _failed({"error": err}, error=err) config_id = int( - choose_auto_model_candidate(candidates, search_space_id)["id"] + choose_auto_model_candidate(candidates, workspace_id)["id"] ) provider_base_url: str | None = None @@ -186,14 +186,14 @@ def create_generate_image_tool( return _failed({"error": err}, error=err) conn = db_model.connection if ( - conn.search_space_id is not None - and conn.search_space_id != search_space_id + conn.workspace_id is not None + and conn.workspace_id != workspace_id ): err = f"Image generation model {config_id} not found" return _failed({"error": err}, error=err) if ( conn.user_id is not None - and conn.user_id != search_space.user_id + and conn.user_id != workspace.user_id ): err = f"Image generation model {config_id} not found" return _failed({"error": err}, error=err) @@ -225,7 +225,7 @@ def create_generate_image_tool( n=n, image_gen_model_id=config_id, response_data=response_dict, - search_space_id=search_space_id, + workspace_id=workspace_id, access_token=access_token, ) session.add(db_image_gen) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/index.py index 8de95f2df..6db66a157 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/index.py @@ -28,28 +28,28 @@ def load_tools( d = {**(dependencies or {}), **kwargs} return [ create_generate_podcast_tool( - search_space_id=d["search_space_id"], + workspace_id=d["workspace_id"], db_session=d["db_session"], thread_id=d["thread_id"], ), create_generate_video_presentation_tool( - search_space_id=d["search_space_id"], + workspace_id=d["workspace_id"], db_session=d["db_session"], thread_id=d["thread_id"], ), create_generate_report_tool( - search_space_id=d["search_space_id"], + workspace_id=d["workspace_id"], thread_id=d["thread_id"], connector_service=d.get("connector_service"), available_connectors=d.get("available_connectors"), available_document_types=d.get("available_document_types"), ), create_generate_resume_tool( - search_space_id=d["search_space_id"], + workspace_id=d["workspace_id"], thread_id=d["thread_id"], ), create_generate_image_tool( - search_space_id=d["search_space_id"], + workspace_id=d["workspace_id"], db_session=d["db_session"], image_gen_model_id_override=d.get("image_gen_model_id_override"), ), diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py index d8d28ceb1..fb71a3a0d 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py @@ -28,11 +28,11 @@ logger = logging.getLogger(__name__) def create_generate_podcast_tool( - search_space_id: int, + workspace_id: int, db_session: AsyncSession, thread_id: int | None = None, ): - """Create ``generate_podcast`` with bound search space and thread; DB writes use a tool-local session.""" + """Create ``generate_podcast`` with bound workspace and thread; DB writes use a tool-local session.""" del db_session # writes use a fresh tool-local session, see below @tool @@ -77,13 +77,13 @@ def create_generate_podcast_tool( service = PodcastService(session) podcast = await service.create( title=podcast_title, - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=resolve_root_thread_id(runtime, thread_id), ) podcast.source_content = source_content spec = await propose_brief( session, - search_space_id=search_space_id, + workspace_id=workspace_id, focus=user_prompt, ) await service.attach_brief(podcast, spec) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/report.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/report.py index c80a2a565..e95d6f61e 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/report.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/report.py @@ -31,7 +31,7 @@ def _report_search_types( """Build the document-type scope for the shared KB search. ``None`` means "search every indexed type"; a tuple narrows the scope to the - connectors/document types the search space actually has. + connectors/document types the workspace actually has. """ types: set[str] = set() if available_document_types: @@ -561,7 +561,7 @@ async def _revise_with_sections( def create_generate_report_tool( - search_space_id: int, + workspace_id: int, thread_id: int | None = None, connector_service: ConnectorService | None = None, available_connectors: list[str] | None = None, @@ -728,7 +728,7 @@ def create_generate_report_tool( "error_message": error_msg, }, report_style=report_style, - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=resolve_root_thread_id(runtime, thread_id), report_group_id=report_group_id, ) @@ -769,7 +769,7 @@ def create_generate_report_tool( "creating standalone report" ) - llm = await get_agent_llm(read_session, search_space_id) + llm = await get_agent_llm(read_session, workspace_id) if not llm: error_msg = ( @@ -846,7 +846,7 @@ def create_generate_report_tool( async with shielded_async_session() as kb_session: return await search_chunks( kb_session, - search_space_id=search_space_id, + workspace_id=workspace_id, query=q, scope=scope, top_k=10, @@ -1044,7 +1044,7 @@ def create_generate_report_tool( content=report_content, report_metadata=metadata, report_style=report_style, - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=resolve_root_thread_id(runtime, thread_id), report_group_id=report_group_id, ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/resume.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/resume.py index 35dc996a1..c3475fb45 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/resume.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/resume.py @@ -421,7 +421,7 @@ def _validate_max_pages(max_pages: int) -> int: def create_generate_resume_tool( - search_space_id: int, + workspace_id: int, thread_id: int | None = None, ): """ @@ -531,7 +531,7 @@ def create_generate_resume_tool( "error_message": error_msg, }, report_style="resume", - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=resolve_root_thread_id(runtime, thread_id), report_group_id=report_group_id, ) @@ -581,7 +581,7 @@ def create_generate_resume_tool( f"(group {report_group_id})" ) - llm = await get_agent_llm(read_session, search_space_id) + llm = await get_agent_llm(read_session, workspace_id) if not llm: error_msg = ( @@ -819,7 +819,7 @@ def create_generate_resume_tool( content_type="typst", report_metadata=metadata, report_style="resume", - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=resolve_root_thread_id(runtime, thread_id), report_group_id=report_group_id, ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py index f0fcf6e73..3797ac3d8 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py @@ -31,11 +31,11 @@ logger = logging.getLogger(__name__) def create_generate_video_presentation_tool( - search_space_id: int, + workspace_id: int, db_session: AsyncSession, thread_id: int | None = None, ): - """Create ``generate_video_presentation`` with bound search space and thread; writes use a tool-local session.""" + """Create ``generate_video_presentation`` with bound workspace and thread; writes use a tool-local session.""" del db_session # writes use a fresh tool-local session, see below @tool @@ -60,7 +60,7 @@ def create_generate_video_presentation_tool( video_pres = VideoPresentation( title=video_title, status=VideoPresentationStatus.PENDING, - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=resolve_root_thread_id(runtime, thread_id), ) session.add(video_pres) @@ -75,7 +75,7 @@ def create_generate_video_presentation_tool( task = generate_video_presentation_task.delay( video_presentation_id=video_pres_id, source_content=source_content, - search_space_id=search_space_id, + workspace_id=workspace_id, user_prompt=user_prompt, ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/agent.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/agent.py index f193c2404..876593a47 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/agent.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/agent.py @@ -36,7 +36,7 @@ _KB_READONLY_RULESET = Ruleset(origin=READONLY_NAME, rules=[]) def _build_search_knowledge_base_tool(dependencies: dict[str, Any]) -> BaseTool: """Construct the hybrid-RAG ``search_knowledge_base`` tool from shared deps.""" return create_search_knowledge_base_tool( - search_space_id=dependencies["search_space_id"], + workspace_id=dependencies["workspace_id"], available_connectors=dependencies.get("available_connectors"), available_document_types=dependencies.get("available_document_types"), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py index 2684e9db7..f0692ebf6 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py @@ -115,7 +115,7 @@ def build_kb_middleware( fs_mw = build_filesystem_mw( backend_resolver=dependencies["backend_resolver"], filesystem_mode=filesystem_mode, - search_space_id=dependencies["search_space_id"], + workspace_id=dependencies["workspace_id"], user_id=dependencies.get("user_id"), thread_id=dependencies.get("thread_id"), read_only=read_only, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/tools/search_knowledge_base.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/tools/search_knowledge_base.py index c6559adee..ace65a7ab 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/tools/search_knowledge_base.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/knowledge_base/tools/search_knowledge_base.py @@ -89,7 +89,7 @@ def _resolve_mention_pins( async def _build_search_scope( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, document_types: tuple[str, ...] | None, runtime: ToolRuntime[None, SurfSenseFilesystemState], ) -> SearchScope: @@ -97,7 +97,7 @@ async def _build_search_scope( mentioned_document_ids, mentioned_folder_ids = _resolve_mention_pins(runtime) document_ids = await referenced_document_ids( session, - search_space_id=search_space_id, + workspace_id=workspace_id, document_ids=mentioned_document_ids, folder_ids=mentioned_folder_ids, ) @@ -109,13 +109,13 @@ async def _build_search_scope( def create_search_knowledge_base_tool( *, - search_space_id: int, + workspace_id: int, available_connectors: list[str] | None = None, available_document_types: list[str] | None = None, ) -> BaseTool: """Factory for the on-demand ``search_knowledge_base`` tool.""" - _space_id = search_space_id + _space_id = workspace_id _document_types = _search_types(available_connectors, available_document_types) async def _impl( @@ -140,13 +140,13 @@ def create_search_knowledge_base_tool( async with shielded_async_session() as session: scope = await _build_search_scope( session, - search_space_id=_space_id, + workspace_id=_space_id, document_types=_document_types, runtime=runtime, ) hits = await search_chunks( session, - search_space_id=_space_id, + workspace_id=_space_id, query=cleaned_query, scope=scope, top_k=clamped_top_k, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/system_prompt.md b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/system_prompt.md index b656c5019..5697b1a61 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/system_prompt.md +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/system_prompt.md @@ -6,7 +6,7 @@ Persist durable preferences/facts/instructions with `update_memory` while avoidi -Memory is search-space-scoped; do not assume cross-workspace visibility. +Memory is workspace-scoped; do not assume cross-workspace visibility. diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/index.py index 0afce9dec..e2feee478 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/index.py @@ -23,7 +23,7 @@ def load_tools( if d.get("thread_visibility") == ChatVisibility.SEARCH_SPACE: return [ create_update_team_memory_tool( - search_space_id=d["search_space_id"], + workspace_id=d["workspace_id"], db_session=d["db_session"], llm=d.get("llm"), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/update_memory.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/update_memory.py index 67bcc3e06..3361f13ae 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/update_memory.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/memory/tools/update_memory.py @@ -51,13 +51,13 @@ def create_update_memory_tool( def create_update_team_memory_tool( - search_space_id: int, + workspace_id: int, db_session: AsyncSession, llm: Any | None = None, ): @tool async def update_memory(updated_memory: str) -> dict[str, Any]: - """Update the team's shared memory document for this search space. + """Update the team's shared memory document for this workspace. The current team memory is shown in . Pass the FULL updated markdown document, not a diff. @@ -65,7 +65,7 @@ def create_update_team_memory_tool( try: result = await save_memory( scope=MemoryScope.TEAM, - target_id=search_space_id, + target_id=workspace_id, content=updated_memory, session=db_session, llm=llm, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/research/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/research/tools/index.py index 5fc2b5699..e1f199cdc 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/research/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/research/tools/index.py @@ -22,7 +22,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} return [ create_web_search_tool( - search_space_id=d.get("search_space_id"), + workspace_id=d.get("workspace_id"), available_connectors=d.get("available_connectors"), ), create_scrape_webpage_tool(firecrawl_api_key=d.get("firecrawl_api_key")), diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/create_event.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/create_event.py index 91a50b3cc..d233dd16d 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/create_event.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/create_event.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) def create_create_calendar_event_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -62,7 +62,7 @@ def create_create_calendar_event_tool( f"create_calendar_event called: summary='{summary}', start='{start_datetime}', end='{end_datetime}'" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Google Calendar tool not properly configured. Please contact support.", @@ -71,7 +71,7 @@ def create_create_calendar_event_tool( try: metadata_service = GoogleCalendarToolMetadataService(db_session) context = await metadata_service.get_creation_context( - search_space_id, user_id + workspace_id, user_id ) if "error" in context: @@ -138,7 +138,7 @@ def create_create_calendar_event_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_calendar_types), ) @@ -153,7 +153,7 @@ def create_create_calendar_event_tool( else: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_calendar_types), ) @@ -318,7 +318,7 @@ def create_create_calendar_event_tool( html_link=created.get("htmlLink"), description=final_description, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/delete_event.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/delete_event.py index 7682dae33..ac5e2374b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/delete_event.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/delete_event.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) def create_delete_calendar_event_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -56,7 +56,7 @@ def create_delete_calendar_event_tool( f"delete_calendar_event called: event_ref='{event_title_or_id}', delete_from_kb={delete_from_kb}" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Google Calendar tool not properly configured. Please contact support.", @@ -65,7 +65,7 @@ def create_delete_calendar_event_tool( try: metadata_service = GoogleCalendarToolMetadataService(db_session) context = await metadata_service.get_deletion_context( - search_space_id, user_id, event_title_or_id + workspace_id, user_id, event_title_or_id ) if "error" in context: @@ -143,7 +143,7 @@ def create_delete_calendar_event_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_calendar_types), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/index.py index b087105d4..2d7f38d50 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/index.py @@ -28,7 +28,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py index cf9a015cf..7439b2694 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py @@ -28,7 +28,7 @@ def _to_calendar_boundary(value: str, *, is_end: bool) -> str: def create_search_calendar_events_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -48,7 +48,7 @@ def create_search_calendar_events_tool( Dictionary with status and a list of events including event_id, summary, start, end, location, attendees. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Calendar tool not properly configured.", @@ -59,7 +59,7 @@ def create_search_calendar_events_tool( try: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_CALENDAR_TYPES), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/update_event.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/update_event.py index 78d3b147b..0ef5be996 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/update_event.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/calendar/tools/update_event.py @@ -32,7 +32,7 @@ def _build_time_body(value: str, context: dict[str, Any] | Any) -> dict[str, str def create_update_calendar_event_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -76,7 +76,7 @@ def create_update_calendar_event_tool( """ logger.info(f"update_calendar_event called: event_ref='{event_title_or_id}'") - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Google Calendar tool not properly configured. Please contact support.", @@ -85,7 +85,7 @@ def create_update_calendar_event_tool( try: metadata_service = GoogleCalendarToolMetadataService(db_session) context = await metadata_service.get_update_context( - search_space_id, user_id, event_title_or_id + workspace_id, user_id, event_title_or_id ) if "error" in context: @@ -176,7 +176,7 @@ def create_update_calendar_event_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_calendar_types), ) @@ -363,7 +363,7 @@ def create_update_calendar_event_tool( document_id=document_id, event_id=final_event_id, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/create_page.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/create_page.py index 17497eee2..7108f0168 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/create_page.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/create_page.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_create_confluence_page_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, connector_id: int | None = None, ): @@ -44,7 +44,7 @@ def create_create_confluence_page_tool( """ logger.info(f"create_confluence_page called: title='{title}'") - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Confluence tool not properly configured.", @@ -53,7 +53,7 @@ def create_create_confluence_page_tool( try: metadata_service = ConfluenceToolMetadataService(db_session) context = await metadata_service.get_creation_context( - search_space_id, user_id + workspace_id, user_id ) if "error" in context: @@ -103,7 +103,7 @@ def create_create_confluence_page_tool( if actual_connector_id is None: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, @@ -120,7 +120,7 @@ def create_create_confluence_page_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == actual_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, @@ -181,7 +181,7 @@ def create_create_confluence_page_tool( space_id=final_space_id, body_content=final_content, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/delete_page.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/delete_page.py index 5e2bd9868..febec227a 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/delete_page.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/delete_page.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_delete_confluence_page_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, connector_id: int | None = None, ): @@ -45,7 +45,7 @@ def create_delete_confluence_page_tool( f"delete_confluence_page called: page_title_or_id='{page_title_or_id}'" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Confluence tool not properly configured.", @@ -54,7 +54,7 @@ def create_delete_confluence_page_tool( try: metadata_service = ConfluenceToolMetadataService(db_session) context = await metadata_service.get_deletion_context( - search_space_id, user_id, page_title_or_id + workspace_id, user_id, page_title_or_id ) if "error" in context: @@ -112,7 +112,7 @@ def create_delete_confluence_page_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/index.py index 73350974e..02a11bc69 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/index.py @@ -26,7 +26,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], "connector_id": d.get("connector_id"), } diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/update_page.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/update_page.py index 7db9a24dc..42fa04a20 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/update_page.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/confluence/tools/update_page.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_update_confluence_page_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, connector_id: int | None = None, ): @@ -47,7 +47,7 @@ def create_update_confluence_page_tool( f"update_confluence_page called: page_title_or_id='{page_title_or_id}'" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Confluence tool not properly configured.", @@ -56,7 +56,7 @@ def create_update_confluence_page_tool( try: metadata_service = ConfluenceToolMetadataService(db_session) context = await metadata_service.get_update_context( - search_space_id, user_id, page_title_or_id + workspace_id, user_id, page_title_or_id ) if "error" in context: @@ -124,7 +124,7 @@ def create_update_confluence_page_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, @@ -183,7 +183,7 @@ def create_update_confluence_page_tool( document_id=final_document_id, page_id=final_page_id, user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, ) if kb_result["status"] == "success": kb_message_suffix = ( diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/_auth.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/_auth.py index 7636aff71..19d60908b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/_auth.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/_auth.py @@ -12,12 +12,12 @@ DISCORD_API = "https://discord.com/api/v10" async def get_discord_connector( db_session: AsyncSession, - search_space_id: int, + workspace_id: int, user_id: str, ) -> SearchSourceConnector | None: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/index.py index fcef3401a..3404857f1 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/index.py @@ -26,7 +26,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/list_channels.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/list_channels.py index 3cc99ac17..68c6b07a0 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/list_channels.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/list_channels.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def create_list_discord_channels_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -22,7 +22,7 @@ def create_list_discord_channels_tool( Returns: Dictionary with status and a list of channels (id, name). """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Discord tool not properly configured.", @@ -30,7 +30,7 @@ def create_list_discord_channels_tool( try: connector = await get_discord_connector( - db_session, search_space_id, user_id + db_session, workspace_id, user_id ) if not connector: return {"status": "error", "message": "No Discord connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/read_messages.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/read_messages.py index d8bf989a1..200289996 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/read_messages.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/read_messages.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def create_read_discord_messages_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -30,7 +30,7 @@ def create_read_discord_messages_tool( Dictionary with status and a list of messages including id, author, content, timestamp. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Discord tool not properly configured.", @@ -40,7 +40,7 @@ def create_read_discord_messages_tool( try: connector = await get_discord_connector( - db_session, search_space_id, user_id + db_session, workspace_id, user_id ) if not connector: return {"status": "error", "message": "No Discord connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/send_message.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/send_message.py index 59ea1de30..d6d6bf1bc 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/send_message.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/discord/tools/send_message.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_send_discord_message_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -36,7 +36,7 @@ def create_send_discord_message_tool( IMPORTANT: - If status is "rejected", the user explicitly declined. Do NOT retry. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Discord tool not properly configured.", @@ -50,7 +50,7 @@ def create_send_discord_message_tool( try: connector = await get_discord_connector( - db_session, search_space_id, user_id + db_session, workspace_id, user_id ) if not connector: return {"status": "error", "message": "No Discord connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/create_file.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/create_file.py index 7732c35e5..c858fe532 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/create_file.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/create_file.py @@ -58,7 +58,7 @@ def _markdown_to_docx(markdown_text: str) -> bytes: def create_create_dropbox_file_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -84,7 +84,7 @@ def create_create_dropbox_file_tool( f"create_dropbox_file called: name='{name}', file_type='{file_type}'" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Dropbox tool not properly configured.", @@ -93,7 +93,7 @@ def create_create_dropbox_file_tool( try: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DROPBOX_CONNECTOR, @@ -195,7 +195,7 @@ def create_create_dropbox_file_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DROPBOX_CONNECTOR, @@ -244,7 +244,7 @@ def create_create_dropbox_file_tool( web_url=web_url, content=final_content, connector_id=connector.id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/index.py index 440b4583c..757fe58bc 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/index.py @@ -25,7 +25,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/trash_file.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/trash_file.py index c713bdd00..11ca73d0c 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/trash_file.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/dropbox/tools/trash_file.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) def create_delete_dropbox_file_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -57,7 +57,7 @@ def create_delete_dropbox_file_tool( f"delete_dropbox_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Dropbox tool not properly configured.", @@ -72,7 +72,7 @@ def create_delete_dropbox_file_tool( ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.DROPBOX_FILE, func.lower(Document.title) == func.lower(file_name), SearchSourceConnector.user_id == user_id, @@ -92,7 +92,7 @@ def create_delete_dropbox_file_tool( ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.DROPBOX_FILE, func.lower( cast( @@ -140,7 +140,7 @@ def create_delete_dropbox_file_tool( select(SearchSourceConnector).filter( and_( SearchSourceConnector.id == document.connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DROPBOX_CONNECTOR, @@ -202,7 +202,7 @@ def create_delete_dropbox_file_tool( select(SearchSourceConnector).filter( and_( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.DROPBOX_CONNECTOR, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/create_draft.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/create_draft.py index 3f25305c5..cd3f9a67e 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/create_draft.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/create_draft.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) def create_create_gmail_draft_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -59,7 +59,7 @@ def create_create_gmail_draft_tool( """ logger.info(f"create_gmail_draft called: to='{to}', subject='{subject}'") - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Gmail tool not properly configured. Please contact support.", @@ -68,7 +68,7 @@ def create_create_gmail_draft_tool( try: metadata_service = GmailToolMetadataService(db_session) context = await metadata_service.get_creation_context( - search_space_id, user_id + workspace_id, user_id ) if "error" in context: @@ -127,7 +127,7 @@ def create_create_gmail_draft_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_gmail_types), ) @@ -142,7 +142,7 @@ def create_create_gmail_draft_tool( else: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_gmail_types), ) @@ -307,7 +307,7 @@ def create_create_gmail_draft_tool( date_str=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), body_text=final_body, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, draft_id=created.get("id"), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/index.py index 60405dcf7..31459bdff 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/index.py @@ -29,7 +29,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py index 10c64c6c5..e1162edef 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py @@ -17,7 +17,7 @@ _GMAIL_TYPES = [ def create_read_gmail_email_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -32,13 +32,13 @@ def create_read_gmail_email_tool( Returns: Dictionary with status and the full email content formatted as markdown. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Gmail tool not properly configured."} try: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_GMAIL_TYPES), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py index 2c633d629..2ed04a26e 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py @@ -17,7 +17,7 @@ _GMAIL_TYPES = [ def create_search_gmail_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -38,7 +38,7 @@ def create_search_gmail_tool( Dictionary with status and a list of email summaries including message_id, subject, from, date, snippet. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Gmail tool not properly configured."} max_results = min(max_results, 20) @@ -46,7 +46,7 @@ def create_search_gmail_tool( try: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_GMAIL_TYPES), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/send_email.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/send_email.py index 3431a2bc3..89af473b7 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/send_email.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/send_email.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) def create_send_gmail_email_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -86,7 +86,7 @@ def create_send_gmail_email_tool( tool_call_id=runtime.tool_call_id, ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: msg = "Gmail tool not properly configured. Please contact support." return _emit( {"status": "error", "message": msg}, @@ -97,7 +97,7 @@ def create_send_gmail_email_tool( try: metadata_service = GmailToolMetadataService(db_session) context = await metadata_service.get_creation_context( - search_space_id, user_id + workspace_id, user_id ) if "error" in context: @@ -168,7 +168,7 @@ def create_send_gmail_email_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_gmail_types), ) @@ -187,7 +187,7 @@ def create_send_gmail_email_tool( else: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_gmail_types), ) @@ -355,7 +355,7 @@ def create_send_gmail_email_tool( date_str=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), body_text=final_body, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/trash_email.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/trash_email.py index ef5882074..2302492a9 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/trash_email.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/trash_email.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_trash_gmail_email_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -57,7 +57,7 @@ def create_trash_gmail_email_tool( f"trash_gmail_email called: email_subject_or_id='{email_subject_or_id}', delete_from_kb={delete_from_kb}" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Gmail tool not properly configured. Please contact support.", @@ -66,7 +66,7 @@ def create_trash_gmail_email_tool( try: metadata_service = GmailToolMetadataService(db_session) context = await metadata_service.get_trash_context( - search_space_id, user_id, email_subject_or_id + workspace_id, user_id, email_subject_or_id ) if "error" in context: @@ -144,7 +144,7 @@ def create_trash_gmail_email_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_gmail_types), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/update_draft.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/update_draft.py index ef7839a1a..1db35883a 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/update_draft.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/gmail/tools/update_draft.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) def create_update_gmail_draft_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -78,7 +78,7 @@ def create_update_gmail_draft_tool( f"update_gmail_draft called: draft_subject_or_id='{draft_subject_or_id}'" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Gmail tool not properly configured. Please contact support.", @@ -87,7 +87,7 @@ def create_update_gmail_draft_tool( try: metadata_service = GmailToolMetadataService(db_session) context = await metadata_service.get_update_context( - search_space_id, user_id, draft_subject_or_id + workspace_id, user_id, draft_subject_or_id ) if "error" in context: @@ -174,7 +174,7 @@ def create_update_gmail_draft_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_gmail_types), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/create_file.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/create_file.py index 9de4e0a4b..24a2d1685 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/create_file.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/create_file.py @@ -22,7 +22,7 @@ _MIME_MAP: dict[str, str] = { def create_create_google_drive_file_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -67,7 +67,7 @@ def create_create_google_drive_file_tool( f"create_google_drive_file called: name='{name}', type='{file_type}'" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Google Drive tool not properly configured. Please contact support.", @@ -82,7 +82,7 @@ def create_create_google_drive_file_tool( try: metadata_service = GoogleDriveToolMetadataService(db_session) context = await metadata_service.get_creation_context( - search_space_id, user_id + workspace_id, user_id ) if "error" in context: @@ -149,7 +149,7 @@ def create_create_google_drive_file_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_drive_types), ) @@ -164,7 +164,7 @@ def create_create_google_drive_file_tool( else: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_drive_types), ) @@ -288,7 +288,7 @@ def create_create_google_drive_file_tool( web_view_link=created.get("webViewLink"), content=final_content, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/index.py index caf06d6ba..33d9c8451 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/index.py @@ -25,7 +25,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/trash_file.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/trash_file.py index c89b54c8e..5d25582ee 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/trash_file.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/google_drive/tools/trash_file.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_delete_google_drive_file_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -57,7 +57,7 @@ def create_delete_google_drive_file_tool( f"delete_google_drive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "Google Drive tool not properly configured. Please contact support.", @@ -66,7 +66,7 @@ def create_delete_google_drive_file_tool( try: metadata_service = GoogleDriveToolMetadataService(db_session) context = await metadata_service.get_trash_context( - search_space_id, user_id, file_name + workspace_id, user_id, file_name ) if "error" in context: @@ -144,7 +144,7 @@ def create_delete_google_drive_file_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(_drive_types), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/_auth.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/_auth.py index c6d1cd148..9dd4acbef 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/_auth.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/_auth.py @@ -10,12 +10,12 @@ LUMA_API = "https://public-api.luma.com/v1" async def get_luma_connector( db_session: AsyncSession, - search_space_id: int, + workspace_id: int, user_id: str, ) -> SearchSourceConnector | None: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LUMA_CONNECTOR, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/create_event.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/create_event.py index 0dffb2d2c..777cc7169 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/create_event.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/create_event.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_create_luma_event_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -42,11 +42,11 @@ def create_create_luma_event_tool( IMPORTANT: - If status is "rejected", the user explicitly declined. Do NOT retry. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Luma tool not properly configured."} try: - connector = await get_luma_connector(db_session, search_space_id, user_id) + connector = await get_luma_connector(db_session, workspace_id, user_id) if not connector: return {"status": "error", "message": "No Luma connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/index.py index a479331bb..d6c4d05e7 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/index.py @@ -26,7 +26,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/list_events.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/list_events.py index aec5ad220..35f01bc63 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/list_events.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/list_events.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def create_list_luma_events_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -28,13 +28,13 @@ def create_list_luma_events_tool( Dictionary with status and a list of events including event_id, name, start_at, end_at, location, url. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Luma tool not properly configured."} max_results = min(max_results, 50) try: - connector = await get_luma_connector(db_session, search_space_id, user_id) + connector = await get_luma_connector(db_session, workspace_id, user_id) if not connector: return {"status": "error", "message": "No Luma connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/read_event.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/read_event.py index b37a9d617..a9fe06657 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/read_event.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/luma/tools/read_event.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def create_read_luma_event_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -26,11 +26,11 @@ def create_read_luma_event_tool( Dictionary with status and full event details including description, attendees count, meeting URL. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Luma tool not properly configured."} try: - connector = await get_luma_connector(db_session, search_space_id, user_id) + connector = await get_luma_connector(db_session, workspace_id, user_id) if not connector: return {"status": "error", "message": "No Luma connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/create_page.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/create_page.py index 49ee0f3aa..57f14a849 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/create_page.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/create_page.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) def create_create_notion_page_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, connector_id: int | None = None, ): @@ -24,7 +24,7 @@ def create_create_notion_page_tool( Args: db_session: Database session for accessing Notion connector - search_space_id: Search space ID to find the Notion connector + workspace_id: Workspace ID to find the Notion connector user_id: User ID for fetching user-specific context connector_id: Optional specific connector ID (if known) @@ -69,7 +69,7 @@ def create_create_notion_page_tool( """ logger.info(f"create_notion_page called: title='{title}'") - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: logger.error( "Notion tool not properly configured - missing required parameters" ) @@ -81,7 +81,7 @@ def create_create_notion_page_tool( try: metadata_service = NotionToolMetadataService(db_session) context = await metadata_service.get_creation_context( - search_space_id, user_id + workspace_id, user_id ) if "error" in context: @@ -144,7 +144,7 @@ def create_create_notion_page_tool( if actual_connector_id is None: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, @@ -154,7 +154,7 @@ def create_create_notion_page_tool( if not connector: logger.warning( - f"No Notion connector found for search_space_id={search_space_id}" + f"No Notion connector found for workspace_id={workspace_id}" ) return { "status": "error", @@ -167,7 +167,7 @@ def create_create_notion_page_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == actual_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, @@ -177,7 +177,7 @@ def create_create_notion_page_tool( if not connector: logger.error( - f"Invalid connector_id={actual_connector_id} for search_space_id={search_space_id}" + f"Invalid connector_id={actual_connector_id} for workspace_id={workspace_id}" ) return { "status": "error", @@ -211,7 +211,7 @@ def create_create_notion_page_tool( page_url=result.get("url"), content=final_content, connector_id=actual_connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/delete_page.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/delete_page.py index a187b2cbc..5c7a8f34c 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/delete_page.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/delete_page.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) def create_delete_notion_page_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, connector_id: int | None = None, ): @@ -28,7 +28,7 @@ def create_delete_notion_page_tool( Args: db_session: Database session for accessing Notion connector - search_space_id: Search space ID to find the Notion connector + workspace_id: Workspace ID to find the Notion connector user_id: User ID for finding the correct Notion connector connector_id: Optional specific connector ID (if known) @@ -91,7 +91,7 @@ def create_delete_notion_page_tool( tool_call_id=runtime.tool_call_id, ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: logger.error( "Notion tool not properly configured - missing required parameters" ) @@ -108,7 +108,7 @@ def create_delete_notion_page_tool( # Get page context (page_id, account, title) from indexed data metadata_service = NotionToolMetadataService(db_session) context = await metadata_service.get_delete_context( - search_space_id, user_id, page_title + workspace_id, user_id, page_title ) if "error" in context: @@ -193,7 +193,7 @@ def create_delete_notion_page_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, @@ -203,7 +203,7 @@ def create_delete_notion_page_tool( if not connector: logger.error( - f"Invalid connector_id={final_connector_id} for search_space_id={search_space_id}" + f"Invalid connector_id={final_connector_id} for workspace_id={workspace_id}" ) return _emit( { diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/index.py index b8f662b03..4e8ed3c68 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/index.py @@ -26,7 +26,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/update_page.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/update_page.py index 6950f0abd..0fb529e89 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/update_page.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/notion/tools/update_page.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) def create_update_notion_page_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, connector_id: int | None = None, ): @@ -24,7 +24,7 @@ def create_update_notion_page_tool( Args: db_session: Database session for accessing Notion connector - search_space_id: Search space ID to find the Notion connector + workspace_id: Workspace ID to find the Notion connector user_id: User ID for fetching user-specific context connector_id: Optional specific connector ID (if known) @@ -73,7 +73,7 @@ def create_update_notion_page_tool( f"update_notion_page called: page_title='{page_title}', content_length={len(content) if content else 0}" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: logger.error( "Notion tool not properly configured - missing required parameters" ) @@ -92,7 +92,7 @@ def create_update_notion_page_tool( try: metadata_service = NotionToolMetadataService(db_session) context = await metadata_service.get_update_context( - search_space_id, user_id, page_title + workspace_id, user_id, page_title ) if "error" in context: @@ -165,7 +165,7 @@ def create_update_notion_page_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, @@ -175,7 +175,7 @@ def create_update_notion_page_tool( if not connector: logger.error( - f"Invalid connector_id={final_connector_id} for search_space_id={search_space_id}" + f"Invalid connector_id={final_connector_id} for workspace_id={workspace_id}" ) return { "status": "error", @@ -212,7 +212,7 @@ def create_update_notion_page_tool( document_id=document_id, appended_content=final_content, user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, appended_block_ids=result.get("appended_block_ids"), ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/create_file.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/create_file.py index 11160650d..ac5ba2451 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/create_file.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/create_file.py @@ -47,7 +47,7 @@ def _markdown_to_docx(markdown_text: str) -> bytes: def create_create_onedrive_file_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -72,7 +72,7 @@ def create_create_onedrive_file_tool( """ logger.info(f"create_onedrive_file called: name='{name}'") - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "OneDrive tool not properly configured.", @@ -81,7 +81,7 @@ def create_create_onedrive_file_tool( try: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR, @@ -179,7 +179,7 @@ def create_create_onedrive_file_tool( result = await db_session.execute( select(SearchSourceConnector).filter( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR, @@ -221,7 +221,7 @@ def create_create_onedrive_file_tool( web_url=created.get("webUrl"), content=final_content, connector_id=connector.id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if kb_result["status"] == "success": diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/index.py index 4f0a2a7d6..2795bc14e 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/index.py @@ -25,7 +25,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/trash_file.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/trash_file.py index 7b4e0b98c..be1b0493c 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/trash_file.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/onedrive/tools/trash_file.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) def create_delete_onedrive_file_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -58,7 +58,7 @@ def create_delete_onedrive_file_tool( f"delete_onedrive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}" ) - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return { "status": "error", "message": "OneDrive tool not properly configured.", @@ -73,7 +73,7 @@ def create_delete_onedrive_file_tool( ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.ONEDRIVE_FILE, func.lower(Document.title) == func.lower(file_name), SearchSourceConnector.user_id == user_id, @@ -93,7 +93,7 @@ def create_delete_onedrive_file_tool( ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.ONEDRIVE_FILE, func.lower( cast( @@ -140,7 +140,7 @@ def create_delete_onedrive_file_tool( select(SearchSourceConnector).filter( and_( SearchSourceConnector.id == document.connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR, @@ -202,7 +202,7 @@ def create_delete_onedrive_file_tool( select(SearchSourceConnector).filter( and_( SearchSourceConnector.id == final_connector_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/_auth.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/_auth.py index 7cdbeb819..e6600e828 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/_auth.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/_auth.py @@ -10,12 +10,12 @@ GRAPH_API = "https://graph.microsoft.com/v1.0" async def get_teams_connector( db_session: AsyncSession, - search_space_id: int, + workspace_id: int, user_id: str, ) -> SearchSourceConnector | None: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.TEAMS_CONNECTOR, diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/index.py index d144eee82..ae4959d5b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/index.py @@ -26,7 +26,7 @@ def load_tools( d = {**(dependencies or {}), **kwargs} common = { "db_session": d["db_session"], - "search_space_id": d["search_space_id"], + "workspace_id": d["workspace_id"], "user_id": d["user_id"], } return [ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/list_channels.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/list_channels.py index d7b000853..ca031b35b 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/list_channels.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/list_channels.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def create_list_teams_channels_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -23,11 +23,11 @@ def create_list_teams_channels_tool( Dictionary with status and a list of teams, each containing team_id, team_name, and a list of channels (id, name). """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Teams tool not properly configured."} try: - connector = await get_teams_connector(db_session, search_space_id, user_id) + connector = await get_teams_connector(db_session, workspace_id, user_id) if not connector: return {"status": "error", "message": "No Teams connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/read_messages.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/read_messages.py index d24a7e4d3..ff3b7c75a 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/read_messages.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/read_messages.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def create_read_teams_messages_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -32,13 +32,13 @@ def create_read_teams_messages_tool( Dictionary with status and a list of messages including id, sender, content, timestamp. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Teams tool not properly configured."} limit = min(limit, 50) try: - connector = await get_teams_connector(db_session, search_space_id, user_id) + connector = await get_teams_connector(db_session, workspace_id, user_id) if not connector: return {"status": "error", "message": "No Teams connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/send_message.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/send_message.py index c4491e82e..47c0fb700 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/send_message.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/connectors/teams/tools/send_message.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def create_send_teams_message_tool( db_session: AsyncSession | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, user_id: str | None = None, ): @tool @@ -41,11 +41,11 @@ def create_send_teams_message_tool( IMPORTANT: - If status is "rejected", the user explicitly declined. Do NOT retry. """ - if db_session is None or search_space_id is None or user_id is None: + if db_session is None or workspace_id is None or user_id is None: return {"status": "error", "message": "Teams tool not properly configured."} try: - connector = await get_teams_connector(db_session, search_space_id, user_id) + connector = await get_teams_connector(db_session, workspace_id, user_id) if not connector: return {"status": "error", "message": "No Teams connector found."} diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/mcp_tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/mcp_tools/index.py index 436b13aea..334f2130a 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/mcp_tools/index.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/mcp_tools/index.py @@ -29,12 +29,12 @@ logger = logging.getLogger(__name__) async def fetch_mcp_connector_metadata_maps( session: AsyncSession, - search_space_id: int, + workspace_id: int, ) -> tuple[dict[int, str], dict[str, str]]: """Resolve connector id and display name to connector type for MCP tool routing.""" result = await session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, cast(SearchSourceConnector.config, JSONB).has_key("server_config"), ), ) @@ -101,13 +101,13 @@ def partition_mcp_tools_by_connector( async def load_mcp_tools_by_connector( session: AsyncSession, - search_space_id: int, + workspace_id: int, ) -> dict[str, list[BaseTool]]: """Load MCP tools and route them to each subagent as a flat list. ``bypass_internal_hitl=True`` is set so tool gating is uniformly the consuming subagent's :class:`PermissionMiddleware` responsibility. """ - flat = await load_mcp_tools(session, search_space_id, bypass_internal_hitl=True) - id_map, name_map = await fetch_mcp_connector_metadata_maps(session, search_space_id) + flat = await load_mcp_tools(session, workspace_id, bypass_internal_hitl=True) + id_map, name_map = await fetch_mcp_connector_metadata_maps(session, workspace_id) return partition_mcp_tools_by_connector(flat, id_map, name_map) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/hitl/approvals/self_gated/auto_approved.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/hitl/approvals/self_gated/auto_approved.py index b99b26f3a..e8056eb27 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/hitl/approvals/self_gated/auto_approved.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/hitl/approvals/self_gated/auto_approved.py @@ -12,7 +12,7 @@ untouched. This keeps tool bodies (logging, metadata fetches, account fallbacks) symmetrical with the prompted path; the only behavior change is "no interrupt fires". -Per-search-space ``agent_permission_rules`` (when wired) take precedence and +Per-workspace ``agent_permission_rules`` (when wired) take precedence and can re-enable prompting for any of these. """ diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/spec.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/spec.py index 6c68b96db..b3aa57c05 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/spec.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/shared/spec.py @@ -13,7 +13,7 @@ from app.agents.chat.multi_agent_chat.shared.permissions import Ruleset # A context-hint provider receives the parent-agent ``runtime.state`` mapping # and the ``description`` the orchestrator wrote, and returns a short string # the runtime prepends to the subagent's first ``HumanMessage``. Used for -# things like "current search-space id is X" or "the user is in workspace Y" — +# things like "current workspace id is X" or "the user is in workspace Y" — # never for full corpora, since the prepended text consumes the subagent's # prompt budget on every invocation. Return ``None`` (or an empty string) to # skip the hint for this call. @@ -52,7 +52,7 @@ class SurfSenseSubagentSpec: invocation, immediately before the subagent runs. Its return value is prepended to the subagent's first ``HumanMessage`` so the subagent can see things it would otherwise have to discover - (active search space, KB root, current user timezone, etc.). + (active workspace, KB root, current user timezone, etc.). Kept out of the deepagents ``spec`` because that dict is forwarded verbatim to upstream code and only recognises its own typed keys. """ diff --git a/surfsense_backend/app/agents/chat/runtime/mention_resolver.py b/surfsense_backend/app/agents/chat/runtime/mention_resolver.py index 4f2f47b24..7996bacd6 100644 --- a/surfsense_backend/app/agents/chat/runtime/mention_resolver.py +++ b/surfsense_backend/app/agents/chat/runtime/mention_resolver.py @@ -89,7 +89,7 @@ def _folder_virtual_path(folder_id: int, folder_paths: dict[int, str]) -> str: """Return ``/documents/Folder/Sub/`` for a folder id. Falls back to the documents root when the folder is missing from - the index (deleted or in a different search space). Trailing slash + the index (deleted or in a different workspace). Trailing slash matches ``KnowledgeTreeMiddleware`` (``/documents/MyFolder/``) so the agent's ``ls`` can dispatch on it as a directory. """ @@ -100,7 +100,7 @@ def _folder_virtual_path(folder_id: int, folder_paths: dict[int, str]) -> str: async def resolve_mentions( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, mentioned_documents: list[MentionedDocumentInfo] | None, mentioned_document_ids: list[int] | None = None, mentioned_folder_ids: list[int] | None = None, @@ -151,13 +151,13 @@ async def resolve_mentions( if not doc_id_pool and not folder_id_pool: return ResolvedMentionSet() - index = await build_path_index(session, search_space_id) + index = await build_path_index(session, workspace_id) doc_rows: dict[int, Document] = {} if doc_id_pool: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.id.in_(doc_id_pool), ) ) @@ -168,7 +168,7 @@ async def resolve_mentions( if folder_id_pool: result = await session.execute( select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.id.in_(folder_id_pool), ) ) @@ -185,7 +185,7 @@ async def resolve_mentions( logger.debug( "mention_resolver: dropping doc id=%s (not found in space=%s)", doc_id, - search_space_id, + workspace_id, ) continue title = chip_titles_by_id.get(("doc", doc_id), str(row.title or "")) @@ -206,7 +206,7 @@ async def resolve_mentions( logger.debug( "mention_resolver: dropping folder id=%s (not found in space=%s)", folder_id, - search_space_id, + workspace_id, ) continue title = chip_titles_by_id.get(("folder", folder_id), str(row.name or "")) diff --git a/surfsense_backend/app/agents/chat/runtime/path_resolver.py b/surfsense_backend/app/agents/chat/runtime/path_resolver.py index 84282b63b..92a6b1934 100644 --- a/surfsense_backend/app/agents/chat/runtime/path_resolver.py +++ b/surfsense_backend/app/agents/chat/runtime/path_resolver.py @@ -98,12 +98,12 @@ class PathIndex: async def _build_folder_paths( session: AsyncSession, - search_space_id: int, + workspace_id: int, ) -> dict[int, str]: """Compute ``Folder.id`` -> absolute virtual path under ``/documents``.""" result = await session.execute( select(Folder.id, Folder.name, Folder.parent_id).where( - Folder.search_space_id == search_space_id + Folder.workspace_id == workspace_id ) ) rows = result.all() @@ -133,11 +133,11 @@ async def _build_folder_paths( async def build_path_index( session: AsyncSession, - search_space_id: int, + workspace_id: int, *, populate_occupants: bool = True, ) -> PathIndex: - """Build a :class:`PathIndex` for a search space. + """Build a :class:`PathIndex` for a workspace. ``populate_occupants`` controls whether the occupancy map is pre-seeded from existing ``Document`` rows. Most callers want this so that @@ -145,12 +145,12 @@ async def build_path_index( the persistence middleware sets this to ``False`` when it is iterating to decide where to place fresh documents. """ - folder_paths = await _build_folder_paths(session, search_space_id) + folder_paths = await _build_folder_paths(session, workspace_id) occupants: dict[str, int] = {} if populate_occupants: rows = await session.execute( select(Document.id, Document.title, Document.folder_id).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) ) for row in rows.all(): @@ -189,7 +189,7 @@ def doc_to_virtual_path( async def virtual_path_to_doc( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, virtual_path: str, ) -> Document | None: """Resolve a virtual path back to a ``Document`` row. @@ -198,7 +198,7 @@ async def virtual_path_to_doc( 1. ``Document.unique_identifier_hash`` lookup (fast path for paths created by SurfSense itself — every NOTE write goes through this hash). 2. If the basename carries a ``" ().xml"`` disambiguation suffix, - try a direct id lookup constrained to the search space. + try a direct id lookup constrained to the workspace. 3. Title-from-basename + folder-resolution lookup as a last resort. """ if not virtual_path or not virtual_path.startswith(DOCUMENTS_ROOT): @@ -207,11 +207,11 @@ async def virtual_path_to_doc( unique_hash = generate_unique_identifier_hash( DocumentType.NOTE, virtual_path, - search_space_id, + workspace_id, ) result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.unique_identifier_hash == unique_hash, ) ) @@ -232,7 +232,7 @@ async def virtual_path_to_doc( if suffix_doc_id is not None: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.id == suffix_doc_id, ) ) @@ -241,7 +241,7 @@ async def virtual_path_to_doc( return document folder_id = await _resolve_folder_id( - session, search_space_id=search_space_id, folder_parts=folder_parts + session, workspace_id=workspace_id, folder_parts=folder_parts ) title_candidates: list[str] = [] raw_title = stem @@ -253,7 +253,7 @@ async def virtual_path_to_doc( if not candidate: continue query = select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.title == candidate, ) if folder_id is None: @@ -272,7 +272,7 @@ async def virtual_path_to_doc( # filename back here. Scan all documents in the resolved folder and match # by ``safe_filename(title)`` to recover the original document. folder_scan = select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) if folder_id is None: folder_scan = folder_scan.where(Document.folder_id.is_(None)) @@ -289,7 +289,7 @@ async def virtual_path_to_doc( async def _resolve_folder_id( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, folder_parts: list[str], ) -> int | None: """Look up the leaf folder id for a chain of folder names; return ``None`` if missing.""" @@ -299,7 +299,7 @@ async def _resolve_folder_id( for raw in folder_parts: name = safe_folder_segment(raw) query = select(Folder.id).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.name == name, ) if parent_id is None: diff --git a/surfsense_backend/app/agents/chat/runtime/referenced_chat_context/resolver.py b/surfsense_backend/app/agents/chat/runtime/referenced_chat_context/resolver.py index bd6c2e150..c1e4ef9eb 100644 --- a/surfsense_backend/app/agents/chat/runtime/referenced_chat_context/resolver.py +++ b/surfsense_backend/app/agents/chat/runtime/referenced_chat_context/resolver.py @@ -19,7 +19,7 @@ from app.db import ( NewChatMessage, NewChatMessageRole, NewChatThread, - SearchSpace, + Workspace, ) from app.tasks.chat.llm_history_normalizer import ( assistant_content_to_llm_text, @@ -35,8 +35,8 @@ def _accessible_thread_filter(user_uuid: UUID | None, *, include_legacy: bool): """Visibility predicate mirroring ``new_chat_routes.search_threads``. A thread is referenceable when the requester created it, it is shared - with the search space, or it is a legacy null-creator thread and the - requester owns the search space (``include_legacy``). Anything else is + with the workspace, or it is a legacy null-creator thread and the + requester owns the workspace (``include_legacy``). Anything else is dropped (fail-closed). """ conditions = [NewChatThread.visibility == ChatVisibility.SEARCH_SPACE] @@ -50,7 +50,7 @@ def _accessible_thread_filter(user_uuid: UUID | None, *, include_legacy: bool): async def resolve_referenced_chats( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, requesting_user_id: str | None, current_chat_id: int, mentioned_thread_ids: list[int] | None, @@ -82,19 +82,19 @@ async def resolve_referenced_chats( if not requested_ids: return [] - # Legacy null-creator threads are referenceable only by the search-space + # Legacy null-creator threads are referenceable only by the workspace # owner, matching ``search_threads`` (the source the picker reads from). include_legacy = False if user_uuid is not None: owner_id = await session.scalar( - select(SearchSpace.user_id).where(SearchSpace.id == search_space_id) + select(Workspace.user_id).where(Workspace.id == workspace_id) ) include_legacy = owner_id == user_uuid thread_rows = await session.execute( select(NewChatThread).where( NewChatThread.id.in_(requested_ids), - NewChatThread.search_space_id == search_space_id, + NewChatThread.workspace_id == workspace_id, _accessible_thread_filter(user_uuid, include_legacy=include_legacy), ) ) @@ -103,7 +103,7 @@ async def resolve_referenced_chats( "resolve_referenced_chats: requested=%s accessible=%s space=%s user=%s", requested_ids, sorted(threads_by_id.keys()), - search_space_id, + workspace_id, user_uuid, ) if not threads_by_id: @@ -119,7 +119,7 @@ async def resolve_referenced_chats( "resolve_referenced_chats: dropping thread id=%s " "(not accessible in space=%s)", thread_id, - search_space_id, + workspace_id, ) continue turns = turns_by_thread.get(thread_id, []) diff --git a/surfsense_backend/app/agents/chat/runtime/references/__init__.py b/surfsense_backend/app/agents/chat/runtime/references/__init__.py index 62530fd71..40e0249d3 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/__init__.py +++ b/surfsense_backend/app/agents/chat/runtime/references/__init__.py @@ -29,7 +29,7 @@ from .reference_pointers import render_reference_pointers async def resolve_references( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, requesting_user_id: str | None, current_chat_id: int, document_ids: list[int] | None = None, @@ -46,18 +46,18 @@ async def resolve_references( references: list[Reference] = [] if document_ids or folder_ids: - index = await build_path_index(session, search_space_id) + index = await build_path_index(session, workspace_id) if document_ids: references += await resolve_document_references( session, - search_space_id=search_space_id, + workspace_id=workspace_id, document_ids=document_ids, index=index, ) if folder_ids: references += await resolve_folder_references( session, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_ids=folder_ids, index=index, ) @@ -65,7 +65,7 @@ async def resolve_references( if connector_ids: references += await resolve_connector_references( session, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_ids=connector_ids, chips=connector_chips, ) @@ -73,7 +73,7 @@ async def resolve_references( if thread_ids: references += await resolve_chat_references( session, - search_space_id=search_space_id, + workspace_id=workspace_id, requesting_user_id=requesting_user_id, current_chat_id=current_chat_id, thread_ids=thread_ids, diff --git a/surfsense_backend/app/agents/chat/runtime/references/chat/access.py b/surfsense_backend/app/agents/chat/runtime/references/chat/access.py index 1f7614b06..df18f99ff 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/chat/access.py +++ b/surfsense_backend/app/agents/chat/runtime/references/chat/access.py @@ -1,8 +1,8 @@ """Access-checked lookup of chat threads the requester may read. The single place chat visibility is enforced: a thread is readable when it is -shared with the search space, the requester created it, or it is a legacy -null-creator thread and the requester owns the search space. Anything else is +shared with the workspace, the requester created it, or it is a legacy +null-creator thread and the requester owns the workspace. Anything else is dropped (fail-closed). """ @@ -14,7 +14,7 @@ from uuid import UUID from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession -from app.db import ChatVisibility, NewChatThread, SearchSpace +from app.db import ChatVisibility, NewChatThread, Workspace logger = logging.getLogger(__name__) @@ -32,7 +32,7 @@ def _visibility_predicate(user_uuid: UUID | None, *, include_legacy: bool): async def accessible_threads( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, requesting_user_id: str | None, thread_ids: list[int], exclude_thread_id: int | None = None, @@ -57,18 +57,18 @@ async def accessible_threads( requesting_user_id, ) - # Legacy null-creator threads are readable only by the search-space owner. + # Legacy null-creator threads are readable only by the workspace owner. include_legacy = False if user_uuid is not None: owner_id = await session.scalar( - select(SearchSpace.user_id).where(SearchSpace.id == search_space_id) + select(Workspace.user_id).where(Workspace.id == workspace_id) ) include_legacy = owner_id == user_uuid rows = await session.execute( select(NewChatThread).where( NewChatThread.id.in_(requested), - NewChatThread.search_space_id == search_space_id, + NewChatThread.workspace_id == workspace_id, _visibility_predicate(user_uuid, include_legacy=include_legacy), ) ) diff --git a/surfsense_backend/app/agents/chat/runtime/references/chat/resolver.py b/surfsense_backend/app/agents/chat/runtime/references/chat/resolver.py index 4e267dff3..3f1566d99 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/chat/resolver.py +++ b/surfsense_backend/app/agents/chat/runtime/references/chat/resolver.py @@ -16,7 +16,7 @@ from .access import accessible_threads async def resolve_chat_references( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, requesting_user_id: str | None, current_chat_id: int, thread_ids: list[int], @@ -27,7 +27,7 @@ async def resolve_chat_references( threads = await accessible_threads( session, - search_space_id=search_space_id, + workspace_id=workspace_id, requesting_user_id=requesting_user_id, thread_ids=thread_ids, exclude_thread_id=current_chat_id, diff --git a/surfsense_backend/app/agents/chat/runtime/references/connectors.py b/surfsense_backend/app/agents/chat/runtime/references/connectors.py index ae2df15c3..78f74c2eb 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/connectors.py +++ b/surfsense_backend/app/agents/chat/runtime/references/connectors.py @@ -29,13 +29,13 @@ def connector_pointer_fields( async def resolve_connector_references( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, connector_ids: list[int], chips: list[MentionedDocumentInfo] | None = None, ) -> list[ConnectorReference]: """Map ``@connector`` ids to references; ids outside the space are dropped. - The DB check only confirms the connector belongs to this search space; + The DB check only confirms the connector belongs to this workspace; display fields come from the user's chip. """ if not connector_ids: @@ -47,7 +47,7 @@ async def resolve_connector_references( SearchSourceConnector.name, SearchSourceConnector.connector_type, ).where( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.id.in_(connector_ids), ) ) diff --git a/surfsense_backend/app/agents/chat/runtime/references/documents/referenced.py b/surfsense_backend/app/agents/chat/runtime/references/documents/referenced.py index 4e05fd324..1e0745240 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/documents/referenced.py +++ b/surfsense_backend/app/agents/chat/runtime/references/documents/referenced.py @@ -3,7 +3,7 @@ Reference resolution, not retrieval: this answers "which knowledge-base documents did the user point at this turn?". ``@document`` ids pass through; ``@folder`` ids expand to the documents directly inside each folder within this -search space (direct children only, not nested subfolders). The caller turns the +workspace (direct children only, not nested subfolders). The caller turns the returned ids into a retrieval ``SearchScope``. """ @@ -18,7 +18,7 @@ from app.db import Document async def referenced_document_ids( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, document_ids: list[int] | None = None, folder_ids: list[int] | None = None, ) -> tuple[int, ...]: @@ -28,7 +28,7 @@ async def referenced_document_ids( if folders: rows = await session.execute( select(Document.id).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.folder_id.in_(folders), ) ) diff --git a/surfsense_backend/app/agents/chat/runtime/references/documents/resolver.py b/surfsense_backend/app/agents/chat/runtime/references/documents/resolver.py index 72a459eb9..b39a365e4 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/documents/resolver.py +++ b/surfsense_backend/app/agents/chat/runtime/references/documents/resolver.py @@ -14,13 +14,13 @@ from ..models import DocumentReference async def resolve_document_references( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, document_ids: list[int], index: PathIndex, ) -> list[DocumentReference]: """Map document ids to references in input order; unknown ids are dropped. - Best-effort and fail-closed: an id outside ``search_space_id`` (deleted or + Best-effort and fail-closed: an id outside ``workspace_id`` (deleted or foreign) simply does not produce a reference. """ if not document_ids: @@ -28,7 +28,7 @@ async def resolve_document_references( rows = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.id.in_(document_ids), ) ) diff --git a/surfsense_backend/app/agents/chat/runtime/references/folders.py b/surfsense_backend/app/agents/chat/runtime/references/folders.py index df0ec457b..57b3a5bfb 100644 --- a/surfsense_backend/app/agents/chat/runtime/references/folders.py +++ b/surfsense_backend/app/agents/chat/runtime/references/folders.py @@ -20,7 +20,7 @@ def folder_pointer_path(folder_id: int, folder_paths: dict[int, str]) -> str: async def resolve_folder_references( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, folder_ids: list[int], index: PathIndex, ) -> list[FolderReference]: @@ -30,7 +30,7 @@ async def resolve_folder_references( rows = await session.execute( select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.id.in_(folder_ids), ) ) diff --git a/surfsense_backend/app/agents/chat/shared/context.py b/surfsense_backend/app/agents/chat/shared/context.py index b543eb6b6..74cae70f5 100644 --- a/surfsense_backend/app/agents/chat/shared/context.py +++ b/surfsense_backend/app/agents/chat/shared/context.py @@ -41,7 +41,7 @@ class SurfSenseContextSchema: are optional; consumers must None-check before reading. Phase 1.5 fields: - search_space_id: Search space the request is scoped to. + workspace_id: Workspace the request is scoped to. mentioned_document_ids: KB documents the user @-mentioned this turn. Read by the ``search_knowledge_base`` tool to pin these docs into the retrieval scope. Stays out of the compiled-agent cache @@ -60,7 +60,7 @@ class SurfSenseContextSchema: by middleware ``__init__`` closures). """ - search_space_id: int | None = None + workspace_id: int | None = None mentioned_document_ids: list[int] = field(default_factory=list) mentioned_folder_ids: list[int] = field(default_factory=list) mentioned_connector_ids: list[int] = field(default_factory=list) diff --git a/surfsense_backend/app/agents/chat/shared/tools/web_search.py b/surfsense_backend/app/agents/chat/shared/tools/web_search.py index 424225b30..78f3c6ae2 100644 --- a/surfsense_backend/app/agents/chat/shared/tools/web_search.py +++ b/surfsense_backend/app/agents/chat/shared/tools/web_search.py @@ -115,7 +115,7 @@ def _to_renderable_web_documents( async def _search_live_connector( connector: str, query: str, - search_space_id: int, + workspace_id: int, top_k: int, semaphore: asyncio.Semaphore, ) -> list[dict[str, Any]]: @@ -128,7 +128,7 @@ async def _search_live_connector( method_name, _includes_date_range, includes_top_k, extra_kwargs = spec kwargs: dict[str, Any] = { "user_query": query, - "search_space_id": search_space_id, + "workspace_id": workspace_id, **extra_kwargs, } if includes_top_k: @@ -137,7 +137,7 @@ async def _search_live_connector( try: t0 = time.perf_counter() async with semaphore, shielded_async_session() as session: - svc = ConnectorService(session, search_space_id) + svc = ConnectorService(session, workspace_id) _, chunks = await getattr(svc, method_name)(**kwargs) perf.info( "[web_search] connector=%s results=%d in %.3fs", @@ -152,7 +152,7 @@ async def _search_live_connector( def create_web_search_tool( - search_space_id: int | None = None, + workspace_id: int | None = None, available_connectors: list[str] | None = None, ) -> BaseTool: """Factory for the ``web_search`` tool. @@ -178,7 +178,7 @@ def create_web_search_tool( "All configured engines are queried in parallel and results are merged." ) - _search_space_id = search_space_id + _workspace_id = workspace_id _active_live = active_live_connectors async def _web_search_impl( @@ -213,14 +213,14 @@ def create_web_search_tool( tasks.append(asyncio.ensure_future(_searxng())) - if _search_space_id is not None: + if _workspace_id is not None: for connector in _active_live: tasks.append( asyncio.ensure_future( _search_live_connector( connector=connector, query=query, - search_space_id=_search_space_id, + workspace_id=_workspace_id, top_k=clamped_top_k, semaphore=semaphore, ) diff --git a/surfsense_backend/app/agents/video_presentation/configuration.py b/surfsense_backend/app/agents/video_presentation/configuration.py index 18724a2ab..260e3f481 100644 --- a/surfsense_backend/app/agents/video_presentation/configuration.py +++ b/surfsense_backend/app/agents/video_presentation/configuration.py @@ -12,7 +12,7 @@ class Configuration: """The configuration for the video presentation agent.""" video_title: str - search_space_id: int + workspace_id: int user_prompt: str | None = None @classmethod diff --git a/surfsense_backend/app/agents/video_presentation/nodes.py b/surfsense_backend/app/agents/video_presentation/nodes.py index bdc0f80f8..dca89059f 100644 --- a/surfsense_backend/app/agents/video_presentation/nodes.py +++ b/surfsense_backend/app/agents/video_presentation/nodes.py @@ -49,12 +49,12 @@ async def create_presentation_slides( """Parse source content into structured presentation slides using LLM.""" configuration = Configuration.from_runnable_config(config) - search_space_id = configuration.search_space_id + workspace_id = configuration.workspace_id user_prompt = configuration.user_prompt - llm = await get_agent_llm(state.db_session, search_space_id) + llm = await get_agent_llm(state.db_session, workspace_id) if not llm: - error_message = f"No LLM configured for search space {search_space_id}" + error_message = f"No LLM configured for workspace {workspace_id}" print(error_message) raise RuntimeError(error_message) @@ -351,11 +351,11 @@ async def assign_slide_themes(state: State, config: RunnableConfig) -> dict[str, Runs in parallel with audio generation since it only needs slide metadata. """ configuration = Configuration.from_runnable_config(config) - search_space_id = configuration.search_space_id + workspace_id = configuration.workspace_id - llm = await get_agent_llm(state.db_session, search_space_id) + llm = await get_agent_llm(state.db_session, workspace_id) if not llm: - raise RuntimeError(f"No LLM configured for search space {search_space_id}") + raise RuntimeError(f"No LLM configured for workspace {workspace_id}") slides = state.slides or [] assignments = await _assign_themes_with_llm(llm, slides) @@ -372,11 +372,11 @@ async def generate_slide_scene_codes( """ configuration = Configuration.from_runnable_config(config) - search_space_id = configuration.search_space_id + workspace_id = configuration.workspace_id - llm = await get_agent_llm(state.db_session, search_space_id) + llm = await get_agent_llm(state.db_session, workspace_id) if not llm: - raise RuntimeError(f"No LLM configured for search space {search_space_id}") + raise RuntimeError(f"No LLM configured for workspace {workspace_id}") slides = state.slides or [] audio_results = state.slide_audio_results or [] diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index 1c81c8c29..179101085 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -465,7 +465,7 @@ async def _warm_agent_jit_caches() -> None: Doing one throwaway compile during ``lifespan`` startup pre-pays that cost so the *first real request* doesn't. We do NOT prime :mod:`agent_cache` because the cache key requires real - ``thread_id`` / ``user_id`` / ``search_space_id`` / etc. — the + ``thread_id`` / ``user_id`` / ``workspace_id`` / etc. — the throwaway agent is genuinely thrown away and immediately collected. Safety diff --git a/surfsense_backend/app/automations/actions/builtin/agent_task/dependencies.py b/surfsense_backend/app/automations/actions/builtin/agent_task/dependencies.py index c9584ae2a..fecf180c2 100644 --- a/surfsense_backend/app/automations/actions/builtin/agent_task/dependencies.py +++ b/surfsense_backend/app/automations/actions/builtin/agent_task/dependencies.py @@ -13,7 +13,7 @@ from app.automations.services.model_policy import ( assert_automation_models_billable, assert_models_billable, ) -from app.db import SearchSpace +from app.db import Workspace from app.tasks.chat.streaming.flows.shared.llm_bundle import load_llm_bundle from app.tasks.chat.streaming.flows.shared.pre_stream_setup import ( setup_connector_and_firecrawl, @@ -38,7 +38,7 @@ class AgentDependencies: async def build_dependencies( *, session: AsyncSession, - search_space_id: int, + workspace_id: int, chat_model_id: int | None = None, image_gen_model_id: int | None = None, vision_model_id: int | None = None, @@ -46,13 +46,13 @@ async def build_dependencies( """Load the LLM bundle, connector service, and a per-invoke in-memory checkpointer. Resolves the chat model from the automation's *captured* model snapshot - (``chat_model_id``) so runs are insulated from later chat/search-space model + (``chat_model_id``) so runs are insulated from later chat/workspace model changes. The model policy is enforced here as a runtime backstop: a captured model that is no longer billable (e.g. a premium global config was removed) fails the run clearly instead of silently consuming a free model. When ``chat_model_id`` is ``None`` (no captured snapshot — defensive fallback), - fall back to the live search space's ``chat_model_id`` and validate that. + fall back to the live workspace's ``chat_model_id`` and validate that. """ if chat_model_id is not None: try: @@ -65,25 +65,25 @@ async def build_dependencies( raise DependencyError(str(exc)) from exc resolved_chat_model_id = chat_model_id or 0 else: - search_space = await session.get(SearchSpace, search_space_id) - if search_space is None: - raise DependencyError(f"search space {search_space_id} not found") + workspace = await session.get(Workspace, workspace_id) + if workspace is None: + raise DependencyError(f"workspace {workspace_id} not found") try: - assert_automation_models_billable(search_space) + assert_automation_models_billable(workspace) except AutomationModelPolicyError as exc: raise DependencyError(str(exc)) from exc - resolved_chat_model_id = search_space.chat_model_id or 0 + resolved_chat_model_id = workspace.chat_model_id or 0 llm, agent_config, err = await load_llm_bundle( session, config_id=resolved_chat_model_id, - search_space_id=search_space_id, + workspace_id=workspace_id, ) if err is not None or llm is None: raise DependencyError(err or "failed to load chat model config") connector_service, firecrawl_api_key = await setup_connector_and_firecrawl( - session, search_space_id=search_space_id + session, workspace_id=workspace_id ) # Per-task InMemorySaver: the shared Postgres checkpointer's connection # pool binds connections to the loop that opened them, but Celery uses a diff --git a/surfsense_backend/app/automations/actions/builtin/agent_task/invoke.py b/surfsense_backend/app/automations/actions/builtin/agent_task/invoke.py index e1ba32ce9..91ebd962e 100644 --- a/surfsense_backend/app/automations/actions/builtin/agent_task/invoke.py +++ b/surfsense_backend/app/automations/actions/builtin/agent_task/invoke.py @@ -65,7 +65,7 @@ def _build_connector_block(connectors: list[dict[str, Any]]) -> str | None: async def _resolve_mention_context( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, query: str, mentioned_document_ids: list[int] | None, mentioned_folder_ids: list[int] | None, @@ -94,7 +94,7 @@ async def _resolve_mention_context( resolved = await resolve_mentions( session, - search_space_id=search_space_id, + workspace_id=workspace_id, mentioned_documents=mentioned_documents, mentioned_document_ids=mentioned_document_ids, mentioned_folder_ids=mentioned_folder_ids, @@ -109,7 +109,7 @@ async def _resolve_mention_context( agent_query = f"{connector_block}\n\n{agent_query}" runtime_context = SurfSenseContextSchema( - search_space_id=search_space_id, + workspace_id=workspace_id, mentioned_document_ids=list( resolved.mentioned_document_ids or (mentioned_document_ids or []) ), @@ -156,7 +156,7 @@ async def run_agent_task( deps = await build_dependencies( session=agent_session, - search_space_id=ctx.search_space_id, + workspace_id=ctx.workspace_id, chat_model_id=ctx.chat_model_id, image_gen_model_id=ctx.image_gen_model_id, vision_model_id=ctx.vision_model_id, @@ -164,7 +164,7 @@ async def run_agent_task( agent = await create_multi_agent_chat_deep_agent( llm=deps.llm, - search_space_id=ctx.search_space_id, + workspace_id=ctx.workspace_id, db_session=agent_session, connector_service=deps.connector_service, checkpointer=deps.checkpointer, @@ -180,7 +180,7 @@ async def run_agent_task( agent_query, runtime_context = await _resolve_mention_context( agent_session, - search_space_id=ctx.search_space_id, + workspace_id=ctx.workspace_id, query=query, mentioned_document_ids=mentioned_document_ids, mentioned_folder_ids=mentioned_folder_ids, @@ -193,7 +193,7 @@ async def run_agent_task( turn_id = f"{request_id}:{int(time.time() * 1000)}" input_state: dict[str, Any] = { "messages": [HumanMessage(content=agent_query)], - "search_space_id": ctx.search_space_id, + "workspace_id": ctx.workspace_id, "request_id": request_id, "turn_id": turn_id, } diff --git a/surfsense_backend/app/automations/actions/types.py b/surfsense_backend/app/automations/actions/types.py index 3ee427512..0273559f3 100644 --- a/surfsense_backend/app/automations/actions/types.py +++ b/surfsense_backend/app/automations/actions/types.py @@ -18,11 +18,11 @@ class ActionContext: session: AsyncSession run_id: int step_id: str - search_space_id: int + workspace_id: int creator_user_id: UUID | None # Captured model snapshot from the automation definition (``definition.models``), - # resolved per run instead of the live search space. ``None`` falls back to the - # search space's current prefs (defensive; should not happen post-capture). + # resolved per run instead of the live workspace. ``None`` falls back to the + # workspace's current prefs (defensive; should not happen post-capture). chat_model_id: int | None = None image_gen_model_id: int | None = None vision_model_id: int | None = None diff --git a/surfsense_backend/app/automations/api/automation.py b/surfsense_backend/app/automations/api/automation.py index 911ae57a6..59f05a7bb 100644 --- a/surfsense_backend/app/automations/api/automation.py +++ b/surfsense_backend/app/automations/api/automation.py @@ -44,14 +44,14 @@ async def create_automation( @router.get("/automations", response_model=AutomationList) async def list_automations( - search_space_id: int = Query(...), + workspace_id: int = Query(...), limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), service: AutomationService = Depends(get_automation_service), ) -> AutomationList: - """List automations in a search space.""" + """List automations in a workspace.""" items, total = await service.list( - search_space_id=search_space_id, limit=limit, offset=offset + workspace_id=workspace_id, limit=limit, offset=offset ) return AutomationList( items=[AutomationSummary.model_validate(a) for a in items], @@ -61,10 +61,10 @@ async def list_automations( @router.get("/automations/model-eligibility", response_model=ModelEligibility) async def get_automation_model_eligibility( - search_space_id: int = Query(...), + workspace_id: int = Query(...), service: AutomationService = Depends(get_automation_service), ) -> ModelEligibility: - """Report whether a search space's models are billable for automations. + """Report whether a workspace's models are billable for automations. Used by the frontend to gate creation: automations may only use premium global models or user BYOK models (free models and Auto mode are blocked). @@ -72,7 +72,7 @@ async def get_automation_model_eligibility( NOTE: declared before ``/automations/{automation_id}`` so the literal path isn't captured by the int-typed ``{automation_id}`` route. """ - result = await service.model_eligibility(search_space_id=search_space_id) + result = await service.model_eligibility(workspace_id=workspace_id) return ModelEligibility.model_validate(result) diff --git a/surfsense_backend/app/automations/runtime/executor.py b/surfsense_backend/app/automations/runtime/executor.py index bcdab3940..d9544cd0c 100644 --- a/surfsense_backend/app/automations/runtime/executor.py +++ b/surfsense_backend/app/automations/runtime/executor.py @@ -108,7 +108,7 @@ def _build_template_ctx( automation_id=run.automation_id, automation_name=automation.name if automation else None, automation_version=automation.version if automation else None, - search_space_id=automation.search_space_id if automation else None, + workspace_id=automation.workspace_id if automation else None, creator_id=automation.created_by_user_id if automation else None, trigger_id=run.trigger_id, trigger_type=trigger.type.value if trigger else None, @@ -130,7 +130,7 @@ def _build_action_ctx( session=session, run_id=run.id, step_id=step.step_id, - search_space_id=automation.search_space_id, + workspace_id=automation.workspace_id, creator_user_id=automation.created_by_user_id, chat_model_id=models.chat_model_id if models else None, image_gen_model_id=models.image_gen_model_id if models else None, diff --git a/surfsense_backend/app/automations/schemas/api/automation.py b/surfsense_backend/app/automations/schemas/api/automation.py index c1defd417..d3dbce1e0 100644 --- a/surfsense_backend/app/automations/schemas/api/automation.py +++ b/surfsense_backend/app/automations/schemas/api/automation.py @@ -17,7 +17,7 @@ class AutomationCreate(BaseModel): model_config = ConfigDict(extra="forbid") - search_space_id: int + workspace_id: int name: str = Field(..., min_length=1, max_length=200) description: str | None = None definition: AutomationDefinition @@ -41,7 +41,7 @@ class AutomationSummary(BaseModel): model_config = ConfigDict(from_attributes=True) id: int - search_space_id: int + workspace_id: int name: str description: str | None = None status: AutomationStatus diff --git a/surfsense_backend/app/automations/schemas/definition/envelope.py b/surfsense_backend/app/automations/schemas/definition/envelope.py index 787534d4a..0a59ff168 100644 --- a/surfsense_backend/app/automations/schemas/definition/envelope.py +++ b/surfsense_backend/app/automations/schemas/definition/envelope.py @@ -14,8 +14,8 @@ from .trigger_spec import TriggerSpec class AutomationModels(BaseModel): """Captured model profile for an automation. - Snapshotted from the search space's model roles at create time so runs are - insulated from later chat/search-space model changes. Model-id conventions + Snapshotted from the workspace's model roles at create time so runs are + insulated from later chat/workspace model changes. Model-id conventions match the shared scheme (``0`` Auto, ``< 0`` global, ``> 0`` BYOK). """ @@ -40,6 +40,6 @@ class AutomationDefinition(BaseModel): execution: Execution = Field(default_factory=Execution) metadata: Metadata = Field(default_factory=Metadata) # Captured server-side at create() and preserved across update(); resolved - # at runtime instead of the live search space. Optional so drafts/builder + # at runtime instead of the live workspace. Optional so drafts/builder # payloads validate without it. models: AutomationModels | None = None diff --git a/surfsense_backend/app/automations/services/automation.py b/surfsense_backend/app/automations/services/automation.py index ed748fb7c..eeb5b61bd 100644 --- a/surfsense_backend/app/automations/services/automation.py +++ b/surfsense_backend/app/automations/services/automation.py @@ -28,7 +28,7 @@ from app.automations.services.model_policy import ( ) from app.automations.triggers import get_trigger from app.automations.triggers.builtin.schedule import compute_next_fire_at -from app.db import Permission, SearchSpace, get_async_session +from app.db import Permission, Workspace, get_async_session from app.users import get_auth_context from app.utils.rbac import check_permission @@ -44,28 +44,28 @@ class AutomationService: async def create(self, payload: AutomationCreate) -> Automation: """Create an automation and its initial triggers in one transaction.""" await self._authorize( - payload.search_space_id, Permission.AUTOMATIONS_CREATE.value + payload.workspace_id, Permission.AUTOMATIONS_CREATE.value ) # Capture the model profile onto the definition so runs are insulated - # from later chat/search-space model changes. Two sources: + # from later chat/workspace model changes. Two sources: # 1. Explicit per-automation selection in ``payload.definition.models`` # (manual builder + chat approval card). Validate the chosen ids. - # 2. Fallback (no selection): snapshot the search space's current prefs. + # 2. Fallback (no selection): snapshot the workspace's current prefs. # Either way the captured ids are guaranteed billable (premium/BYOK). selected_models = payload.definition.models if selected_models is not None: self._assert_selected_models_billable(selected_models) else: - search_space = await self._assert_models_billable(payload.search_space_id) + workspace = await self._assert_models_billable(payload.workspace_id) payload.definition.models = AutomationModels( - chat_model_id=search_space.chat_model_id or 0, - image_gen_model_id=search_space.image_gen_model_id or 0, - vision_model_id=search_space.vision_model_id or 0, + chat_model_id=workspace.chat_model_id or 0, + image_gen_model_id=workspace.image_gen_model_id or 0, + vision_model_id=workspace.vision_model_id or 0, ) automation = Automation( - search_space_id=payload.search_space_id, + workspace_id=payload.workspace_id, created_by_user_id=self.user.id, name=payload.name, description=payload.description, @@ -82,14 +82,14 @@ class AutomationService: async def list( self, *, - search_space_id: int, + workspace_id: int, limit: int, offset: int, ) -> tuple[list[Automation], int]: """Return a page of automations and the total count.""" - await self._authorize(search_space_id, Permission.AUTOMATIONS_READ.value) + await self._authorize(workspace_id, Permission.AUTOMATIONS_READ.value) - base = select(Automation).where(Automation.search_space_id == search_space_id) + base = select(Automation).where(Automation.workspace_id == workspace_id) total = await self.session.scalar( select(func.count()).select_from(base.subquery()) ) @@ -111,7 +111,7 @@ class AutomationService: """Get an automation with its triggers loaded.""" automation = await self._get_with_triggers_or_raise(automation_id) await self._authorize( - automation.search_space_id, Permission.AUTOMATIONS_READ.value + automation.workspace_id, Permission.AUTOMATIONS_READ.value ) return automation @@ -119,7 +119,7 @@ class AutomationService: """Patch fields. Bumps ``version`` when ``definition`` changes.""" automation = await self._get_with_triggers_or_raise(automation_id) await self._authorize( - automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value + automation.workspace_id, Permission.AUTOMATIONS_UPDATE.value ) data = patch.model_dump(exclude_unset=True) @@ -135,7 +135,7 @@ class AutomationService: # Model snapshot handling on edit: # * absent in the patch -> preserve the captured snapshot # (a non-model definition change never silently re-binds the - # automation to the current chat/search-space selection). + # automation to the current chat/workspace selection). # * unchanged from the snapshot -> keep as-is, no re-validation # (so editing an automation whose captured model later drifted # out of premium isn't blocked by an unrelated name/schedule edit). @@ -158,7 +158,7 @@ class AutomationService: """Delete an automation; FK cascades remove triggers and runs.""" automation = await self._get_or_raise(automation_id) await self._authorize( - automation.search_space_id, Permission.AUTOMATIONS_DELETE.value + automation.workspace_id, Permission.AUTOMATIONS_DELETE.value ) await self.session.delete(automation) await self.session.commit() @@ -184,46 +184,46 @@ class AutomationService: ) return automation - async def model_eligibility(self, *, search_space_id: int) -> dict: - """Return whether a search space's models are billable for automations. + async def model_eligibility(self, *, workspace_id: int) -> dict: + """Return whether a workspace's models are billable for automations. ``{"allowed": bool, "violations": [{kind, config_id, reason}, ...]}``. """ - await self._authorize(search_space_id, Permission.AUTOMATIONS_READ.value) - search_space = await self.session.get(SearchSpace, search_space_id) - if search_space is None: + await self._authorize(workspace_id, Permission.AUTOMATIONS_READ.value) + workspace = await self.session.get(Workspace, workspace_id) + if workspace is None: raise HTTPException( - status_code=404, detail=f"search space {search_space_id} not found" + status_code=404, detail=f"workspace {workspace_id} not found" ) - return get_automation_model_eligibility(search_space) + return get_automation_model_eligibility(workspace) - async def _assert_models_billable(self, search_space_id: int) -> SearchSpace: - """Reject creation when the search space's models aren't billable. + async def _assert_models_billable(self, workspace_id: int) -> Workspace: + """Reject creation when the workspace's models aren't billable. Automations may only use premium global models or user BYOK models; free global models and Auto mode are blocked. Mirrors the runtime backstop in ``agent_task`` so users can't save an automation that would fail to run. - Returns the loaded :class:`SearchSpace` so the caller can capture its + Returns the loaded :class:`Workspace` so the caller can capture its model prefs without a second DB read. """ - search_space = await self.session.get(SearchSpace, search_space_id) - if search_space is None: + workspace = await self.session.get(Workspace, workspace_id) + if workspace is None: raise HTTPException( - status_code=404, detail=f"search space {search_space_id} not found" + status_code=404, detail=f"workspace {workspace_id} not found" ) try: - assert_automation_models_billable(search_space) + assert_automation_models_billable(workspace) except AutomationModelPolicyError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc - return search_space + return workspace def _assert_selected_models_billable(self, models: AutomationModels) -> None: """Reject creation when an explicitly selected model isn't billable. Used when the client supplies ``definition.models`` (per-automation selection from the builder or chat approval card). Same policy as the - search-space path: premium global or BYOK only, no free/Auto. + workspace path: premium global or BYOK only, no free/Auto. """ try: assert_models_billable( @@ -234,13 +234,13 @@ class AutomationService: except AutomationModelPolicyError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc - async def _authorize(self, search_space_id: int, permission: str) -> None: + async def _authorize(self, workspace_id: int, permission: str) -> None: await check_permission( self.session, self.auth, - search_space_id, + workspace_id, permission, - f"You don't have permission to {permission.split(':')[1]} automations in this search space", + f"You don't have permission to {permission.split(':')[1]} automations in this workspace", ) diff --git a/surfsense_backend/app/automations/services/model_policy.py b/surfsense_backend/app/automations/services/model_policy.py index e18264246..e767e55d1 100644 --- a/surfsense_backend/app/automations/services/model_policy.py +++ b/surfsense_backend/app/automations/services/model_policy.py @@ -22,7 +22,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: - from app.db import SearchSpace + from app.db import Workspace ModelKind = Literal["chat", "image", "vision"] @@ -58,7 +58,7 @@ def _classify(kind: ModelKind, model_id: int | None) -> tuple[bool, str]: ) if model_id > 0: - # Positive id -> user/search-space BYOK model. Always allowed. + # Positive id -> user/workspace BYOK model. Always allowed. return True, "" # Negative id -> global model. Allowed only if premium. @@ -80,7 +80,7 @@ def get_model_eligibility( ) -> dict: """Return ``{"allowed": bool, "violations": [...]}`` for explicit model ids. - The ID-based core shared by both the search-space path (creation/eligibility) + The ID-based core shared by both the workspace path (creation/eligibility) and the captured-snapshot path (runtime backstop). Each violation is ``{"kind", "model_id", "reason"}``. """ @@ -99,21 +99,21 @@ def get_model_eligibility( return {"allowed": not violations, "violations": violations} -def get_automation_model_eligibility(search_space: SearchSpace) -> dict: - """Return ``{"allowed": bool, "violations": [...]}`` for a search space. +def get_automation_model_eligibility(workspace: Workspace) -> dict: + """Return ``{"allowed": bool, "violations": [...]}`` for a workspace. Used by the eligibility endpoint and the chat tool's early check. Thin wrapper over :func:`get_model_eligibility`. """ return get_model_eligibility( - chat_model_id=search_space.chat_model_id, - image_gen_model_id=search_space.image_gen_model_id, - vision_model_id=search_space.vision_model_id, + chat_model_id=workspace.chat_model_id, + image_gen_model_id=workspace.image_gen_model_id, + vision_model_id=workspace.vision_model_id, ) class AutomationModelPolicyError(Exception): - """Raised when a search space's models are not billable for automations.""" + """Raised when a workspace's models are not billable for automations.""" def __init__(self, violations: list[dict]) -> None: self.violations = violations @@ -143,8 +143,8 @@ def assert_models_billable( raise AutomationModelPolicyError(result["violations"]) -def assert_automation_models_billable(search_space: SearchSpace) -> None: +def assert_automation_models_billable(workspace: Workspace) -> None: """Raise :class:`AutomationModelPolicyError` if any model slot is not billable.""" - result = get_automation_model_eligibility(search_space) + result = get_automation_model_eligibility(workspace) if not result["allowed"]: raise AutomationModelPolicyError(result["violations"]) diff --git a/surfsense_backend/app/automations/services/run.py b/surfsense_backend/app/automations/services/run.py index 9bcd1393e..1e130b00d 100644 --- a/surfsense_backend/app/automations/services/run.py +++ b/surfsense_backend/app/automations/services/run.py @@ -65,9 +65,9 @@ class RunService: await check_permission( self.session, self.auth, - automation.search_space_id, + automation.workspace_id, permission, - f"You don't have permission to {permission.split(':')[1]} automations in this search space", + f"You don't have permission to {permission.split(':')[1]} automations in this workspace", ) return automation diff --git a/surfsense_backend/app/automations/services/trigger.py b/surfsense_backend/app/automations/services/trigger.py index 52c827c67..5175eb15e 100644 --- a/surfsense_backend/app/automations/services/trigger.py +++ b/surfsense_backend/app/automations/services/trigger.py @@ -103,9 +103,9 @@ class TriggerService: await check_permission( self.session, self.auth, - automation.search_space_id, + automation.workspace_id, permission, - f"You don't have permission to {permission.split(':')[1]} automations in this search space", + f"You don't have permission to {permission.split(':')[1]} automations in this workspace", ) return automation diff --git a/surfsense_backend/app/automations/templating/context.py b/surfsense_backend/app/automations/templating/context.py index 96fdb02e9..654311610 100644 --- a/surfsense_backend/app/automations/templating/context.py +++ b/surfsense_backend/app/automations/templating/context.py @@ -13,7 +13,7 @@ def build_run_context( automation_id: int, automation_name: str | None, automation_version: int | None, - search_space_id: int | None, + workspace_id: int | None, creator_id: Any, trigger_id: int | None, trigger_type: str | None, @@ -29,7 +29,7 @@ def build_run_context( "automation_id": automation_id, "automation_name": automation_name, "automation_version": automation_version, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "creator_id": creator_id, "trigger_id": trigger_id, "trigger_type": trigger_type, diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 47e529741..c4ee33c30 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -839,8 +839,8 @@ class Config: # Legacy environment variables removed in favor of user-specific configurations # True when an operator-provided global_llm_config.yaml is present. - # Used to gate the per-search-space LLM onboarding flow: when a global - # config file exists, search spaces inherit it and onboarding is skipped. + # Used to gate the per-workspace LLM onboarding flow: when a global + # config file exists, workspaces inherit it and onboarding is skipped. GLOBAL_LLM_CONFIG_FILE_EXISTS = ( BASE_DIR / "app" / "config" / "global_llm_config.yaml" ).exists() diff --git a/surfsense_backend/app/connectors/google_drive/content_extractor.py b/surfsense_backend/app/connectors/google_drive/content_extractor.py index 1ea047978..9df7640eb 100644 --- a/surfsense_backend/app/connectors/google_drive/content_extractor.py +++ b/surfsense_backend/app/connectors/google_drive/content_extractor.py @@ -136,7 +136,7 @@ async def _parse_file_to_markdown( async def download_and_process_file( client: GoogleDriveClient, file: dict[str, Any], - search_space_id: int, + workspace_id: int, user_id: str, session: AsyncSession, task_logger: TaskLoggingService, @@ -149,7 +149,7 @@ async def download_and_process_file( Args: client: GoogleDriveClient instance file: File metadata from Drive API - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user session: Database session task_logger: Task logging service @@ -246,7 +246,7 @@ async def download_and_process_file( await process_file_in_background( file_path=temp_file_path, filename=etl_filename, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, session=session, task_logger=task_logger, diff --git a/surfsense_backend/app/event_bus/__init__.py b/surfsense_backend/app/event_bus/__init__.py index da5735fe6..8cee59125 100644 --- a/surfsense_backend/app/event_bus/__init__.py +++ b/surfsense_backend/app/event_bus/__init__.py @@ -4,7 +4,7 @@ Domain-agnostic pub/sub. Producers ``await bus.publish(...)``; subscribers ``bus.subscribe(...)``. Domain modules depend on it, never the reverse. from app.event_bus import bus - await bus.publish("document.indexed", {"document_id": 42}, search_space_id=7) + await bus.publish("document.indexed", {"document_id": 42}, workspace_id=7) """ from __future__ import annotations diff --git a/surfsense_backend/app/event_bus/bus.py b/surfsense_backend/app/event_bus/bus.py index 38c93ba7c..7d89e9d4c 100644 --- a/surfsense_backend/app/event_bus/bus.py +++ b/surfsense_backend/app/event_bus/bus.py @@ -40,13 +40,13 @@ class EventBus: event_type: str, payload: dict[str, Any] | None = None, *, - search_space_id: int, + workspace_id: int, ) -> None: """Stamp an :class:`Event` and fan it out. Call after your commit.""" event = Event( event_type=event_type, payload=payload or {}, - search_space_id=search_space_id, + workspace_id=workspace_id, ) await self.dispatch(event) diff --git a/surfsense_backend/app/event_bus/event.py b/surfsense_backend/app/event_bus/event.py index 5dc3f7081..4bc346d09 100644 --- a/surfsense_backend/app/event_bus/event.py +++ b/surfsense_backend/app/event_bus/event.py @@ -25,7 +25,7 @@ class Event(BaseModel): """A published domain fact. ``event_type`` is a dotted namespace (``document.indexed``, etc). ``payload`` is - JSON-serializable. ``search_space_id`` scopes delivery. ``event_id`` and + JSON-serializable. ``workspace_id`` scopes delivery. ``event_id`` and ``occurred_at`` are engine-stamped. """ @@ -33,6 +33,6 @@ class Event(BaseModel): event_type: str payload: dict[str, Any] = Field(default_factory=dict) - search_space_id: int + workspace_id: int event_id: str = Field(default_factory=_new_event_id) occurred_at: datetime = Field(default_factory=_now) diff --git a/surfsense_backend/app/event_bus/events/document_entered_folder.py b/surfsense_backend/app/event_bus/events/document_entered_folder.py index fc4e2de14..66560e9e5 100644 --- a/surfsense_backend/app/event_bus/events/document_entered_folder.py +++ b/surfsense_backend/app/event_bus/events/document_entered_folder.py @@ -48,7 +48,7 @@ catalog.register( def payload_if_entered_folder( *, document_id: int, - search_space_id: int, + workspace_id: int, new_folder_id: int | None, previous_folder_id: int | None, folder_id_changed: bool, @@ -73,7 +73,7 @@ def payload_if_entered_folder( return { "event_type": EVENT_TYPE, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "payload": { "document_id": document_id, "folder_id": new_folder_id, diff --git a/surfsense_backend/app/file_storage/api.py b/surfsense_backend/app/file_storage/api.py index 80417baaf..50a931d7e 100644 --- a/surfsense_backend/app/file_storage/api.py +++ b/surfsense_backend/app/file_storage/api.py @@ -37,9 +37,9 @@ async def _load_readable_document( await check_permission( session, auth, - document.search_space_id, + document.workspace_id, Permission.DOCUMENTS_READ.value, - "You don't have permission to read documents in this search space", + "You don't have permission to read documents in this workspace", ) return document diff --git a/surfsense_backend/app/file_storage/keys.py b/surfsense_backend/app/file_storage/keys.py index 22eaa9473..8438cf42e 100644 --- a/surfsense_backend/app/file_storage/keys.py +++ b/surfsense_backend/app/file_storage/keys.py @@ -10,18 +10,18 @@ from app.file_storage.persistence.enums import DocumentFileKind def build_document_file_key( *, - search_space_id: int, + workspace_id: int, document_id: int, kind: DocumentFileKind, filename: str, ) -> str: """Build the storage key for one document file. - Shape: ``documents/{search_space_id}/{document_id}/{kind}/{uuid}{ext}``. + Shape: ``documents/{workspace_id}/{document_id}/{kind}/{uuid}{ext}``. """ extension = os.path.splitext(filename)[1].lower() unique = uuid.uuid4().hex return ( - f"documents/{search_space_id}/{document_id}/" + f"documents/{workspace_id}/{document_id}/" f"{kind.value.lower()}/{unique}{extension}" ) diff --git a/surfsense_backend/app/file_storage/service.py b/surfsense_backend/app/file_storage/service.py index bdadcfca3..472bef89f 100644 --- a/surfsense_backend/app/file_storage/service.py +++ b/surfsense_backend/app/file_storage/service.py @@ -27,7 +27,7 @@ async def store_document_file( session: AsyncSession, *, document_id: int, - search_space_id: int, + workspace_id: int, data: bytes, filename: str, mime_type: str | None = None, @@ -38,7 +38,7 @@ async def store_document_file( """Write bytes to storage and add a ``DocumentFile`` row to the session.""" backend = backend or get_storage_backend() key = build_document_file_key( - search_space_id=search_space_id, + workspace_id=workspace_id, document_id=document_id, kind=kind, filename=filename, @@ -47,7 +47,7 @@ async def store_document_file( record = DocumentFile( document_id=document_id, - search_space_id=search_space_id, + workspace_id=workspace_id, kind=kind, storage_backend=backend.backend_name, storage_key=key, diff --git a/surfsense_backend/app/gateway/agent_invoke.py b/surfsense_backend/app/gateway/agent_invoke.py index e03ea8c8b..8a402a665 100644 --- a/surfsense_backend/app/gateway/agent_invoke.py +++ b/surfsense_backend/app/gateway/agent_invoke.py @@ -75,7 +75,7 @@ async def call_agent_for_gateway( try: stream = stream_new_chat( user_query=user_text, - search_space_id=binding.search_space_id, + workspace_id=binding.workspace_id, chat_id=thread.id, user_id=str(user.id), needs_history_bootstrap=thread.needs_history_bootstrap, diff --git a/surfsense_backend/app/gateway/auth_invariant.py b/surfsense_backend/app/gateway/auth_invariant.py index 008250957..72c7ddf61 100644 --- a/surfsense_backend/app/gateway/auth_invariant.py +++ b/surfsense_backend/app/gateway/auth_invariant.py @@ -9,7 +9,7 @@ from app.auth.context import AuthContext from app.db import ExternalChatBinding, Permission, User from app.gateway.bindings import suspend_binding from app.observability.metrics import record_gateway_auth_invariant_failure -from app.utils.rbac import check_permission, check_search_space_access +from app.utils.rbac import check_permission, check_workspace_access class GatewaySuspendedError(RuntimeError): @@ -43,13 +43,13 @@ async def assert_authorization_invariant( auth = AuthContext.system(user, source="gateway") try: - await check_search_space_access(session, auth, binding.search_space_id) + await check_workspace_access(session, auth, binding.workspace_id) await check_permission( session, auth, - binding.search_space_id, + binding.workspace_id, Permission.CHATS_CREATE.value, - "External chat owner no longer has permission to chat in this search space", + "External chat owner no longer has permission to chat in this workspace", ) except HTTPException as exc: await _fail(session, binding, f"rbac_{exc.status_code}") diff --git a/surfsense_backend/app/gateway/bindings.py b/surfsense_backend/app/gateway/bindings.py index 821dd21ca..8feefd510 100644 --- a/surfsense_backend/app/gateway/bindings.py +++ b/surfsense_backend/app/gateway/bindings.py @@ -34,7 +34,7 @@ async def get_or_create_thread_for_binding( thread = NewChatThread( title=f"{source.title()} chat", - search_space_id=binding.search_space_id, + workspace_id=binding.workspace_id, created_by_id=binding.user_id, visibility=ChatVisibility.PRIVATE, source=source, diff --git a/surfsense_backend/app/gateway/inbox_processor.py b/surfsense_backend/app/gateway/inbox_processor.py index 7343b3a03..34c35f9d6 100644 --- a/surfsense_backend/app/gateway/inbox_processor.py +++ b/surfsense_backend/app/gateway/inbox_processor.py @@ -210,7 +210,7 @@ async def _resolve_slack_thread_binding( thread_binding = ExternalChatBinding( account_id=account.id, user_id=user_binding.user_id, - search_space_id=user_binding.search_space_id, + workspace_id=user_binding.workspace_id, state=ExternalChatBindingState.BOUND, external_peer_id=thread_peer_id, external_peer_kind=ExternalChatPeerKind.CHANNEL, @@ -272,7 +272,7 @@ async def _resolve_discord_thread_binding( thread_binding = ExternalChatBinding( account_id=account.id, user_id=user_binding.user_id, - search_space_id=user_binding.search_space_id, + workspace_id=user_binding.workspace_id, state=ExternalChatBindingState.BOUND, external_peer_id=thread_peer_id, external_peer_kind=ExternalChatPeerKind.CHANNEL, @@ -366,12 +366,12 @@ async def _dispatch_inbound_event( if ( bundle.auto_bind_owner and account.owner_user_id - and account.owner_search_space_id + and account.owner_workspace_id ): binding = ExternalChatBinding( account_id=account.id, user_id=account.owner_user_id, - search_space_id=account.owner_search_space_id, + workspace_id=account.owner_workspace_id, state=ExternalChatBindingState.BOUND, external_peer_id=parsed.external_peer_id, external_peer_kind=parsed.external_peer_kind, diff --git a/surfsense_backend/app/indexing_pipeline/adapters/file_upload_adapter.py b/surfsense_backend/app/indexing_pipeline/adapters/file_upload_adapter.py index 9a9e4e4d6..27378324b 100644 --- a/surfsense_backend/app/indexing_pipeline/adapters/file_upload_adapter.py +++ b/surfsense_backend/app/indexing_pipeline/adapters/file_upload_adapter.py @@ -16,7 +16,7 @@ class UploadDocumentAdapter: markdown_content: str, filename: str, etl_service: str, - search_space_id: int, + workspace_id: int, user_id: str, ) -> None: connector_doc = ConnectorDocument( @@ -24,7 +24,7 @@ class UploadDocumentAdapter: source_markdown=markdown_content, unique_id=filename, document_type=DocumentType.FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, connector_id=None, should_use_code_chunker=False, @@ -59,7 +59,7 @@ class UploadDocumentAdapter: source_markdown=document.source_markdown, unique_id=document.title, document_type=document.document_type, - search_space_id=document.search_space_id, + workspace_id=document.workspace_id, created_by_id=str(document.created_by_id), connector_id=document.connector_id, should_use_code_chunker=False, diff --git a/surfsense_backend/app/indexing_pipeline/connector_document.py b/surfsense_backend/app/indexing_pipeline/connector_document.py index 1297a6b46..08b1d254d 100644 --- a/surfsense_backend/app/indexing_pipeline/connector_document.py +++ b/surfsense_backend/app/indexing_pipeline/connector_document.py @@ -10,7 +10,7 @@ class ConnectorDocument(BaseModel): source_markdown: str unique_id: str document_type: DocumentType - search_space_id: int = Field(gt=0) + workspace_id: int = Field(gt=0) should_use_code_chunker: bool = False metadata: dict = {} connector_id: int | None = None diff --git a/surfsense_backend/app/indexing_pipeline/document_hashing.py b/surfsense_backend/app/indexing_pipeline/document_hashing.py index a0716f401..2f9485107 100644 --- a/surfsense_backend/app/indexing_pipeline/document_hashing.py +++ b/surfsense_backend/app/indexing_pipeline/document_hashing.py @@ -4,21 +4,21 @@ from app.indexing_pipeline.connector_document import ConnectorDocument def compute_identifier_hash( - document_type_value: str, unique_id: str, search_space_id: int + document_type_value: str, unique_id: str, workspace_id: int ) -> str: """Return a stable SHA-256 hash from raw identity components.""" - combined = f"{document_type_value}:{unique_id}:{search_space_id}" + combined = f"{document_type_value}:{unique_id}:{workspace_id}" return hashlib.sha256(combined.encode("utf-8")).hexdigest() def compute_unique_identifier_hash(doc: ConnectorDocument) -> str: """Return a stable SHA-256 hash identifying a document by its source identity.""" return compute_identifier_hash( - doc.document_type.value, doc.unique_id, doc.search_space_id + doc.document_type.value, doc.unique_id, doc.workspace_id ) def compute_content_hash(doc: ConnectorDocument) -> str: - """Return a SHA-256 hash of the document's content scoped to its search space.""" - combined = f"{doc.search_space_id}:{doc.source_markdown}" + """Return a SHA-256 hash of the document's content scoped to its workspace.""" + combined = f"{doc.workspace_id}:{doc.source_markdown}" return hashlib.sha256(combined.encode("utf-8")).hexdigest() diff --git a/surfsense_backend/app/indexing_pipeline/indexing_pipeline_service.py b/surfsense_backend/app/indexing_pipeline/indexing_pipeline_service.py index 30ea9d5d6..a9f2dd757 100644 --- a/surfsense_backend/app/indexing_pipeline/indexing_pipeline_service.py +++ b/surfsense_backend/app/indexing_pipeline/indexing_pipeline_service.py @@ -73,7 +73,7 @@ class PlaceholderInfo: title: str document_type: DocumentType unique_id: str - search_space_id: int + workspace_id: int connector_id: int | None created_by_id: str metadata: dict = field(default_factory=dict) @@ -111,7 +111,7 @@ class IndexingPipelineService: for p in placeholders: try: uid_hash = compute_identifier_hash( - p.document_type.value, p.unique_id, p.search_space_id + p.document_type.value, p.unique_id, p.workspace_id ) uid_hashes.setdefault(uid_hash, p) except Exception: @@ -145,7 +145,7 @@ class IndexingPipelineService: content_hash=content_hash, unique_identifier_hash=uid_hash, document_metadata=p.metadata or {}, - search_space_id=p.search_space_id, + workspace_id=p.workspace_id, connector_id=p.connector_id, created_by_id=p.created_by_id, updated_at=datetime.now(UTC), @@ -186,7 +186,7 @@ class IndexingPipelineService: continue legacy_hash = compute_identifier_hash( - legacy_type, doc.unique_id, doc.search_space_id + legacy_type, doc.unique_id, doc.workspace_id ) result = await self.session.execute( select(Document).filter(Document.unique_identifier_hash == legacy_hash) @@ -196,7 +196,7 @@ class IndexingPipelineService: continue native_hash = compute_identifier_hash( - doc.document_type.value, doc.unique_id, doc.search_space_id + doc.document_type.value, doc.unique_id, doc.workspace_id ) existing.unique_identifier_hash = native_hash existing.document_type = doc.document_type @@ -235,14 +235,14 @@ class IndexingPipelineService: seen_hashes: set[str] = set() batch_ctx = PipelineLogContext( connector_id=connector_docs[0].connector_id if connector_docs else 0, - search_space_id=connector_docs[0].search_space_id if connector_docs else 0, + workspace_id=connector_docs[0].workspace_id if connector_docs else 0, unique_id="batch", ) for connector_doc in connector_docs: ctx = PipelineLogContext( connector_id=connector_doc.connector_id, - search_space_id=connector_doc.search_space_id, + workspace_id=connector_doc.workspace_id, unique_id=connector_doc.unique_id, ) try: @@ -318,7 +318,7 @@ class IndexingPipelineService: unique_identifier_hash=unique_identifier_hash, source_markdown=connector_doc.source_markdown, document_metadata=connector_doc.metadata, - search_space_id=connector_doc.search_space_id, + workspace_id=connector_doc.workspace_id, connector_id=connector_doc.connector_id, created_by_id=connector_doc.created_by_id, updated_at=datetime.now(UTC), @@ -358,7 +358,7 @@ class IndexingPipelineService: """ ctx = PipelineLogContext( connector_id=connector_doc.connector_id, - search_space_id=connector_doc.search_space_id, + workspace_id=connector_doc.workspace_id, unique_id=connector_doc.unique_id, doc_id=document.id, ) @@ -565,13 +565,13 @@ class IndexingPipelineService: return len(new_texts) async def _enqueue_ai_sort_if_enabled(self, document: Document) -> None: - """Fire-and-forget: enqueue incremental AI sort if the search space has it enabled.""" + """Fire-and-forget: enqueue incremental AI sort if the workspace has it enabled.""" try: - from app.db import SearchSpace + from app.db import Workspace result = await self.session.execute( - select(SearchSpace.ai_file_sort_enabled).where( - SearchSpace.id == document.search_space_id + select(Workspace.ai_file_sort_enabled).where( + Workspace.id == document.workspace_id ) ) enabled = result.scalar() @@ -581,7 +581,7 @@ class IndexingPipelineService: from app.tasks.celery_tasks.document_tasks import ai_sort_document_task user_id = str(document.created_by_id) if document.created_by_id else "" - ai_sort_document_task.delay(document.search_space_id, user_id, document.id) + ai_sort_document_task.delay(document.workspace_id, user_id, document.id) except Exception: logging.getLogger(__name__).warning( "Failed to enqueue AI sort for document %s", document.id, exc_info=True diff --git a/surfsense_backend/app/indexing_pipeline/pipeline_logger.py b/surfsense_backend/app/indexing_pipeline/pipeline_logger.py index 281a92c52..1d85dcefc 100644 --- a/surfsense_backend/app/indexing_pipeline/pipeline_logger.py +++ b/surfsense_backend/app/indexing_pipeline/pipeline_logger.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) @dataclass class PipelineLogContext: connector_id: int | None - search_space_id: int + workspace_id: int unique_id: str # always available from ConnectorDocument doc_id: int | None = None # set once the DB row exists (index phase only) @@ -36,7 +36,7 @@ class LogMessages: def _format_context(ctx: PipelineLogContext) -> str: parts = [ f"connector_id={ctx.connector_id}", - f"search_space_id={ctx.search_space_id}", + f"workspace_id={ctx.workspace_id}", f"unique_id={ctx.unique_id}", ] if ctx.doc_id is not None: diff --git a/surfsense_backend/app/notifications/api/api.py b/surfsense_backend/app/notifications/api/api.py index 7794a5867..9bf52fa34 100644 --- a/surfsense_backend/app/notifications/api/api.py +++ b/surfsense_backend/app/notifications/api/api.py @@ -35,7 +35,7 @@ router = APIRouter(prefix="/notifications", tags=["notifications"]) @router.get("/unread-counts-batch", response_model=BatchUnreadCountResponse) async def get_unread_counts_batch( - search_space_id: int | None = Query(None, description="Filter by search space ID"), + workspace_id: int | None = Query(None, description="Filter by workspace ID"), auth: AuthContext = Depends(require_session_context), session: AsyncSession = Depends(get_async_session), ) -> BatchUnreadCountResponse: @@ -48,11 +48,11 @@ async def get_unread_counts_batch( Notification.read == False, # noqa: E712 ] - if search_space_id is not None: - # Include global (null search-space) notifications. + if workspace_id is not None: + # Include global (null workspace) notifications. base_filter.append( - (Notification.search_space_id == search_space_id) - | (Notification.search_space_id.is_(None)) + (Notification.workspace_id == workspace_id) + | (Notification.workspace_id.is_(None)) ) is_comments = Notification.type.in_(CATEGORY_TYPES["comments"]) @@ -87,7 +87,7 @@ async def get_unread_counts_batch( @router.get("/source-types", response_model=SourceTypesResponse) async def get_notification_source_types( - search_space_id: int | None = Query(None, description="Filter by search space ID"), + workspace_id: int | None = Query(None, description="Filter by workspace ID"), auth: AuthContext = Depends(require_session_context), session: AsyncSession = Depends(get_async_session), ) -> SourceTypesResponse: @@ -95,11 +95,11 @@ async def get_notification_source_types( user = auth.user base_filter = [Notification.user_id == user.id] - if search_space_id is not None: - # Include global (null search-space) notifications. + if workspace_id is not None: + # Include global (null workspace) notifications. base_filter.append( - (Notification.search_space_id == search_space_id) - | (Notification.search_space_id.is_(None)) + (Notification.workspace_id == workspace_id) + | (Notification.workspace_id.is_(None)) ) connector_type_expr = Notification.notification_metadata["connector_type"].astext @@ -156,7 +156,7 @@ async def get_notification_source_types( @router.get("/unread-count", response_model=UnreadCountResponse) async def get_unread_count( - search_space_id: int | None = Query(None, description="Filter by search space ID"), + workspace_id: int | None = Query(None, description="Filter by workspace ID"), type_filter: NotificationType | None = Query( None, alias="type", description="Filter by notification type" ), @@ -179,11 +179,11 @@ async def get_unread_count( Notification.read == False, # noqa: E712 ] - if search_space_id is not None: - # Include global (null search-space) notifications. + if workspace_id is not None: + # Include global (null workspace) notifications. base_filter.append( - (Notification.search_space_id == search_space_id) - | (Notification.search_space_id.is_(None)) + (Notification.workspace_id == workspace_id) + | (Notification.workspace_id.is_(None)) ) if type_filter: @@ -211,7 +211,7 @@ async def get_unread_count( @router.get("", response_model=NotificationListResponse) async def list_notifications( - search_space_id: int | None = Query(None, description="Filter by search space ID"), + workspace_id: int | None = Query(None, description="Filter by workspace ID"), type_filter: NotificationType | None = Query( None, alias="type", description="Filter by notification type" ), @@ -244,15 +244,15 @@ async def list_notifications( Notification.user_id == user.id ) - if search_space_id is not None: - # Include global (null search-space) notifications. + if workspace_id is not None: + # Include global (null workspace) notifications. query = query.where( - (Notification.search_space_id == search_space_id) - | (Notification.search_space_id.is_(None)) + (Notification.workspace_id == workspace_id) + | (Notification.workspace_id.is_(None)) ) count_query = count_query.where( - (Notification.search_space_id == search_space_id) - | (Notification.search_space_id.is_(None)) + (Notification.workspace_id == workspace_id) + | (Notification.workspace_id.is_(None)) ) if type_filter: diff --git a/surfsense_backend/app/notifications/api/schemas.py b/surfsense_backend/app/notifications/api/schemas.py index 727e5485a..3dbb2fb4c 100644 --- a/surfsense_backend/app/notifications/api/schemas.py +++ b/surfsense_backend/app/notifications/api/schemas.py @@ -10,7 +10,7 @@ class NotificationResponse(BaseModel): id: int user_id: str - search_space_id: int | None + workspace_id: int | None type: str title: str message: str diff --git a/surfsense_backend/app/notifications/api/transform.py b/surfsense_backend/app/notifications/api/transform.py index 8970cb0b8..14027a019 100644 --- a/surfsense_backend/app/notifications/api/transform.py +++ b/surfsense_backend/app/notifications/api/transform.py @@ -47,7 +47,7 @@ def to_response(notification: Notification) -> NotificationResponse: return NotificationResponse( id=notification.id, user_id=str(notification.user_id), - search_space_id=notification.search_space_id, + workspace_id=notification.workspace_id, type=notification.type, title=notification.title, message=notification.message, diff --git a/surfsense_backend/app/notifications/service/base.py b/surfsense_backend/app/notifications/service/base.py index 31b378cda..8b416a380 100644 --- a/surfsense_backend/app/notifications/service/base.py +++ b/surfsense_backend/app/notifications/service/base.py @@ -27,7 +27,7 @@ class BaseNotificationHandler: session: AsyncSession, user_id: UUID, operation_id: str, - search_space_id: int | None = None, + workspace_id: int | None = None, ) -> Notification | None: """Return the notification for ``operation_id``, if one exists.""" query = select(Notification).where( @@ -35,8 +35,8 @@ class BaseNotificationHandler: Notification.type == self.notification_type, Notification.notification_metadata["operation_id"].astext == operation_id, ) - if search_space_id is not None: - query = query.where(Notification.search_space_id == search_space_id) + if workspace_id is not None: + query = query.where(Notification.workspace_id == workspace_id) result = await session.execute(query) return result.scalar_one_or_none() @@ -48,12 +48,12 @@ class BaseNotificationHandler: operation_id: str, title: str, message: str, - search_space_id: int | None = None, + workspace_id: int | None = None, initial_metadata: dict[str, Any] | None = None, ) -> Notification: """Upsert a notification keyed by ``operation_id``.""" notification = await self.find_notification_by_operation( - session, user_id, operation_id, search_space_id + session, user_id, operation_id, workspace_id ) if notification: @@ -77,7 +77,7 @@ class BaseNotificationHandler: notification = Notification( user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, type=self.notification_type, title=title, message=message, diff --git a/surfsense_backend/app/notifications/service/facade.py b/surfsense_backend/app/notifications/service/facade.py index 9f4ad50d0..29a36f6bf 100644 --- a/surfsense_backend/app/notifications/service/facade.py +++ b/surfsense_backend/app/notifications/service/facade.py @@ -38,13 +38,13 @@ class NotificationService: notification_type: str, title: str, message: str, - search_space_id: int | None = None, + workspace_id: int | None = None, notification_metadata: dict[str, Any] | None = None, ) -> Notification: """Create a generic notification of any ``notification_type``.""" notification = Notification( user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, type=notification_type, title=title, message=message, diff --git a/surfsense_backend/app/notifications/service/handlers/auto_reload_failed.py b/surfsense_backend/app/notifications/service/handlers/auto_reload_failed.py index 0234a436d..d371a96b9 100644 --- a/surfsense_backend/app/notifications/service/handlers/auto_reload_failed.py +++ b/surfsense_backend/app/notifications/service/handlers/auto_reload_failed.py @@ -30,7 +30,7 @@ class AutoReloadFailedNotificationHandler(BaseNotificationHandler): ) -> Notification: """Notify that an off-session auto-reload charge was declined. - Not tied to a search space (``search_space_id`` is None); the action + Not tied to a workspace (``workspace_id`` is None); the action links to the billing settings so the user can fix their card. """ op_id = msg.operation_id(payment_intent_id or "") @@ -42,7 +42,7 @@ class AutoReloadFailedNotificationHandler(BaseNotificationHandler): operation_id=op_id, title=title, message=message, - search_space_id=None, + workspace_id=None, initial_metadata={ "amount_micros": amount_micros, "payment_intent_id": payment_intent_id, diff --git a/surfsense_backend/app/notifications/service/handlers/comment_reply.py b/surfsense_backend/app/notifications/service/handlers/comment_reply.py index 7d9a9495a..54055bd81 100644 --- a/surfsense_backend/app/notifications/service/handlers/comment_reply.py +++ b/surfsense_backend/app/notifications/service/handlers/comment_reply.py @@ -49,7 +49,7 @@ class CommentReplyNotificationHandler(BaseNotificationHandler): author_avatar_url: str | None, author_email: str, content_preview: str, - search_space_id: int, + workspace_id: int, ) -> Notification: """Notify of a reply; idempotent on ``reply_id`` per user.""" existing = await self.find_notification_by_reply(session, reply_id, user_id) @@ -78,7 +78,7 @@ class CommentReplyNotificationHandler(BaseNotificationHandler): try: notification = Notification( user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, type=self.notification_type, title=title, message=message, diff --git a/surfsense_backend/app/notifications/service/handlers/connector_indexing.py b/surfsense_backend/app/notifications/service/handlers/connector_indexing.py index 9ebfae2ea..c656d30ec 100644 --- a/surfsense_backend/app/notifications/service/handlers/connector_indexing.py +++ b/surfsense_backend/app/notifications/service/handlers/connector_indexing.py @@ -24,7 +24,7 @@ class ConnectorIndexingNotificationHandler(BaseNotificationHandler): connector_id: int, connector_name: str, connector_type: str, - search_space_id: int, + workspace_id: int, start_date: str | None = None, end_date: str | None = None, ) -> Notification: @@ -49,7 +49,7 @@ class ConnectorIndexingNotificationHandler(BaseNotificationHandler): operation_id=operation_id, title=title, message=message, - search_space_id=search_space_id, + workspace_id=workspace_id, initial_metadata=metadata, ) @@ -144,7 +144,7 @@ class ConnectorIndexingNotificationHandler(BaseNotificationHandler): connector_id: int, connector_name: str, connector_type: str, - search_space_id: int, + workspace_id: int, folder_count: int, file_count: int, folder_names: list[str] | None = None, @@ -178,6 +178,6 @@ class ConnectorIndexingNotificationHandler(BaseNotificationHandler): operation_id=operation_id, title=title, message=message, - search_space_id=search_space_id, + workspace_id=workspace_id, initial_metadata=metadata, ) diff --git a/surfsense_backend/app/notifications/service/handlers/document_processing.py b/surfsense_backend/app/notifications/service/handlers/document_processing.py index 714c4f1aa..5a65324e7 100644 --- a/surfsense_backend/app/notifications/service/handlers/document_processing.py +++ b/surfsense_backend/app/notifications/service/handlers/document_processing.py @@ -23,11 +23,11 @@ class DocumentProcessingNotificationHandler(BaseNotificationHandler): user_id: UUID, document_type: str, document_name: str, - search_space_id: int, + workspace_id: int, file_size: int | None = None, ) -> Notification: """Open the notification when document processing is queued.""" - operation_id = msg.operation_id(document_type, document_name, search_space_id) + operation_id = msg.operation_id(document_type, document_name, workspace_id) title = msg.started_title(document_name) message = "Waiting in queue" @@ -46,7 +46,7 @@ class DocumentProcessingNotificationHandler(BaseNotificationHandler): operation_id=operation_id, title=title, message=message, - search_space_id=search_space_id, + workspace_id=workspace_id, initial_metadata=metadata, ) diff --git a/surfsense_backend/app/notifications/service/handlers/insufficient_credits.py b/surfsense_backend/app/notifications/service/handlers/insufficient_credits.py index 46124f222..b6cd12976 100644 --- a/surfsense_backend/app/notifications/service/handlers/insufficient_credits.py +++ b/surfsense_backend/app/notifications/service/handlers/insufficient_credits.py @@ -26,12 +26,12 @@ class InsufficientCreditsNotificationHandler(BaseNotificationHandler): user_id: UUID, document_name: str, document_type: str, - search_space_id: int, + workspace_id: int, balance_micros: int, required_micros: int, ) -> Notification: """Notify that a document was blocked by insufficient credit.""" - operation_id = msg.operation_id(document_name, search_space_id) + operation_id = msg.operation_id(document_name, workspace_id) title, message = msg.summary(document_name, balance_micros, required_micros) metadata = { @@ -43,13 +43,13 @@ class InsufficientCreditsNotificationHandler(BaseNotificationHandler): "status": "failed", "error_type": "insufficient_credits", # Where the inbox item links to. - "action_url": f"/dashboard/{search_space_id}/buy-more", + "action_url": f"/dashboard/{workspace_id}/buy-more", "action_label": "Buy credits", } notification = Notification( user_id=user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, type=self.notification_type, title=title, message=message, diff --git a/surfsense_backend/app/notifications/service/handlers/mention.py b/surfsense_backend/app/notifications/service/handlers/mention.py index 568dc01de..2502648a2 100644 --- a/surfsense_backend/app/notifications/service/handlers/mention.py +++ b/surfsense_backend/app/notifications/service/handlers/mention.py @@ -48,7 +48,7 @@ class MentionNotificationHandler(BaseNotificationHandler): author_avatar_url: str | None, author_email: str, content_preview: str, - search_space_id: int, + workspace_id: int, ) -> Notification: """Notify a mentioned user; idempotent on ``mention_id``.""" existing = await self.find_notification_by_mention(session, mention_id) @@ -77,7 +77,7 @@ class MentionNotificationHandler(BaseNotificationHandler): try: notification = Notification( user_id=mentioned_user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, type=self.notification_type, title=title, message=message, diff --git a/surfsense_backend/app/notifications/service/messages/document_processing.py b/surfsense_backend/app/notifications/service/messages/document_processing.py index 1f324b35d..fea583f07 100644 --- a/surfsense_backend/app/notifications/service/messages/document_processing.py +++ b/surfsense_backend/app/notifications/service/messages/document_processing.py @@ -9,11 +9,11 @@ from typing import Any from app.notifications.service.messages.text import format_title -def operation_id(document_type: str, filename: str, search_space_id: int) -> str: +def operation_id(document_type: str, filename: str, workspace_id: int) -> str: """Build a unique id for a document processing run.""" timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S_%f") filename_hash = hashlib.md5(filename.encode()).hexdigest()[:8] - return f"doc_{document_type}_{search_space_id}_{timestamp}_{filename_hash}" + return f"doc_{document_type}_{workspace_id}_{timestamp}_{filename_hash}" def started_title(document_name: str) -> str: diff --git a/surfsense_backend/app/notifications/service/messages/insufficient_credits.py b/surfsense_backend/app/notifications/service/messages/insufficient_credits.py index fad26ad91..5e8ca5299 100644 --- a/surfsense_backend/app/notifications/service/messages/insufficient_credits.py +++ b/surfsense_backend/app/notifications/service/messages/insufficient_credits.py @@ -8,11 +8,11 @@ from datetime import UTC, datetime from app.notifications.service.messages.text import truncate -def operation_id(document_name: str, search_space_id: int) -> str: +def operation_id(document_name: str, workspace_id: int) -> str: """Build a unique id for an insufficient-credits notification.""" timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S_%f") doc_hash = hashlib.md5(document_name.encode()).hexdigest()[:8] - return f"insufficient_credits_{search_space_id}_{timestamp}_{doc_hash}" + return f"insufficient_credits_{workspace_id}_{timestamp}_{doc_hash}" def summary( diff --git a/surfsense_backend/app/observability/metrics.py b/surfsense_backend/app/observability/metrics.py index ade43ab01..4fd4e8c1f 100644 --- a/surfsense_backend/app/observability/metrics.py +++ b/surfsense_backend/app/observability/metrics.py @@ -534,12 +534,12 @@ def record_tool_call_error(*, tool_name: str) -> None: def record_kb_search_duration( - duration_ms: float, *, search_space_id: int | None, surface: str + duration_ms: float, *, workspace_id: int | None, surface: str ) -> None: _record( _kb_search_duration(), duration_ms, - {"search_space.id": search_space_id, "search.surface": surface}, + {"search_space.id": workspace_id, "search.surface": surface}, ) diff --git a/surfsense_backend/app/observability/otel.py b/surfsense_backend/app/observability/otel.py index ad2178f39..c2ec951dd 100644 --- a/surfsense_backend/app/observability/otel.py +++ b/surfsense_backend/app/observability/otel.py @@ -254,14 +254,14 @@ def model_call_span( def kb_search_span( *, - search_space_id: int | None = None, + workspace_id: int | None = None, query_chars: int | None = None, extra: dict[str, Any] | None = None, ): """Span around knowledge-base search routines.""" attrs: dict[str, Any] = {} - if search_space_id is not None: - attrs["search_space.id"] = int(search_space_id) + if workspace_id is not None: + attrs["search_space.id"] = int(workspace_id) if query_chars is not None: attrs["query.chars"] = int(query_chars) if extra: @@ -289,7 +289,7 @@ def kb_persist_span( def chat_request_span( *, chat_id: int | None = None, - search_space_id: int | None = None, + workspace_id: int | None = None, flow: str | None = None, request_id: str | None = None, turn_id: str | None = None, @@ -302,8 +302,8 @@ def chat_request_span( attrs: dict[str, Any] = {} if chat_id is not None: attrs["chat.id"] = int(chat_id) - if search_space_id is not None: - attrs["search_space.id"] = int(search_space_id) + if workspace_id is not None: + attrs["search_space.id"] = int(workspace_id) if flow: attrs["chat.flow"] = flow if request_id: diff --git a/surfsense_backend/app/podcasts/api/routes.py b/surfsense_backend/app/podcasts/api/routes.py index 582b0531e..3cf293a2f 100644 --- a/surfsense_backend/app/podcasts/api/routes.py +++ b/surfsense_backend/app/podcasts/api/routes.py @@ -22,8 +22,8 @@ from app.auth.context import AuthContext from app.config import config as app_config from app.db import ( Permission, - SearchSpace, - SearchSpaceMembership, + Workspace, + WorkspaceMembership, get_async_session, ) from app.podcasts.generation.brief import propose_brief @@ -59,7 +59,7 @@ router = APIRouter() @router.get("/podcasts", response_model=list[PodcastSummary]) async def list_podcasts( - search_space_id: int | None = None, + workspace_id: int | None = None, skip: int = 0, limit: int = 100, session: AsyncSession = Depends(get_async_session), @@ -69,11 +69,11 @@ async def list_podcasts( if skip < 0 or limit < 1: raise HTTPException(status_code=400, detail="Invalid pagination parameters") - if search_space_id is not None: - await _require(session, auth, search_space_id, Permission.PODCASTS_READ) + if workspace_id is not None: + await _require(session, auth, workspace_id, Permission.PODCASTS_READ) query = ( select(Podcast) - .where(Podcast.search_space_id == search_space_id) + .where(Podcast.workspace_id == workspace_id) .order_by(Podcast.created_at.desc()) .offset(skip) .limit(limit) @@ -81,9 +81,9 @@ async def list_podcasts( else: query = ( select(Podcast) - .join(SearchSpace) - .join(SearchSpaceMembership) - .where(SearchSpaceMembership.user_id == user.id) + .join(Workspace) + .join(WorkspaceMembership) + .where(WorkspaceMembership.user_id == user.id) .order_by(Podcast.created_at.desc()) .offset(skip) .limit(limit) @@ -159,19 +159,19 @@ async def create_podcast( session: AsyncSession = Depends(get_async_session), auth: AuthContext = Depends(get_auth_context), ): - await _require(session, auth, body.search_space_id, Permission.PODCASTS_CREATE) + await _require(session, auth, body.workspace_id, Permission.PODCASTS_CREATE) service = PodcastService(session) podcast = await service.create( title=body.title, - search_space_id=body.search_space_id, + workspace_id=body.workspace_id, thread_id=body.thread_id, ) podcast.source_content = body.source_content spec = await propose_brief( session, - search_space_id=body.search_space_id, + workspace_id=body.workspace_id, speaker_count=body.speaker_count, min_seconds=body.min_seconds, max_seconds=body.max_seconds, @@ -219,7 +219,7 @@ async def approve_brief( async with _lifecycle_errors(): await PodcastService(session).begin_drafting(podcast) await session.commit() - draft_transcript_task.delay(podcast.id, podcast.search_space_id) + draft_transcript_task.delay(podcast.id, podcast.workspace_id) return PodcastDetail.of(podcast) @@ -325,15 +325,15 @@ async def stream_podcast( async def _require( session: AsyncSession, auth: AuthContext, - search_space_id: int, + workspace_id: int, permission: Permission, ) -> None: await check_permission( session, auth, - search_space_id, + workspace_id, permission.value, - "You don't have permission for podcasts in this search space", + "You don't have permission for podcasts in this workspace", ) @@ -346,7 +346,7 @@ async def _load( podcast = await PodcastRepository(session).get(podcast_id) if podcast is None: raise HTTPException(status_code=404, detail="Podcast not found") - await _require(session, auth, podcast.search_space_id, permission) + await _require(session, auth, podcast.workspace_id, permission) return podcast diff --git a/surfsense_backend/app/podcasts/api/schemas.py b/surfsense_backend/app/podcasts/api/schemas.py index e9d6e6b0c..12b978c09 100644 --- a/surfsense_backend/app/podcasts/api/schemas.py +++ b/surfsense_backend/app/podcasts/api/schemas.py @@ -30,7 +30,7 @@ class CreatePodcastRequest(BaseModel): """Create a podcast and kick off brief proposal.""" title: str = Field(..., min_length=1, max_length=500) - search_space_id: int + workspace_id: int source_content: str = Field(..., min_length=1) thread_id: int | None = None speaker_count: int = Field(default=DEFAULT_SPEAKER_COUNT, ge=1, le=6) @@ -83,7 +83,7 @@ class PodcastSummary(BaseModel): title: str status: PodcastStatus created_at: datetime - search_space_id: int + workspace_id: int thread_id: int | None = None @@ -100,7 +100,7 @@ class PodcastDetail(BaseModel): duration_seconds: int | None error: str | None created_at: datetime - search_space_id: int + workspace_id: int thread_id: int | None @classmethod @@ -116,6 +116,6 @@ class PodcastDetail(BaseModel): duration_seconds=podcast.duration_seconds, error=podcast.error, created_at=podcast.created_at, - search_space_id=podcast.search_space_id, + workspace_id=podcast.workspace_id, thread_id=podcast.thread_id, ) diff --git a/surfsense_backend/app/podcasts/generation/brief/propose.py b/surfsense_backend/app/podcasts/generation/brief/propose.py index 09d74840e..915a03909 100644 --- a/surfsense_backend/app/podcasts/generation/brief/propose.py +++ b/surfsense_backend/app/podcasts/generation/brief/propose.py @@ -17,7 +17,7 @@ from .state import BriefState async def propose_brief( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, speaker_count: int = DEFAULT_SPEAKER_COUNT, min_seconds: int = DEFAULT_MIN_SECONDS, max_seconds: int = DEFAULT_MAX_SECONDS, @@ -25,7 +25,7 @@ async def propose_brief( ) -> PodcastSpec: """Reuse the last-used language and voices, else English; return the spec.""" last_language, last_voices = preferences_from( - await PodcastRepository(session).latest_with_spec(search_space_id) + await PodcastRepository(session).latest_with_spec(workspace_id) ) config = { "configurable": { diff --git a/surfsense_backend/app/podcasts/generation/transcript/config.py b/surfsense_backend/app/podcasts/generation/transcript/config.py index f627fc166..6c4e7d508 100644 --- a/surfsense_backend/app/podcasts/generation/transcript/config.py +++ b/surfsense_backend/app/podcasts/generation/transcript/config.py @@ -13,7 +13,7 @@ from app.podcasts.schemas import PodcastSpec class TranscriptConfig: """The approved spec and user focus that drive drafting.""" - search_space_id: int + workspace_id: int spec: PodcastSpec focus: str | None = None diff --git a/surfsense_backend/app/podcasts/generation/transcript/nodes.py b/surfsense_backend/app/podcasts/generation/transcript/nodes.py index 7b472348d..6bbbb7777 100644 --- a/surfsense_backend/app/podcasts/generation/transcript/nodes.py +++ b/surfsense_backend/app/podcasts/generation/transcript/nodes.py @@ -97,10 +97,10 @@ def finalize(state: TranscriptState, config: RunnableConfig) -> dict[str, Any]: async def _require_llm(state: TranscriptState, tc: TranscriptConfig): - llm = await get_agent_llm(state.db_session, tc.search_space_id) + llm = await get_agent_llm(state.db_session, tc.workspace_id) if llm is None: raise RuntimeError( - f"no agent LLM configured for search space {tc.search_space_id}" + f"no agent LLM configured for workspace {tc.workspace_id}" ) return llm diff --git a/surfsense_backend/app/podcasts/persistence/repository.py b/surfsense_backend/app/podcasts/persistence/repository.py index 04eae9ce1..d1620d136 100644 --- a/surfsense_backend/app/podcasts/persistence/repository.py +++ b/surfsense_backend/app/podcasts/persistence/repository.py @@ -28,7 +28,7 @@ class PodcastRepository: await self._session.flush() return podcast - async def latest_with_spec(self, search_space_id: int) -> Podcast | None: + async def latest_with_spec(self, workspace_id: int) -> Podcast | None: """Most recent podcast in the space that has a stored brief. Used to seed language/voice defaults for a new podcast from what the @@ -37,7 +37,7 @@ class PodcastRepository: result = await self._session.execute( select(Podcast) .where( - Podcast.search_space_id == search_space_id, + Podcast.workspace_id == workspace_id, Podcast.spec.is_not(None), ) .order_by(Podcast.created_at.desc()) diff --git a/surfsense_backend/app/podcasts/service.py b/surfsense_backend/app/podcasts/service.py index 165bc77a4..e1da27b4f 100644 --- a/surfsense_backend/app/podcasts/service.py +++ b/surfsense_backend/app/podcasts/service.py @@ -86,12 +86,12 @@ class PodcastService: self._repo = PodcastRepository(session) async def create( - self, *, title: str, search_space_id: int, thread_id: int | None = None + self, *, title: str, workspace_id: int, thread_id: int | None = None ) -> Podcast: """Create a fresh podcast in ``PENDING`` awaiting its brief.""" podcast = Podcast( title=title, - search_space_id=search_space_id, + workspace_id=workspace_id, thread_id=thread_id, status=PodcastStatus.PENDING, spec_version=1, diff --git a/surfsense_backend/app/podcasts/storage.py b/surfsense_backend/app/podcasts/storage.py index c3326460d..1c0f577dc 100644 --- a/surfsense_backend/app/podcasts/storage.py +++ b/surfsense_backend/app/podcasts/storage.py @@ -16,21 +16,21 @@ from app.podcasts.persistence import Podcast _AUDIO_CONTENT_TYPE = "audio/mpeg" -def build_audio_key(*, search_space_id: int, podcast_id: int) -> str: +def build_audio_key(*, workspace_id: int, podcast_id: int) -> str: """Object key for a podcast's audio. - Shape: ``podcasts/{search_space_id}/{podcast_id}/{uuid}.mp3``. The uuid lets + Shape: ``podcasts/{workspace_id}/{podcast_id}/{uuid}.mp3``. The uuid lets a re-render write a fresh object before the old one is purged. """ - return f"podcasts/{search_space_id}/{podcast_id}/{uuid.uuid4().hex}.mp3" + return f"podcasts/{workspace_id}/{podcast_id}/{uuid.uuid4().hex}.mp3" async def store_audio( - *, search_space_id: int, podcast_id: int, data: bytes + *, workspace_id: int, podcast_id: int, data: bytes ) -> tuple[str, str]: """Persist audio bytes and return ``(backend_name, storage_key)``.""" backend = get_storage_backend() - key = build_audio_key(search_space_id=search_space_id, podcast_id=podcast_id) + key = build_audio_key(workspace_id=workspace_id, podcast_id=podcast_id) await backend.put(key, data, content_type=_AUDIO_CONTENT_TYPE) return backend.backend_name, key diff --git a/surfsense_backend/app/podcasts/tasks/draft.py b/surfsense_backend/app/podcasts/tasks/draft.py index c5b489571..7b5a82d70 100644 --- a/surfsense_backend/app/podcasts/tasks/draft.py +++ b/surfsense_backend/app/podcasts/tasks/draft.py @@ -19,7 +19,7 @@ from app.podcasts.service import PodcastService, read_spec from app.services.billable_calls import ( BillingSettlementError, QuotaInsufficientError, - _resolve_agent_billing_for_search_space, + _resolve_agent_billing_for_workspace, billable_call, ) from app.tasks.celery_tasks import get_celery_session_maker, run_async_celery_task @@ -31,10 +31,10 @@ logger = logging.getLogger(__name__) @celery_app.task(name="podcast.draft_transcript", bind=True) -def draft_transcript_task(self, podcast_id: int, search_space_id: int) -> dict: +def draft_transcript_task(self, podcast_id: int, workspace_id: int) -> dict: try: return run_async_celery_task( - lambda: _draft_transcript(podcast_id, search_space_id) + lambda: _draft_transcript(podcast_id, workspace_id) ) except Exception as exc: logger.error("Podcast %s drafting failed: %s", podcast_id, exc) @@ -43,7 +43,7 @@ def draft_transcript_task(self, podcast_id: int, search_space_id: int) -> dict: return {"status": "failed", "podcast_id": podcast_id} -async def _draft_transcript(podcast_id: int, search_space_id: int) -> dict: +async def _draft_transcript(podcast_id: int, workspace_id: int) -> dict: async with get_celery_session_maker()() as session: repo = PodcastRepository(session) service = PodcastService(session) @@ -55,8 +55,8 @@ async def _draft_transcript(podcast_id: int, search_space_id: int) -> dict: if spec is None: raise ValueError(f"podcast {podcast_id} has no approved brief") - owner_id, tier, base_model = await _resolve_agent_billing_for_search_space( - session, search_space_id, thread_id=podcast.thread_id + owner_id, tier, base_model = await _resolve_agent_billing_for_workspace( + session, workspace_id, thread_id=podcast.thread_id ) state = TranscriptState( @@ -64,7 +64,7 @@ async def _draft_transcript(podcast_id: int, search_space_id: int) -> dict: ) config = { "configurable": { - "search_space_id": search_space_id, + "workspace_id": workspace_id, "spec": spec, "focus": spec.focus, } @@ -73,7 +73,7 @@ async def _draft_transcript(podcast_id: int, search_space_id: int) -> dict: try: async with billable_call( user_id=owner_id, - search_space_id=search_space_id, + workspace_id=workspace_id, billing_tier=tier, base_model=base_model, quota_reserve_micros_override=app_config.QUOTA_DEFAULT_PODCAST_RESERVE_MICROS, diff --git a/surfsense_backend/app/podcasts/tasks/render.py b/surfsense_backend/app/podcasts/tasks/render.py index 2e550a868..7759691a9 100644 --- a/surfsense_backend/app/podcasts/tasks/render.py +++ b/surfsense_backend/app/podcasts/tasks/render.py @@ -67,7 +67,7 @@ async def _render_audio(podcast_id: int) -> dict: superseded_key = podcast.storage_key backend_name, key = await store_audio( - search_space_id=podcast.search_space_id, + workspace_id=podcast.workspace_id, podcast_id=podcast_id, data=rendered.data, ) diff --git a/surfsense_backend/app/retriever/chunks_hybrid_search.py b/surfsense_backend/app/retriever/chunks_hybrid_search.py index 5e5edec2e..8f247b661 100644 --- a/surfsense_backend/app/retriever/chunks_hybrid_search.py +++ b/surfsense_backend/app/retriever/chunks_hybrid_search.py @@ -14,29 +14,29 @@ def _instrument_search(mode: str): def _decorator(func): @functools.wraps(func) async def _wrapper( - self, query_text: str, top_k: int, search_space_id: int, *args, **kwargs + self, query_text: str, top_k: int, workspace_id: int, *args, **kwargs ): t0 = time.perf_counter() with ot.kb_search_span( - search_space_id=search_space_id, + workspace_id=workspace_id, query_chars=len(query_text), extra={"search.surface": "chunks", "search.mode": mode}, ) as sp: try: result = await func( - self, query_text, top_k, search_space_id, *args, **kwargs + self, query_text, top_k, workspace_id, *args, **kwargs ) except Exception: ot_metrics.record_kb_search_duration( (time.perf_counter() - t0) * 1000, - search_space_id=search_space_id, + workspace_id=workspace_id, surface="chunks", ) raise sp.set_attribute("result.count", len(result)) ot_metrics.record_kb_search_duration( (time.perf_counter() - t0) * 1000, - search_space_id=search_space_id, + workspace_id=workspace_id, surface="chunks", ) return result @@ -61,7 +61,7 @@ class ChucksHybridSearchRetriever: self, query_text: str, top_k: int, - search_space_id: int, + workspace_id: int, start_date: datetime | None = None, end_date: datetime | None = None, ) -> list: @@ -71,7 +71,7 @@ class ChucksHybridSearchRetriever: Args: query_text: The search query text top_k: Number of results to return - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -96,12 +96,12 @@ class ChucksHybridSearchRetriever: time.perf_counter() - t_embed, ) - # Build the query filtered by search space + # Build the query filtered by workspace query = ( select(Chunk) - .options(joinedload(Chunk.document).joinedload(Document.search_space)) + .options(joinedload(Chunk.document).joinedload(Document.workspace)) .join(Document, Chunk.document_id == Document.id) - .where(Document.search_space_id == search_space_id) + .where(Document.workspace_id == workspace_id) ) # Add time-based filtering if provided @@ -122,7 +122,7 @@ class ChucksHybridSearchRetriever: time.perf_counter() - t_db, len(chunks), time.perf_counter() - t0, - search_space_id, + workspace_id, ) return chunks @@ -132,7 +132,7 @@ class ChucksHybridSearchRetriever: self, query_text: str, top_k: int, - search_space_id: int, + workspace_id: int, start_date: datetime | None = None, end_date: datetime | None = None, ) -> list: @@ -142,7 +142,7 @@ class ChucksHybridSearchRetriever: Args: query_text: The search query text top_k: Number of results to return - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -161,12 +161,12 @@ class ChucksHybridSearchRetriever: tsvector = func.to_tsvector("english", Chunk.content) tsquery = func.plainto_tsquery("english", query_text) - # Build the query filtered by search space + # Build the query filtered by workspace query = ( select(Chunk) - .options(joinedload(Chunk.document).joinedload(Document.search_space)) + .options(joinedload(Chunk.document).joinedload(Document.workspace)) .join(Document, Chunk.document_id == Document.id) - .where(Document.search_space_id == search_space_id) + .where(Document.workspace_id == workspace_id) .where( tsvector.op("@@")(tsquery) ) # Only include results that match the query @@ -188,7 +188,7 @@ class ChucksHybridSearchRetriever: "[chunk_search] full_text_search in %.3fs results=%d space=%d", time.perf_counter() - t0, len(chunks), - search_space_id, + workspace_id, ) return chunks @@ -198,7 +198,7 @@ class ChucksHybridSearchRetriever: self, query_text: str, top_k: int, - search_space_id: int, + workspace_id: int, document_type: str | list[str] | None = None, start_date: datetime | None = None, end_date: datetime | None = None, @@ -213,7 +213,7 @@ class ChucksHybridSearchRetriever: Args: query_text: The search query text top_k: Number of documents to return - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within document_type: Optional document type to filter results (e.g., "FILE", "CRAWLED_URL") start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -252,10 +252,10 @@ class ChucksHybridSearchRetriever: tsvector = func.to_tsvector("english", Chunk.content) tsquery = func.plainto_tsquery("english", query_text) - # Base conditions for chunk filtering - search space is required. + # Base conditions for chunk filtering - workspace is required. # Exclude documents in "deleting" state (background deletion in progress). base_conditions = [ - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, func.coalesce(Document.status["state"].astext, "ready") != "deleting", ] @@ -284,7 +284,7 @@ class ChucksHybridSearchRetriever: if end_date is not None: base_conditions.append(Document.updated_at <= end_date) - # CTE for semantic search filtered by search space + # CTE for semantic search filtered by workspace semantic_search_cte = ( select( Chunk.id, @@ -302,7 +302,7 @@ class ChucksHybridSearchRetriever: .cte("semantic_search") ) - # CTE for keyword search filtered by search space + # CTE for keyword search filtered by workspace keyword_search_cte = ( select( Chunk.id, @@ -355,7 +355,7 @@ class ChucksHybridSearchRetriever: "[chunk_search] hybrid_search RRF query in %.3fs results=%d space=%d type=%s", time.perf_counter() - t_rrf, len(chunks_with_scores), - search_space_id, + workspace_id, document_type, ) @@ -493,7 +493,7 @@ class ChucksHybridSearchRetriever: "[chunk_search] hybrid_search TOTAL in %.3fs docs=%d space=%d type=%s", time.perf_counter() - t0, len(final_docs), - search_space_id, + workspace_id, document_type, ) return final_docs diff --git a/surfsense_backend/app/retriever/documents_hybrid_search.py b/surfsense_backend/app/retriever/documents_hybrid_search.py index d856e93cf..9daa2d510 100644 --- a/surfsense_backend/app/retriever/documents_hybrid_search.py +++ b/surfsense_backend/app/retriever/documents_hybrid_search.py @@ -13,29 +13,29 @@ def _instrument_search(mode: str): def _decorator(func): @functools.wraps(func) async def _wrapper( - self, query_text: str, top_k: int, search_space_id: int, *args, **kwargs + self, query_text: str, top_k: int, workspace_id: int, *args, **kwargs ): t0 = time.perf_counter() with ot.kb_search_span( - search_space_id=search_space_id, + workspace_id=workspace_id, query_chars=len(query_text), extra={"search.surface": "documents", "search.mode": mode}, ) as sp: try: result = await func( - self, query_text, top_k, search_space_id, *args, **kwargs + self, query_text, top_k, workspace_id, *args, **kwargs ) except Exception: ot_metrics.record_kb_search_duration( (time.perf_counter() - t0) * 1000, - search_space_id=search_space_id, + workspace_id=workspace_id, surface="documents", ) raise sp.set_attribute("result.count", len(result)) ot_metrics.record_kb_search_duration( (time.perf_counter() - t0) * 1000, - search_space_id=search_space_id, + workspace_id=workspace_id, surface="documents", ) return result @@ -60,7 +60,7 @@ class DocumentHybridSearchRetriever: self, query_text: str, top_k: int, - search_space_id: int, + workspace_id: int, start_date: datetime | None = None, end_date: datetime | None = None, ) -> list: @@ -70,7 +70,7 @@ class DocumentHybridSearchRetriever: Args: query_text: The search query text top_k: Number of results to return - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -90,11 +90,11 @@ class DocumentHybridSearchRetriever: embedding_model = config.embedding_model_instance query_embedding = embedding_model.embed(query_text) - # Build the query filtered by search space + # Build the query filtered by workspace query = ( select(Document) - .options(joinedload(Document.search_space)) - .where(Document.search_space_id == search_space_id) + .options(joinedload(Document.workspace)) + .where(Document.workspace_id == workspace_id) ) # Add time-based filtering if provided @@ -115,7 +115,7 @@ class DocumentHybridSearchRetriever: "[doc_search] vector_search in %.3fs results=%d space=%d", time.perf_counter() - t0, len(documents), - search_space_id, + workspace_id, ) return documents @@ -125,7 +125,7 @@ class DocumentHybridSearchRetriever: self, query_text: str, top_k: int, - search_space_id: int, + workspace_id: int, start_date: datetime | None = None, end_date: datetime | None = None, ) -> list: @@ -135,7 +135,7 @@ class DocumentHybridSearchRetriever: Args: query_text: The search query text top_k: Number of results to return - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -154,11 +154,11 @@ class DocumentHybridSearchRetriever: tsvector = func.to_tsvector("english", Document.content) tsquery = func.plainto_tsquery("english", query_text) - # Build the query filtered by search space + # Build the query filtered by workspace query = ( select(Document) - .options(joinedload(Document.search_space)) - .where(Document.search_space_id == search_space_id) + .options(joinedload(Document.workspace)) + .where(Document.workspace_id == workspace_id) .where( tsvector.op("@@")(tsquery) ) # Only include results that match the query @@ -180,7 +180,7 @@ class DocumentHybridSearchRetriever: "[doc_search] full_text_search in %.3fs results=%d space=%d", time.perf_counter() - t0, len(documents), - search_space_id, + workspace_id, ) return documents @@ -190,7 +190,7 @@ class DocumentHybridSearchRetriever: self, query_text: str, top_k: int, - search_space_id: int, + workspace_id: int, document_type: str | list[str] | None = None, start_date: datetime | None = None, end_date: datetime | None = None, @@ -205,7 +205,7 @@ class DocumentHybridSearchRetriever: Args: query_text: The search query text top_k: Number of documents to return - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within document_type: Optional document type to filter results (e.g., "FILE", "CRAWLED_URL") start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -232,10 +232,10 @@ class DocumentHybridSearchRetriever: tsvector = func.to_tsvector("english", Document.content) tsquery = func.plainto_tsquery("english", query_text) - # Base conditions for document filtering - search space is required. + # Base conditions for document filtering - workspace is required. # Exclude documents in "deleting" state (background deletion in progress). base_conditions = [ - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, func.coalesce(Document.status["state"].astext, "ready") != "deleting", ] @@ -264,7 +264,7 @@ class DocumentHybridSearchRetriever: if end_date is not None: base_conditions.append(Document.updated_at <= end_date) - # CTE for semantic search filtered by search space + # CTE for semantic search filtered by workspace semantic_search_cte = select( Document.id, func.rank() @@ -278,7 +278,7 @@ class DocumentHybridSearchRetriever: .cte("semantic_search") ) - # CTE for keyword search filtered by search space + # CTE for keyword search filtered by workspace keyword_search_cte = ( select( Document.id, @@ -317,7 +317,7 @@ class DocumentHybridSearchRetriever: Document.id == func.coalesce(semantic_search_cte.c.id, keyword_search_cte.c.id), ) - .options(joinedload(Document.search_space)) + .options(joinedload(Document.workspace)) .order_by(text("score DESC")) .limit(top_k) ) @@ -419,7 +419,7 @@ class DocumentHybridSearchRetriever: "[doc_search] hybrid_search TOTAL in %.3fs docs=%d space=%d type=%s", time.perf_counter() - t0, len(final_docs), - search_space_id, + workspace_id, document_type, ) return final_docs diff --git a/surfsense_backend/app/services/ai_file_sort_service.py b/surfsense_backend/app/services/ai_file_sort_service.py index 1bf4d325e..cc4727608 100644 --- a/surfsense_backend/app/services/ai_file_sort_service.py +++ b/surfsense_backend/app/services/ai_file_sort_service.py @@ -271,7 +271,7 @@ async def ai_sort_document( leaf_folder = await ensure_folder_hierarchy_with_depth_validation( session, - document.search_space_id, + document.workspace_id, segments, ) @@ -282,13 +282,13 @@ async def ai_sort_document( async def ai_sort_all_documents( session: AsyncSession, - search_space_id: int, + workspace_id: int, llm, ) -> tuple[int, int]: - """Sort all documents in a search space. Returns (sorted_count, failed_count).""" + """Sort all documents in a workspace. Returns (sorted_count, failed_count).""" stmt = ( select(Document) - .where(Document.search_space_id == search_space_id) + .where(Document.workspace_id == workspace_id) .options(selectinload(Document.connector)) ) result = await session.execute(stmt) @@ -310,7 +310,7 @@ async def ai_sort_all_documents( leaf_folder = await ensure_folder_hierarchy_with_depth_validation( session, - search_space_id, + workspace_id, segments, ) doc.folder_id = leaf_folder.id @@ -321,8 +321,8 @@ async def ai_sort_all_documents( await session.commit() logger.info( - "AI sort complete for search_space=%d: sorted=%d, failed=%d", - search_space_id, + "AI sort complete for workspace=%d: sorted=%d, failed=%d", + workspace_id, sorted_count, failed_count, ) diff --git a/surfsense_backend/app/services/auto_model_pin_service.py b/surfsense_backend/app/services/auto_model_pin_service.py index f98933a65..08a64d0d4 100644 --- a/surfsense_backend/app/services/auto_model_pin_service.py +++ b/surfsense_backend/app/services/auto_model_pin_service.py @@ -7,7 +7,7 @@ subsequent turns are stable. Single-writer invariant: this module is the only writer of ``NewChatThread.pinned_llm_config_id`` (aside from the bulk clear in -``model_connections_routes`` when a search space's ``chat_model_id`` changes). +``model_connections_routes`` when a workspace's ``chat_model_id`` changes). Therefore a non-NULL value unambiguously means "this thread has an Auto-resolved pin"; no separate source/policy column is needed. """ @@ -366,7 +366,7 @@ def _global_candidates( async def _db_candidates( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, user_id: str | UUID | None, capability: str, requires_image_input: bool = False, @@ -388,7 +388,7 @@ async def _db_candidates( conn = model.connection if not conn: continue - if conn.search_space_id is not None and conn.search_space_id != search_space_id: + if conn.workspace_id is not None and conn.workspace_id != workspace_id: continue if ( conn.user_id is not None @@ -430,7 +430,7 @@ async def _db_candidates( async def auto_model_candidates( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, user_id: str | UUID | None, capability: str, requires_image_input: bool = False, @@ -445,7 +445,7 @@ async def auto_model_candidates( shared_global_cooled_down_ids = _shared_runtime_cooled_down_ids(global_ids) db_candidates = await _db_candidates( session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, capability=capability, requires_image_input=requires_image_input, @@ -517,7 +517,7 @@ async def resolve_or_get_pinned_llm_config_id( session: AsyncSession, *, thread_id: int, - search_space_id: int, + workspace_id: int, user_id: str | UUID | None, selected_llm_config_id: int, force_repin_free: bool = False, @@ -550,9 +550,9 @@ async def resolve_or_get_pinned_llm_config_id( ) if thread is None: raise ValueError(f"Thread {thread_id} not found") - if thread.search_space_id != search_space_id: + if thread.workspace_id != workspace_id: raise ValueError( - f"Thread {thread_id} does not belong to search space {search_space_id}" + f"Thread {thread_id} does not belong to workspace {workspace_id}" ) # Explicit model selected: clear any stale pin. @@ -569,7 +569,7 @@ async def resolve_or_get_pinned_llm_config_id( excluded_ids = {int(cid) for cid in (exclude_config_ids or set())} candidates = await auto_model_candidates( session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, capability="chat", requires_image_input=requires_image_input, @@ -595,9 +595,9 @@ async def resolve_or_get_pinned_llm_config_id( ): pinned_cfg = candidate_by_id[int(pinned_id)] logger.info( - "auto_pin_reused thread_id=%s search_space_id=%s resolved_config_id=%s tier=%s", + "auto_pin_reused thread_id=%s workspace_id=%s resolved_config_id=%s tier=%s", thread_id, - search_space_id, + workspace_id, pinned_id, _tier_of(pinned_cfg), ) @@ -622,16 +622,16 @@ async def resolve_or_get_pinned_llm_config_id( # the user's image attachment instead of suspecting a cooldown. if requires_image_input: logger.info( - "auto_pin_repinned_for_image thread_id=%s search_space_id=%s " + "auto_pin_repinned_for_image thread_id=%s workspace_id=%s " "previous_config_id=%s", thread_id, - search_space_id, + workspace_id, pinned_id, ) logger.info( - "auto_pin_invalid thread_id=%s search_space_id=%s pinned_config_id=%s", + "auto_pin_invalid thread_id=%s workspace_id=%s pinned_config_id=%s", thread_id, - search_space_id, + workspace_id, pinned_id, ) @@ -663,27 +663,27 @@ async def resolve_or_get_pinned_llm_config_id( if force_repin_free: logger.info( - "auto_pin_forced_free_repin thread_id=%s search_space_id=%s previous_config_id=%s resolved_config_id=%s", + "auto_pin_forced_free_repin thread_id=%s workspace_id=%s previous_config_id=%s resolved_config_id=%s", thread_id, - search_space_id, + workspace_id, pinned_id, selected_id, ) if pinned_id is None: logger.info( - "auto_pin_created thread_id=%s search_space_id=%s resolved_config_id=%s tier=%s premium_eligible=%s", + "auto_pin_created thread_id=%s workspace_id=%s resolved_config_id=%s tier=%s premium_eligible=%s", thread_id, - search_space_id, + workspace_id, selected_id, selected_tier, premium_eligible, ) else: logger.info( - "auto_pin_repaired thread_id=%s search_space_id=%s previous_config_id=%s resolved_config_id=%s tier=%s premium_eligible=%s", + "auto_pin_repaired thread_id=%s workspace_id=%s previous_config_id=%s resolved_config_id=%s tier=%s premium_eligible=%s", thread_id, - search_space_id, + workspace_id, pinned_id, selected_id, selected_tier, diff --git a/surfsense_backend/app/services/billable_calls.py b/surfsense_backend/app/services/billable_calls.py index 15a3c3e55..9529dd493 100644 --- a/surfsense_backend/app/services/billable_calls.py +++ b/surfsense_backend/app/services/billable_calls.py @@ -117,7 +117,7 @@ async def _record_audit_best_effort( *, session_factory: BillableSessionFactory, usage_type: str, - search_space_id: int, + workspace_id: int, user_id: UUID, prompt_tokens: int, completion_tokens: int, @@ -156,7 +156,7 @@ async def _record_audit_best_effort( await record_token_usage( audit_session, usage_type=usage_type, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, @@ -217,7 +217,7 @@ async def _record_audit_best_effort( async def billable_call( *, user_id: UUID, - search_space_id: int, + workspace_id: int, billing_tier: str, base_model: str, quota_reserve_tokens: int | None = None, @@ -233,9 +233,9 @@ async def billable_call( Args: user_id: Owner of the credit pool to debit. For vision-LLM during - indexing this is the *search-space owner* (issue M), not the + indexing this is the *workspace owner* (issue M), not the triggering user. - search_space_id: Required — recorded on the ``TokenUsage`` audit row. + workspace_id: Required — recorded on the ``TokenUsage`` audit row. billing_tier: ``"premium"`` debits; anything else (``"free"``) skips the reserve/finalize dance but still records an audit row with the captured cost. @@ -281,7 +281,7 @@ async def billable_call( await _record_audit_best_effort( session_factory=session_factory, usage_type=usage_type, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, prompt_tokens=acc.total_prompt_tokens, completion_tokens=acc.total_completion_tokens, @@ -423,7 +423,7 @@ async def billable_call( await _record_audit_best_effort( session_factory=session_factory, usage_type=usage_type, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, prompt_tokens=acc.total_prompt_tokens, completion_tokens=acc.total_completion_tokens, @@ -438,21 +438,21 @@ async def billable_call( ) -async def _resolve_agent_billing_for_search_space( +async def _resolve_agent_billing_for_workspace( session: AsyncSession, - search_space_id: int, + workspace_id: int, *, thread_id: int | None = None, ) -> tuple[UUID, str, str]: - """Resolve ``(owner_user_id, billing_tier, base_model)`` for the search-space + """Resolve ``(owner_user_id, billing_tier, base_model)`` for the workspace chat model. Used by Celery tasks (podcast generation, video presentation) to bill the - search-space owner's premium credit pool when the chat model is premium. + workspace owner's premium credit pool when the chat model is premium. Resolution rules mirror the chat model role resolver: - - Search space not found / no ``chat_model_id``: raise ``ValueError``. + - Workspace not found / no ``chat_model_id``: raise ``ValueError``. - **Auto mode** (``id == AUTO_MODE_ID == 0``): * ``thread_id`` is set: delegate to ``resolve_or_get_pinned_llm_config_id`` (the same call chat uses) and @@ -481,22 +481,22 @@ async def _resolve_agent_billing_for_search_space( from sqlalchemy import select from sqlalchemy.orm import selectinload - from app.db import Model, SearchSpace + from app.db import Model, Workspace result = await session.execute( - select(SearchSpace).where(SearchSpace.id == search_space_id) + select(Workspace).where(Workspace.id == workspace_id) ) - search_space = result.scalars().first() - if search_space is None: - raise ValueError(f"Search space {search_space_id} not found") + workspace = result.scalars().first() + if workspace is None: + raise ValueError(f"Workspace {workspace_id} not found") - chat_model_id = search_space.chat_model_id + chat_model_id = workspace.chat_model_id if chat_model_id is None: raise ValueError( - f"Search space {search_space_id} has no chat_model_id configured" + f"Workspace {workspace_id} has no chat_model_id configured" ) - owner_user_id: UUID = search_space.user_id + owner_user_id: UUID = workspace.user_id from app.services.auto_model_pin_service import ( AUTO_MODE_ID, @@ -510,15 +510,15 @@ async def _resolve_agent_billing_for_search_space( resolution = await resolve_or_get_pinned_llm_config_id( session, thread_id=thread_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=str(owner_user_id), selected_llm_config_id=AUTO_MODE_ID, ) except ValueError: logger.warning( "[agent_billing] Auto-mode pin resolution failed for " - "search_space=%s thread=%s; falling back to free", - search_space_id, + "workspace=%s thread=%s; falling back to free", + workspace_id, thread_id, exc_info=True, ) @@ -546,7 +546,7 @@ async def _resolve_agent_billing_for_search_space( and model.connection is not None and model.connection.enabled and ( - model.connection.search_space_id in (None, search_space_id) + model.connection.workspace_id in (None, workspace_id) and model.connection.user_id in (None, owner_user_id) ) ): @@ -558,7 +558,7 @@ async def _resolve_agent_billing_for_search_space( __all__ = [ "BillingSettlementError", "QuotaInsufficientError", - "_resolve_agent_billing_for_search_space", + "_resolve_agent_billing_for_workspace", "billable_call", ] diff --git a/surfsense_backend/app/services/chat_comments_service.py b/surfsense_backend/app/services/chat_comments_service.py index b44f6f37c..dfe25c267 100644 --- a/surfsense_backend/app/services/chat_comments_service.py +++ b/surfsense_backend/app/services/chat_comments_service.py @@ -17,7 +17,7 @@ from app.db import ( NewChatMessageRole, NewChatThread, Permission, - SearchSpaceMembership, + WorkspaceMembership, User, has_permission, ) @@ -64,7 +64,7 @@ async def process_mentions( session: AsyncSession, comment_id: int, content: str, - search_space_id: int, + workspace_id: int, ) -> dict[UUID, int]: """ Parse mentions from content, validate users are members, and insert mention records. @@ -73,7 +73,7 @@ async def process_mentions( session: Database session comment_id: ID of the comment containing mentions content: Comment text with @[uuid] mentions - search_space_id: ID of the search space for membership validation + workspace_id: ID of the workspace for membership validation Returns: Dictionary mapping mentioned user UUID to their mention record ID @@ -84,9 +84,9 @@ async def process_mentions( # Get valid members from the mentioned UUIDs result = await session.execute( - select(SearchSpaceMembership.user_id).filter( - SearchSpaceMembership.search_space_id == search_space_id, - SearchSpaceMembership.user_id.in_(mentioned_uuids), + select(WorkspaceMembership.user_id).filter( + WorkspaceMembership.workspace_id == workspace_id, + WorkspaceMembership.user_id.in_(mentioned_uuids), ) ) valid_member_ids = result.scalars().all() @@ -166,19 +166,19 @@ async def get_comments_for_message( if not message: raise HTTPException(status_code=404, detail="Message not found") - search_space_id = message.thread.search_space_id + workspace_id = message.thread.workspace_id # Check permission to read comments await check_permission( session, auth, - search_space_id, + workspace_id, Permission.COMMENTS_READ.value, - "You don't have permission to read comments in this search space", + "You don't have permission to read comments in this workspace", ) # Get user permissions for can_delete computation - user_permissions = await get_user_permissions(session, user.id, search_space_id) + user_permissions = await get_user_permissions(session, user.id, workspace_id) can_delete_any = has_permission(user_permissions, Permission.COMMENTS_DELETE.value) # Get top-level comments (parent_id IS NULL) with their authors and replies @@ -276,7 +276,7 @@ async def get_comments_for_messages_batch( """ Batch-fetch comments for multiple messages in a single DB round-trip. - Validates that all messages exist and belong to search spaces the user + Validates that all messages exist and belong to workspaces the user can read comments in, then loads all comments with eager-loaded authors and replies. """ @@ -293,15 +293,15 @@ async def get_comments_for_messages_batch( messages = result.scalars().all() msg_map = {m.id: m for m in messages} - search_space_ids = {m.thread.search_space_id for m in messages} + workspace_ids = {m.thread.workspace_id for m in messages} permissions_cache: dict[int, set] = {} - for ss_id in search_space_ids: + for ss_id in workspace_ids: await check_permission( session, auth, ss_id, Permission.COMMENTS_READ.value, - "You don't have permission to read comments in this search space", + "You don't have permission to read comments in this workspace", ) permissions_cache[ss_id] = await get_user_permissions(session, user.id, ss_id) @@ -338,7 +338,7 @@ async def get_comments_for_messages_batch( comments_by_message[mid] = CommentListResponse(comments=[], total_count=0) continue - ss_id = msg.thread.search_space_id + ss_id = msg.thread.workspace_id user_perms = permissions_cache.get(ss_id, set()) can_delete_any = has_permission(user_perms, Permission.COMMENTS_DELETE.value) @@ -447,14 +447,14 @@ async def create_comment( detail="Comments can only be added to AI responses", ) - search_space_id = message.thread.search_space_id + workspace_id = message.thread.workspace_id # Check permission to create comments - user_permissions = await get_user_permissions(session, user.id, search_space_id) + user_permissions = await get_user_permissions(session, user.id, workspace_id) if not has_permission(user_permissions, Permission.COMMENTS_CREATE.value): raise HTTPException( status_code=403, - detail="You don't have permission to create comments in this search space", + detail="You don't have permission to create comments in this workspace", ) thread = message.thread @@ -468,7 +468,7 @@ async def create_comment( await session.flush() # Process mentions - returns map of user_id -> mention_id - mentions_map = await process_mentions(session, comment.id, content, search_space_id) + mentions_map = await process_mentions(session, comment.id, content, workspace_id) await session.commit() await session.refresh(comment) @@ -495,7 +495,7 @@ async def create_comment( author_avatar_url=user.avatar_url, author_email=user.email, content_preview=content_preview[:200], - search_space_id=search_space_id, + workspace_id=workspace_id, ) author = AuthorResponse( @@ -561,14 +561,14 @@ async def create_reply( detail="Cannot reply to a reply", ) - search_space_id = parent_comment.message.thread.search_space_id + workspace_id = parent_comment.message.thread.workspace_id # Check permission to create comments - user_permissions = await get_user_permissions(session, user.id, search_space_id) + user_permissions = await get_user_permissions(session, user.id, workspace_id) if not has_permission(user_permissions, Permission.COMMENTS_CREATE.value): raise HTTPException( status_code=403, - detail="You don't have permission to create comments in this search space", + detail="You don't have permission to create comments in this workspace", ) thread = parent_comment.message.thread @@ -583,7 +583,7 @@ async def create_reply( await session.flush() # Process mentions - returns map of user_id -> mention_id - mentions_map = await process_mentions(session, reply.id, content, search_space_id) + mentions_map = await process_mentions(session, reply.id, content, workspace_id) await session.commit() await session.refresh(reply) @@ -610,7 +610,7 @@ async def create_reply( author_avatar_url=user.avatar_url, author_email=user.email, content_preview=content_preview[:200], - search_space_id=search_space_id, + workspace_id=workspace_id, ) # Notify thread participants (excluding replier and mentioned users) @@ -635,7 +635,7 @@ async def create_reply( author_avatar_url=user.avatar_url, author_email=user.email, content_preview=content_preview[:200], - search_space_id=search_space_id, + workspace_id=workspace_id, ) author = AuthorResponse( @@ -699,7 +699,7 @@ async def update_comment( detail="You can only edit your own comments", ) - search_space_id = comment.message.thread.search_space_id + workspace_id = comment.message.thread.workspace_id # Get existing mentioned user IDs existing_result = await session.execute( @@ -712,12 +712,12 @@ async def update_comment( # Parse new mentions from updated content new_mention_uuids = set(parse_mentions(content)) - # Validate new mentions are search space members + # Validate new mentions are workspace members if new_mention_uuids: valid_result = await session.execute( - select(SearchSpaceMembership.user_id).filter( - SearchSpaceMembership.search_space_id == search_space_id, - SearchSpaceMembership.user_id.in_(new_mention_uuids), + select(WorkspaceMembership.user_id).filter( + WorkspaceMembership.workspace_id == workspace_id, + WorkspaceMembership.user_id.in_(new_mention_uuids), ) ) valid_new_mentions = set(valid_result.scalars().all()) @@ -777,7 +777,7 @@ async def update_comment( author_avatar_url=user.avatar_url, author_email=user.email, content_preview=content_preview[:200], - search_space_id=search_space_id, + workspace_id=workspace_id, ) author = AuthorResponse( @@ -833,8 +833,8 @@ async def delete_comment( is_author = comment.author_id == user.id # Check if user has COMMENTS_DELETE permission - search_space_id = comment.message.thread.search_space_id - user_permissions = await get_user_permissions(session, user.id, search_space_id) + workspace_id = comment.message.thread.workspace_id + user_permissions = await get_user_permissions(session, user.id, workspace_id) can_delete_any = has_permission(user_permissions, Permission.COMMENTS_DELETE.value) if not is_author and not can_delete_any: @@ -852,21 +852,21 @@ async def delete_comment( async def get_user_mentions( session: AsyncSession, auth: AuthContext, - search_space_id: int | None = None, + workspace_id: int | None = None, ) -> MentionListResponse: user = auth.user """ - Get mentions for the current user, optionally filtered by search space. + Get mentions for the current user, optionally filtered by workspace. Args: session: Database session user: The current authenticated user - search_space_id: Optional search space ID to filter mentions + workspace_id: Optional workspace ID to filter mentions Returns: MentionListResponse with mentions and total count """ - # Build query with joins for filtering by search_space_id + # Build query with joins for filtering by workspace_id query = ( select(ChatCommentMention) .join(ChatComment, ChatCommentMention.comment_id == ChatComment.id) @@ -880,18 +880,18 @@ async def get_user_mentions( .order_by(ChatCommentMention.created_at.desc()) ) - if search_space_id is not None: - query = query.filter(NewChatThread.search_space_id == search_space_id) + if workspace_id is not None: + query = query.filter(NewChatThread.workspace_id == workspace_id) result = await session.execute(query) mention_records = result.scalars().all() - # Fetch search space info for context (single query for all unique search spaces) + # Fetch workspace info for context (single query for all unique workspaces) thread_ids = {m.comment.message.thread_id for m in mention_records} if thread_ids: thread_result = await session.execute( select(NewChatThread) - .options(selectinload(NewChatThread.search_space)) + .options(selectinload(NewChatThread.workspace)) .filter(NewChatThread.id.in_(thread_ids)) ) threads_map = {t.id: t for t in thread_result.scalars().all()} @@ -903,7 +903,7 @@ async def get_user_mentions( comment = mention.comment message = comment.message thread = threads_map.get(message.thread_id) - search_space = thread.search_space if thread else None + workspace = thread.workspace if thread else None author = None if comment.author: @@ -934,8 +934,8 @@ async def get_user_mentions( thread_id=thread.id if thread else 0, thread_title=thread.title or "Untitled" if thread else "Unknown", message_id=message.id, - search_space_id=search_space.id if search_space else 0, - search_space_name=search_space.name if search_space else "Unknown", + workspace_id=workspace.id if workspace else 0, + workspace_name=workspace.name if workspace else "Unknown", ), ) ) diff --git a/surfsense_backend/app/services/confluence/kb_sync_service.py b/surfsense_backend/app/services/confluence/kb_sync_service.py index 7154637b4..478ac8739 100644 --- a/surfsense_backend/app/services/confluence/kb_sync_service.py +++ b/surfsense_backend/app/services/confluence/kb_sync_service.py @@ -28,7 +28,7 @@ class ConfluenceKBSyncService: space_id: str, body_content: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -40,7 +40,7 @@ class ConfluenceKBSyncService: try: unique_hash = generate_unique_identifier_hash( - DocumentType.CONFLUENCE_CONNECTOR, page_id, search_space_id + DocumentType.CONFLUENCE_CONNECTOR, page_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -55,7 +55,7 @@ class ConfluenceKBSyncService: page_content = f"# {page_title}\n\n{indexable_content}" - content_hash = generate_content_hash(page_content, search_space_id) + content_hash = generate_content_hash(page_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -85,7 +85,7 @@ class ConfluenceKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, updated_at=get_current_timestamp(), created_by_id=user_id, @@ -126,7 +126,7 @@ class ConfluenceKBSyncService: document_id: int, page_id: str, user_id: str, - search_space_id: int, + workspace_id: int, ) -> dict: from app.tasks.connector_indexers.base import ( get_current_timestamp, @@ -170,7 +170,7 @@ class ConfluenceKBSyncService: document.title = page_title document.content = summary_content - document.content_hash = generate_content_hash(page_content, search_space_id) + document.content_hash = generate_content_hash(page_content, workspace_id) document.embedding = summary_embedding from sqlalchemy.orm.attributes import flag_modified diff --git a/surfsense_backend/app/services/confluence/tool_metadata_service.py b/surfsense_backend/app/services/confluence/tool_metadata_service.py index a66725bc6..ac2447a1a 100644 --- a/surfsense_backend/app/services/confluence/tool_metadata_service.py +++ b/surfsense_backend/app/services/confluence/tool_metadata_service.py @@ -110,12 +110,12 @@ class ConfluenceToolMetadataService: ) return True - async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: + async def get_creation_context(self, workspace_id: int, user_id: str) -> dict: """Return context needed to create a new Confluence page. Fetches all connected accounts, and for the first healthy one fetches spaces. """ - connectors = await self._get_all_confluence_connectors(search_space_id, user_id) + connectors = await self._get_all_confluence_connectors(workspace_id, user_id) if not connectors: return {"error": "No Confluence account connected"} @@ -158,13 +158,13 @@ class ConfluenceToolMetadataService: } async def get_update_context( - self, search_space_id: int, user_id: str, page_ref: str + self, workspace_id: int, user_id: str, page_ref: str ) -> dict: """Return context needed to update an indexed Confluence page. Resolves the page from KB, then fetches current content and version from API. """ - document = await self._resolve_page(search_space_id, user_id, page_ref) + document = await self._resolve_page(workspace_id, user_id, page_ref) if not document: return { "error": f"Page '{page_ref}' not found in your synced Confluence pages. " @@ -232,10 +232,10 @@ class ConfluenceToolMetadataService: } async def get_deletion_context( - self, search_space_id: int, user_id: str, page_ref: str + self, workspace_id: int, user_id: str, page_ref: str ) -> dict: """Return context needed to delete a Confluence page (KB metadata only).""" - document = await self._resolve_page(search_space_id, user_id, page_ref) + document = await self._resolve_page(workspace_id, user_id, page_ref) if not document: return { "error": f"Page '{page_ref}' not found in your synced Confluence pages. " @@ -256,7 +256,7 @@ class ConfluenceToolMetadataService: } async def _resolve_page( - self, search_space_id: int, user_id: str, page_ref: str + self, workspace_id: int, user_id: str, page_ref: str ) -> Document | None: """Resolve a page from KB: page_title -> document.title.""" ref_lower = page_ref.lower() @@ -268,7 +268,7 @@ class ConfluenceToolMetadataService: ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.CONFLUENCE_CONNECTOR, SearchSourceConnector.user_id == user_id, or_( @@ -284,12 +284,12 @@ class ConfluenceToolMetadataService: return result.scalars().first() async def _get_all_confluence_connectors( - self, search_space_id: int, user_id: str + self, workspace_id: int, user_id: str ) -> list[SearchSourceConnector]: result = await self._db_session.execute( select(SearchSourceConnector).filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, diff --git a/surfsense_backend/app/services/connector_service.py b/surfsense_backend/app/services/connector_service.py index 2694a8e69..a13a9b3ea 100644 --- a/surfsense_backend/app/services/connector_service.py +++ b/surfsense_backend/app/services/connector_service.py @@ -26,11 +26,11 @@ from app.utils.perf import get_perf_logger class ConnectorService: - def __init__(self, session: AsyncSession, search_space_id: int | None = None): + def __init__(self, session: AsyncSession, workspace_id: int | None = None): self.session = session self.chunk_retriever = ChucksHybridSearchRetriever(session) self.document_retriever = DocumentHybridSearchRetriever(session) - self.search_space_id = search_space_id + self.workspace_id = workspace_id self.source_id_counter = ( 100000 # High starting value to avoid collisions with existing IDs ) @@ -40,22 +40,22 @@ class ConnectorService: async def initialize_counter(self): """ - Initialize the source_id_counter based on the total number of chunks for the search space. + Initialize the source_id_counter based on the total number of chunks for the workspace. This ensures unique IDs across different sessions. """ - if self.search_space_id: + if self.workspace_id: try: - # Count total chunks for documents belonging to this search space + # Count total chunks for documents belonging to this workspace result = await self.session.execute( select(func.count(Chunk.id)) .join(Document) - .filter(Document.search_space_id == self.search_space_id) + .filter(Document.workspace_id == self.workspace_id) ) chunk_count = result.scalar() or 0 self.source_id_counter = chunk_count + 1 print( - f"Initialized source_id_counter to {self.source_id_counter} for search space {self.search_space_id}" + f"Initialized source_id_counter to {self.source_id_counter} for workspace {self.workspace_id}" ) except Exception as e: print(f"Error initializing source_id_counter: {e!s}") @@ -65,7 +65,7 @@ class ConnectorService: async def search_crawled_urls( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -77,7 +77,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -87,7 +87,7 @@ class ConnectorService: """ crawled_urls_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="CRAWLED_URL", top_k=top_k, start_date=start_date, @@ -155,7 +155,7 @@ class ConnectorService: async def search_files( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -167,7 +167,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -177,7 +177,7 @@ class ConnectorService: """ files_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="FILE", top_k=top_k, start_date=start_date, @@ -221,7 +221,7 @@ class ConnectorService: async def _combined_rrf_search( self, query_text: str, - search_space_id: int, + workspace_id: int, document_type: str | list[str], top_k: int = 20, start_date: datetime | None = None, @@ -243,7 +243,7 @@ class ConnectorService: Args: query_text: The search query text - search_space_id: The search space ID to search within + workspace_id: The workspace ID to search within document_type: Document type(s) to filter (e.g., "FILE", "CRAWLED_URL", or a list for multi-type queries) top_k: Number of results to return @@ -289,7 +289,7 @@ class ConnectorService: search_kwargs = { "query_text": query_text, "top_k": retriever_top_k, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "document_type": resolved_type, "start_date": start_date, "end_date": end_date, @@ -388,7 +388,7 @@ class ConnectorService: time.perf_counter() - t0, len(combined_results), document_type, - search_space_id, + workspace_id, ) return combined_results @@ -458,20 +458,20 @@ class ConnectorService: async def get_connector_by_type( self, connector_type: SearchSourceConnectorType, - search_space_id: int, + workspace_id: int, ) -> SearchSourceConnector | None: """ - Get a connector by type for a specific search space + Get a connector by type for a specific workspace Args: connector_type: The connector type to retrieve - search_space_id: The search space ID to filter by + workspace_id: The workspace ID to filter by Returns: Optional[SearchSourceConnector]: The connector if found, None otherwise """ query = select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.connector_type == connector_type, ) @@ -479,14 +479,14 @@ class ConnectorService: return result.scalars().first() async def search_tavily( - self, user_query: str, search_space_id: int, top_k: int = 20 + self, user_query: str, workspace_id: int, top_k: int = 20 ) -> tuple: """ Search using Tavily API and return both the source information and documents Args: user_query: The user's query - search_space_id: The search space ID + workspace_id: The workspace ID top_k: Maximum number of results to return Returns: @@ -494,7 +494,7 @@ class ConnectorService: """ # Get Tavily connector configuration tavily_connector = await self.get_connector_by_type( - SearchSourceConnectorType.TAVILY_API, search_space_id + SearchSourceConnectorType.TAVILY_API, workspace_id ) if not tavily_connector: @@ -587,7 +587,7 @@ class ConnectorService: async def search_searxng( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, ) -> tuple: """Search using the platform SearXNG instance. @@ -614,7 +614,7 @@ class ConnectorService: async def search_baidu( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, ) -> tuple: """ @@ -625,7 +625,7 @@ class ConnectorService: Args: user_query: User's search query - search_space_id: Search space ID + workspace_id: Workspace ID top_k: Maximum number of results to return Returns: @@ -633,7 +633,7 @@ class ConnectorService: """ # Get Baidu connector configuration baidu_connector = await self.get_connector_by_type( - SearchSourceConnectorType.BAIDU_SEARCH_API, search_space_id + SearchSourceConnectorType.BAIDU_SEARCH_API, workspace_id ) if not baidu_connector: @@ -838,7 +838,7 @@ class ConnectorService: async def search_slack( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -850,7 +850,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -860,7 +860,7 @@ class ConnectorService: """ slack_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="SLACK_CONNECTOR", top_k=top_k, start_date=start_date, @@ -912,7 +912,7 @@ class ConnectorService: async def search_notion( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -924,7 +924,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -934,7 +934,7 @@ class ConnectorService: """ notion_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="NOTION_CONNECTOR", top_k=top_k, start_date=start_date, @@ -982,7 +982,7 @@ class ConnectorService: async def search_extension( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -994,7 +994,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1004,7 +1004,7 @@ class ConnectorService: """ extension_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="EXTENSION", top_k=top_k, start_date=start_date, @@ -1079,7 +1079,7 @@ class ConnectorService: async def search_youtube( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1091,7 +1091,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1101,7 +1101,7 @@ class ConnectorService: """ youtube_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="YOUTUBE_VIDEO", top_k=top_k, start_date=start_date, @@ -1160,7 +1160,7 @@ class ConnectorService: async def search_github( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1172,7 +1172,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1182,7 +1182,7 @@ class ConnectorService: """ github_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="GITHUB_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1219,7 +1219,7 @@ class ConnectorService: async def search_linear( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1231,7 +1231,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1241,7 +1241,7 @@ class ConnectorService: """ linear_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="LINEAR_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1319,7 +1319,7 @@ class ConnectorService: async def search_jira( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1331,7 +1331,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1341,7 +1341,7 @@ class ConnectorService: """ jira_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="JIRA_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1421,7 +1421,7 @@ class ConnectorService: async def search_google_calendar( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1433,7 +1433,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1443,7 +1443,7 @@ class ConnectorService: """ calendar_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="GOOGLE_CALENDAR_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1527,7 +1527,7 @@ class ConnectorService: async def search_airtable( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1539,7 +1539,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1549,7 +1549,7 @@ class ConnectorService: """ airtable_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="AIRTABLE_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1603,7 +1603,7 @@ class ConnectorService: async def search_google_gmail( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1615,7 +1615,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1625,7 +1625,7 @@ class ConnectorService: """ gmail_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="GOOGLE_GMAIL_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1703,7 +1703,7 @@ class ConnectorService: async def search_google_drive( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1715,7 +1715,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1725,7 +1725,7 @@ class ConnectorService: """ drive_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="GOOGLE_DRIVE_FILE", top_k=top_k, start_date=start_date, @@ -1803,7 +1803,7 @@ class ConnectorService: async def search_confluence( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1815,7 +1815,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1825,7 +1825,7 @@ class ConnectorService: """ confluence_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="CONFLUENCE_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1874,7 +1874,7 @@ class ConnectorService: async def search_clickup( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -1886,7 +1886,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -1896,7 +1896,7 @@ class ConnectorService: """ clickup_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="CLICKUP_CONNECTOR", top_k=top_k, start_date=start_date, @@ -1968,7 +1968,7 @@ class ConnectorService: async def search_linkup( self, user_query: str, - search_space_id: int, + workspace_id: int, mode: str = "standard", ) -> tuple: """ @@ -1976,7 +1976,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID + workspace_id: The workspace ID mode: Search depth mode, can be "standard" or "deep" Returns: @@ -1984,7 +1984,7 @@ class ConnectorService: """ # Get Linkup connector configuration linkup_connector = await self.get_connector_by_type( - SearchSourceConnectorType.LINKUP_API, search_space_id + SearchSourceConnectorType.LINKUP_API, workspace_id ) if not linkup_connector: @@ -2089,7 +2089,7 @@ class ConnectorService: async def search_discord( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2101,7 +2101,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2111,7 +2111,7 @@ class ConnectorService: """ discord_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="DISCORD_CONNECTOR", top_k=top_k, start_date=start_date, @@ -2164,7 +2164,7 @@ class ConnectorService: async def search_teams( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2176,7 +2176,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2186,7 +2186,7 @@ class ConnectorService: """ teams_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="TEAMS_CONNECTOR", top_k=top_k, start_date=start_date, @@ -2238,7 +2238,7 @@ class ConnectorService: async def search_luma( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2250,7 +2250,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2260,7 +2260,7 @@ class ConnectorService: """ luma_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="LUMA_CONNECTOR", top_k=top_k, start_date=start_date, @@ -2343,7 +2343,7 @@ class ConnectorService: async def search_elasticsearch( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2355,7 +2355,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2365,7 +2365,7 @@ class ConnectorService: """ elasticsearch_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="ELASTICSEARCH_CONNECTOR", top_k=top_k, start_date=start_date, @@ -2429,7 +2429,7 @@ class ConnectorService: async def search_notes( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2441,7 +2441,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2451,7 +2451,7 @@ class ConnectorService: """ notes_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="NOTE", top_k=top_k, start_date=start_date, @@ -2498,7 +2498,7 @@ class ConnectorService: async def search_bookstack( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2511,7 +2511,7 @@ class ConnectorService: Args: user_query: The user's query user_id: The user's ID - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2521,7 +2521,7 @@ class ConnectorService: """ bookstack_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="BOOKSTACK_CONNECTOR", top_k=top_k, start_date=start_date, @@ -2572,7 +2572,7 @@ class ConnectorService: async def search_circleback( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2584,7 +2584,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2594,7 +2594,7 @@ class ConnectorService: """ circleback_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="CIRCLEBACK", top_k=top_k, start_date=start_date, @@ -2672,7 +2672,7 @@ class ConnectorService: async def search_obsidian( self, user_query: str, - search_space_id: int, + workspace_id: int, top_k: int = 20, start_date: datetime | None = None, end_date: datetime | None = None, @@ -2684,7 +2684,7 @@ class ConnectorService: Args: user_query: The user's query - search_space_id: The search space ID to search in + workspace_id: The workspace ID to search in top_k: Maximum number of results to return start_date: Optional start date for filtering documents by updated_at end_date: Optional end date for filtering documents by updated_at @@ -2694,7 +2694,7 @@ class ConnectorService: """ obsidian_docs = await self._combined_rrf_search( query_text=user_query, - search_space_id=search_space_id, + workspace_id=workspace_id, document_type="OBSIDIAN_CONNECTOR", top_k=top_k, start_date=start_date, @@ -2766,60 +2766,60 @@ class ConnectorService: async def get_available_connectors( self, - search_space_id: int, + workspace_id: int, ) -> list[SearchSourceConnectorType]: """ - Get all available (enabled) connector types for a search space. + Get all available (enabled) connector types for a workspace. - Phase 1.4: results are cached per ``search_space_id`` for + Phase 1.4: results are cached per ``workspace_id`` for :data:`_DISCOVERY_TTL_SECONDS`. Cache key is independent of session identity — the cached value is plain data, safe to share across requests. Invalidate on connector add/update/delete via :func:`invalidate_connector_discovery_cache`. Args: - search_space_id: The search space ID + workspace_id: The workspace ID Returns: List of SearchSourceConnectorType enums for enabled connectors """ - cached = _get_cached_connectors(search_space_id) + cached = _get_cached_connectors(workspace_id) if cached is not None: return list(cached) query = ( select(SearchSourceConnector.connector_type) .filter( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, ) .distinct() ) result = await self.session.execute(query) connector_types = list(result.scalars().all()) - _set_cached_connectors(search_space_id, connector_types) + _set_cached_connectors(workspace_id, connector_types) return connector_types async def get_available_document_types( self, - search_space_id: int, + workspace_id: int, ) -> list[str]: """ - Get all document types that have at least one document in the search space. + Get all document types that have at least one document in the workspace. - Phase 1.4: cached per ``search_space_id`` for + Phase 1.4: cached per ``workspace_id`` for :data:`_DISCOVERY_TTL_SECONDS`. Invalidate via :func:`invalidate_connector_discovery_cache` when a connector finishes indexing new documents (or document types are otherwise added/removed). Args: - search_space_id: The search space ID + workspace_id: The workspace ID Returns: List of document type strings that have documents indexed """ - cached = _get_cached_doc_types(search_space_id) + cached = _get_cached_doc_types(workspace_id) if cached is not None: return list(cached) @@ -2828,12 +2828,12 @@ class ConnectorService: from app.db import Document query = select(distinct(Document.document_type)).filter( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, ) result = await self.session.execute(query) doc_types = [str(dt) for dt in result.scalars().all()] - _set_cached_doc_types(search_space_id, doc_types) + _set_cached_doc_types(workspace_id, doc_types) return doc_types @@ -2850,7 +2850,7 @@ class ConnectorService: # DB roundtrip with bounded staleness. # # Invalidation: connector mutation routes (create / update / delete) call -# ``invalidate_connector_discovery_cache(search_space_id)`` to clear the +# ``invalidate_connector_discovery_cache(workspace_id)`` to clear the # entry for the affected space. Multi-replica deployments still pay one # DB roundtrip per replica per TTL window, which is fine — staleness is # bounded and the alternative (cross-replica fanout) is not worth the @@ -2858,7 +2858,7 @@ class ConnectorService: _DISCOVERY_TTL_SECONDS: float = config.CONNECTOR_DISCOVERY_TTL_SECONDS -# Per-search-space caches. Keyed by ``search_space_id``; value is +# Per-workspace caches. Keyed by ``workspace_id``; value is # ``(expires_at_monotonic, payload)``. Plain dicts protected by a lock — # read-mostly workload, sub-microsecond contention. _connectors_cache: dict[int, tuple[float, list[SearchSourceConnectorType]]] = {} @@ -2867,55 +2867,55 @@ _cache_lock = Lock() def _get_cached_connectors( - search_space_id: int, + workspace_id: int, ) -> list[SearchSourceConnectorType] | None: if _DISCOVERY_TTL_SECONDS <= 0: return None with _cache_lock: - entry = _connectors_cache.get(search_space_id) + entry = _connectors_cache.get(workspace_id) if entry is None: return None expires_at, payload = entry if time.monotonic() >= expires_at: - _connectors_cache.pop(search_space_id, None) + _connectors_cache.pop(workspace_id, None) return None return payload def _set_cached_connectors( - search_space_id: int, payload: list[SearchSourceConnectorType] + workspace_id: int, payload: list[SearchSourceConnectorType] ) -> None: if _DISCOVERY_TTL_SECONDS <= 0: return expires_at = time.monotonic() + _DISCOVERY_TTL_SECONDS with _cache_lock: - _connectors_cache[search_space_id] = (expires_at, list(payload)) + _connectors_cache[workspace_id] = (expires_at, list(payload)) -def _get_cached_doc_types(search_space_id: int) -> list[str] | None: +def _get_cached_doc_types(workspace_id: int) -> list[str] | None: if _DISCOVERY_TTL_SECONDS <= 0: return None with _cache_lock: - entry = _doc_types_cache.get(search_space_id) + entry = _doc_types_cache.get(workspace_id) if entry is None: return None expires_at, payload = entry if time.monotonic() >= expires_at: - _doc_types_cache.pop(search_space_id, None) + _doc_types_cache.pop(workspace_id, None) return None return payload -def _set_cached_doc_types(search_space_id: int, payload: list[str]) -> None: +def _set_cached_doc_types(workspace_id: int, payload: list[str]) -> None: if _DISCOVERY_TTL_SECONDS <= 0: return expires_at = time.monotonic() + _DISCOVERY_TTL_SECONDS with _cache_lock: - _doc_types_cache[search_space_id] = (expires_at, list(payload)) + _doc_types_cache[workspace_id] = (expires_at, list(payload)) -def invalidate_connector_discovery_cache(search_space_id: int | None = None) -> None: - """Drop cached discovery results for ``search_space_id`` (or all spaces). +def invalidate_connector_discovery_cache(workspace_id: int | None = None) -> None: + """Drop cached discovery results for ``workspace_id`` (or all spaces). Connector CRUD routes / indexer pipelines call this when they mutate the rows backing :func:`ConnectorService.get_available_connectors` / @@ -2923,28 +2923,28 @@ def invalidate_connector_discovery_cache(search_space_id: int | None = None) -> useful in tests and on bulk imports. """ with _cache_lock: - if search_space_id is None: + if workspace_id is None: _connectors_cache.clear() _doc_types_cache.clear() else: - _connectors_cache.pop(search_space_id, None) - _doc_types_cache.pop(search_space_id, None) + _connectors_cache.pop(workspace_id, None) + _doc_types_cache.pop(workspace_id, None) -def _invalidate_connectors_only(search_space_id: int | None = None) -> None: +def _invalidate_connectors_only(workspace_id: int | None = None) -> None: with _cache_lock: - if search_space_id is None: + if workspace_id is None: _connectors_cache.clear() else: - _connectors_cache.pop(search_space_id, None) + _connectors_cache.pop(workspace_id, None) -def _invalidate_doc_types_only(search_space_id: int | None = None) -> None: +def _invalidate_doc_types_only(workspace_id: int | None = None) -> None: with _cache_lock: - if search_space_id is None: + if workspace_id is None: _doc_types_cache.clear() else: - _doc_types_cache.pop(search_space_id, None) + _doc_types_cache.pop(workspace_id, None) def _register_invalidation_listeners() -> None: @@ -2952,7 +2952,7 @@ def _register_invalidation_listeners() -> None: Listening on ``after_insert`` / ``after_update`` / ``after_delete`` means every successful INSERT/UPDATE/DELETE that goes through the ORM - invalidates the affected search space's cached discovery payload — + invalidates the affected workspace's cached discovery payload — no need to sprinkle ``invalidate_*`` calls across 30+ connector routes. Bulk operations that bypass the ORM (e.g. ``session.execute(insert(...))`` without a mapped object) still need @@ -2967,12 +2967,12 @@ def _register_invalidation_listeners() -> None: from app.db import Document, SearchSourceConnector def _connector_changed(_mapper, _connection, target) -> None: - sid = getattr(target, "search_space_id", None) + sid = getattr(target, "workspace_id", None) if sid is not None: _invalidate_connectors_only(int(sid)) def _document_changed(_mapper, _connection, target) -> None: - sid = getattr(target, "search_space_id", None) + sid = getattr(target, "workspace_id", None) if sid is not None: _invalidate_doc_types_only(int(sid)) diff --git a/surfsense_backend/app/services/dropbox/kb_sync_service.py b/surfsense_backend/app/services/dropbox/kb_sync_service.py index a25cc054d..92ad68a79 100644 --- a/surfsense_backend/app/services/dropbox/kb_sync_service.py +++ b/surfsense_backend/app/services/dropbox/kb_sync_service.py @@ -26,7 +26,7 @@ class DropboxKBSyncService: web_url: str | None, content: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -38,7 +38,7 @@ class DropboxKBSyncService: try: unique_hash = compute_identifier_hash( - DocumentType.DROPBOX_FILE.value, file_id, search_space_id + DocumentType.DROPBOX_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -56,7 +56,7 @@ class DropboxKBSyncService: if not indexable_content: indexable_content = f"Dropbox file: {file_name}" - content_hash = generate_content_hash(indexable_content, search_space_id) + content_hash = generate_content_hash(indexable_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -93,7 +93,7 @@ class DropboxKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, source_markdown=content, updated_at=get_current_timestamp(), diff --git a/surfsense_backend/app/services/export_service.py b/surfsense_backend/app/services/export_service.py index 9e6869fe1..2d1a8c2cf 100644 --- a/surfsense_backend/app/services/export_service.py +++ b/surfsense_backend/app/services/export_service.py @@ -81,7 +81,7 @@ class ExportResult: async def build_export_zip( session: AsyncSession, - search_space_id: int, + workspace_id: int, folder_id: int | None = None, ) -> ExportResult: """Build a ZIP archive of markdown documents preserving folder structure. @@ -93,13 +93,13 @@ async def build_export_zip( """ if folder_id is not None: folder = await session.get(Folder, folder_id) - if not folder or folder.search_space_id != search_space_id: + if not folder or folder.workspace_id != workspace_id: raise ValueError("Folder not found") target_folder_ids = set(await get_folder_subtree_ids(session, folder_id)) else: target_folder_ids = None - folder_query = select(Folder).where(Folder.search_space_id == search_space_id) + folder_query = select(Folder).where(Folder.workspace_id == workspace_id) if target_folder_ids is not None: folder_query = folder_query.where(Folder.id.in_(target_folder_ids)) folder_result = await session.execute(folder_query) @@ -109,7 +109,7 @@ async def build_export_zip( batch_size = 100 - base_doc_query = select(Document).where(Document.search_space_id == search_space_id) + base_doc_query = select(Document).where(Document.workspace_id == workspace_id) if target_folder_ids is not None: base_doc_query = base_doc_query.where(Document.folder_id.in_(target_folder_ids)) base_doc_query = base_doc_query.order_by(Document.id) diff --git a/surfsense_backend/app/services/folder_service.py b/surfsense_backend/app/services/folder_service.py index f5b608600..fd31e41d3 100644 --- a/surfsense_backend/app/services/folder_service.py +++ b/surfsense_backend/app/services/folder_service.py @@ -111,7 +111,7 @@ async def check_no_circular_reference( async def generate_folder_position( session: AsyncSession, - search_space_id: int, + workspace_id: int, parent_id: int | None, before_position: str | None = None, after_position: str | None = None, @@ -129,7 +129,7 @@ async def generate_folder_position( query = ( select(Folder.position) .where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.parent_id == parent_id if parent_id is not None else Folder.parent_id.is_(None), @@ -144,7 +144,7 @@ async def generate_folder_position( async def ensure_folder_hierarchy_with_depth_validation( session: AsyncSession, - search_space_id: int, + workspace_id: int, path_segments: list[dict], ) -> Folder: """Create or return a nested folder chain, validating depth at each step. @@ -163,7 +163,7 @@ async def ensure_folder_hierarchy_with_depth_validation( metadata = segment.get("metadata") stmt = select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.name == name, Folder.parent_id == parent_id if parent_id is not None @@ -175,11 +175,11 @@ async def ensure_folder_hierarchy_with_depth_validation( if folder is None: await validate_folder_depth(session, parent_id, subtree_depth=0) position = await generate_folder_position( - session, search_space_id, parent_id + session, workspace_id, parent_id ) folder = Folder( name=name, - search_space_id=search_space_id, + workspace_id=workspace_id, parent_id=parent_id, position=position, folder_metadata=metadata, diff --git a/surfsense_backend/app/services/gmail/kb_sync_service.py b/surfsense_backend/app/services/gmail/kb_sync_service.py index 192570339..5cb640ddb 100644 --- a/surfsense_backend/app/services/gmail/kb_sync_service.py +++ b/surfsense_backend/app/services/gmail/kb_sync_service.py @@ -28,7 +28,7 @@ class GmailKBSyncService: date_str: str, body_text: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, draft_id: str | None = None, ) -> dict: @@ -41,7 +41,7 @@ class GmailKBSyncService: try: unique_hash = generate_unique_identifier_hash( - DocumentType.GOOGLE_GMAIL_CONNECTOR, message_id, search_space_id + DocumentType.GOOGLE_GMAIL_CONNECTOR, message_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -62,7 +62,7 @@ class GmailKBSyncService: if not indexable_content: indexable_content = f"Gmail message: {subject}" - content_hash = generate_content_hash(indexable_content, search_space_id) + content_hash = generate_content_hash(indexable_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -103,7 +103,7 @@ class GmailKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, source_markdown=body_text, updated_at=get_current_timestamp(), diff --git a/surfsense_backend/app/services/gmail/tool_metadata_service.py b/surfsense_backend/app/services/gmail/tool_metadata_service.py index 4855c1cc9..40d80b41b 100644 --- a/surfsense_backend/app/services/gmail/tool_metadata_service.py +++ b/surfsense_backend/app/services/gmail/tool_metadata_service.py @@ -216,13 +216,13 @@ class GmailToolMetadataService: ) async def _get_accounts( - self, search_space_id: int, user_id: str + self, workspace_id: int, user_id: str ) -> list[GmailAccount]: result = await self._db_session.execute( select(SearchSourceConnector) .filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_( [ @@ -237,8 +237,8 @@ class GmailToolMetadataService: connectors = result.scalars().all() return [GmailAccount.from_connector(c) for c in connectors] - async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: - accounts = await self._get_accounts(search_space_id, user_id) + async def get_creation_context(self, workspace_id: int, user_id: str) -> dict: + accounts = await self._get_accounts(workspace_id, user_id) if not accounts: return { @@ -289,10 +289,10 @@ class GmailToolMetadataService: return {"accounts": accounts_with_status} async def get_update_context( - self, search_space_id: int, user_id: str, email_ref: str + self, workspace_id: int, user_id: str, email_ref: str ) -> dict: document, connector = await self._resolve_email( - search_space_id, user_id, email_ref + workspace_id, user_id, email_ref ) if not document or not connector: @@ -478,10 +478,10 @@ class GmailToolMetadataService: return text_content.strip() if text_content.strip() else None async def get_trash_context( - self, search_space_id: int, user_id: str, email_ref: str + self, workspace_id: int, user_id: str, email_ref: str ) -> dict: document, connector = await self._resolve_email( - search_space_id, user_id, email_ref + workspace_id, user_id, email_ref ) if not document or not connector: @@ -509,7 +509,7 @@ class GmailToolMetadataService: } async def _resolve_email( - self, search_space_id: int, user_id: str, email_ref: str + self, workspace_id: int, user_id: str, email_ref: str ) -> tuple[Document | None, SearchSourceConnector | None]: result = await self._db_session.execute( select(Document, SearchSourceConnector) @@ -519,7 +519,7 @@ class GmailToolMetadataService: ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type.in_( [ DocumentType.GOOGLE_GMAIL_CONNECTOR, diff --git a/surfsense_backend/app/services/google_calendar/kb_sync_service.py b/surfsense_backend/app/services/google_calendar/kb_sync_service.py index 495720a2d..cab6302a2 100644 --- a/surfsense_backend/app/services/google_calendar/kb_sync_service.py +++ b/surfsense_backend/app/services/google_calendar/kb_sync_service.py @@ -40,7 +40,7 @@ class GoogleCalendarKBSyncService: html_link: str | None, description: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -52,7 +52,7 @@ class GoogleCalendarKBSyncService: try: unique_hash = generate_unique_identifier_hash( - DocumentType.GOOGLE_CALENDAR_CONNECTOR, event_id, search_space_id + DocumentType.GOOGLE_CALENDAR_CONNECTOR, event_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -74,7 +74,7 @@ class GoogleCalendarKBSyncService: f"{description or ''}" ).strip() - content_hash = generate_content_hash(indexable_content, search_space_id) + content_hash = generate_content_hash(indexable_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -116,7 +116,7 @@ class GoogleCalendarKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, source_markdown=indexable_content, updated_at=get_current_timestamp(), @@ -165,7 +165,7 @@ class GoogleCalendarKBSyncService: document_id: int, event_id: str, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -260,7 +260,7 @@ class GoogleCalendarKBSyncService: document.title = event_summary document.content = summary_content document.content_hash = generate_content_hash( - indexable_content, search_space_id + indexable_content, workspace_id ) document.embedding = summary_embedding diff --git a/surfsense_backend/app/services/google_calendar/tool_metadata_service.py b/surfsense_backend/app/services/google_calendar/tool_metadata_service.py index 7e50ab039..2037f940b 100644 --- a/surfsense_backend/app/services/google_calendar/tool_metadata_service.py +++ b/surfsense_backend/app/services/google_calendar/tool_metadata_service.py @@ -240,13 +240,13 @@ class GoogleCalendarToolMetadataService: ) async def _get_accounts( - self, search_space_id: int, user_id: str + self, workspace_id: int, user_id: str ) -> list[GoogleCalendarAccount]: result = await self._db_session.execute( select(SearchSourceConnector) .filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(CALENDAR_CONNECTOR_TYPES), ) @@ -256,8 +256,8 @@ class GoogleCalendarToolMetadataService: connectors = result.scalars().all() return [GoogleCalendarAccount.from_connector(c) for c in connectors] - async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: - accounts = await self._get_accounts(search_space_id, user_id) + async def get_creation_context(self, workspace_id: int, user_id: str) -> dict: + accounts = await self._get_accounts(workspace_id, user_id) if not accounts: return { @@ -360,9 +360,9 @@ class GoogleCalendarToolMetadataService: } async def get_update_context( - self, search_space_id: int, user_id: str, event_ref: str + self, workspace_id: int, user_id: str, event_ref: str ) -> dict: - resolved = await self._resolve_event(search_space_id, user_id, event_ref) + resolved = await self._resolve_event(workspace_id, user_id, event_ref) if not resolved: return { "error": ( @@ -449,12 +449,12 @@ class GoogleCalendarToolMetadataService: } async def get_deletion_context( - self, search_space_id: int, user_id: str, event_ref: str + self, workspace_id: int, user_id: str, event_ref: str ) -> dict: - resolved = await self._resolve_event(search_space_id, user_id, event_ref) + resolved = await self._resolve_event(workspace_id, user_id, event_ref) if not resolved: live_resolved = await self._resolve_live_event( - search_space_id, user_id, event_ref + workspace_id, user_id, event_ref ) if not live_resolved: return { @@ -495,7 +495,7 @@ class GoogleCalendarToolMetadataService: } async def _resolve_event( - self, search_space_id: int, user_id: str, event_ref: str + self, workspace_id: int, user_id: str, event_ref: str ) -> tuple[Document, SearchSourceConnector] | None: result = await self._db_session.execute( select(Document, SearchSourceConnector) @@ -505,7 +505,7 @@ class GoogleCalendarToolMetadataService: ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type.in_(CALENDAR_DOCUMENT_TYPES), SearchSourceConnector.user_id == user_id, or_( @@ -526,13 +526,13 @@ class GoogleCalendarToolMetadataService: return None async def _resolve_live_event( - self, search_space_id: int, user_id: str, event_ref: str + self, workspace_id: int, user_id: str, event_ref: str ) -> tuple[SearchSourceConnector, dict] | None: result = await self._db_session.execute( select(SearchSourceConnector) .filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_(CALENDAR_CONNECTOR_TYPES), ) diff --git a/surfsense_backend/app/services/google_drive/kb_sync_service.py b/surfsense_backend/app/services/google_drive/kb_sync_service.py index 30fbc14f2..4cf93c228 100644 --- a/surfsense_backend/app/services/google_drive/kb_sync_service.py +++ b/surfsense_backend/app/services/google_drive/kb_sync_service.py @@ -26,7 +26,7 @@ class GoogleDriveKBSyncService: web_view_link: str | None, content: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -38,7 +38,7 @@ class GoogleDriveKBSyncService: try: unique_hash = generate_unique_identifier_hash( - DocumentType.GOOGLE_DRIVE_FILE, file_id, search_space_id + DocumentType.GOOGLE_DRIVE_FILE, file_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -58,7 +58,7 @@ class GoogleDriveKBSyncService: f"Google Drive file: {file_name} (type: {mime_type})" ) - content_hash = generate_content_hash(indexable_content, search_space_id) + content_hash = generate_content_hash(indexable_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -95,7 +95,7 @@ class GoogleDriveKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, source_markdown=content, updated_at=get_current_timestamp(), diff --git a/surfsense_backend/app/services/google_drive/tool_metadata_service.py b/surfsense_backend/app/services/google_drive/tool_metadata_service.py index 0f654bc78..1be8606f6 100644 --- a/surfsense_backend/app/services/google_drive/tool_metadata_service.py +++ b/surfsense_backend/app/services/google_drive/tool_metadata_service.py @@ -103,8 +103,8 @@ class GoogleDriveToolMetadataService: return inner, None return data, None - async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: - accounts = await self._get_google_drive_accounts(search_space_id, user_id) + async def get_creation_context(self, workspace_id: int, user_id: str) -> dict: + accounts = await self._get_google_drive_accounts(workspace_id, user_id) if not accounts: return { @@ -132,7 +132,7 @@ class GoogleDriveToolMetadataService: } async def get_trash_context( - self, search_space_id: int, user_id: str, file_name: str + self, workspace_id: int, user_id: str, file_name: str ) -> dict: result = await self._db_session.execute( select(Document) @@ -141,7 +141,7 @@ class GoogleDriveToolMetadataService: ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.GOOGLE_DRIVE_FILE, func.lower(Document.title) == func.lower(file_name), SearchSourceConnector.user_id == user_id, @@ -198,13 +198,13 @@ class GoogleDriveToolMetadataService: } async def _get_google_drive_accounts( - self, search_space_id: int, user_id: str + self, workspace_id: int, user_id: str ) -> list[GoogleDriveAccount]: result = await self._db_session.execute( select(SearchSourceConnector) .filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type.in_( [ diff --git a/surfsense_backend/app/services/linear/kb_sync_service.py b/surfsense_backend/app/services/linear/kb_sync_service.py index 3b8def6c3..49deb0113 100644 --- a/surfsense_backend/app/services/linear/kb_sync_service.py +++ b/surfsense_backend/app/services/linear/kb_sync_service.py @@ -34,7 +34,7 @@ class LinearKBSyncService: issue_url: str | None, description: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -46,7 +46,7 @@ class LinearKBSyncService: try: unique_hash = generate_unique_identifier_hash( - DocumentType.LINEAR_CONNECTOR, issue_id, search_space_id + DocumentType.LINEAR_CONNECTOR, issue_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -68,7 +68,7 @@ class LinearKBSyncService: f"# {issue_identifier}: {issue_title}\n\n{indexable_content}" ) - content_hash = generate_content_hash(issue_content, search_space_id) + content_hash = generate_content_hash(issue_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -107,7 +107,7 @@ class LinearKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, updated_at=get_current_timestamp(), created_by_id=user_id, @@ -155,7 +155,7 @@ class LinearKBSyncService: document_id: int, issue_id: str, user_id: str, - search_space_id: int, + workspace_id: int, ) -> dict: """Re-index a Linear issue document after it has been updated via the API. @@ -163,7 +163,7 @@ class LinearKBSyncService: document_id: The KB document ID to update. issue_id: The Linear issue UUID to fetch fresh content from. user_id: Used to select the correct LLM configuration. - search_space_id: Used to select the correct LLM configuration. + workspace_id: Used to select the correct LLM configuration. Returns: dict with 'status': 'success' | 'not_indexed' | 'error'. @@ -214,7 +214,7 @@ class LinearKBSyncService: document.title = f"{issue_identifier}: {issue_title}" document.content = summary_content document.content_hash = generate_content_hash( - issue_content, search_space_id + issue_content, workspace_id ) document.embedding = summary_embedding from sqlalchemy.orm.attributes import flag_modified diff --git a/surfsense_backend/app/services/linear/tool_metadata_service.py b/surfsense_backend/app/services/linear/tool_metadata_service.py index 9848534b0..10033454e 100644 --- a/surfsense_backend/app/services/linear/tool_metadata_service.py +++ b/surfsense_backend/app/services/linear/tool_metadata_service.py @@ -90,7 +90,7 @@ class LinearToolMetadataService: def __init__(self, db_session: AsyncSession): self._db_session = db_session - async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: + async def get_creation_context(self, workspace_id: int, user_id: str) -> dict: """Return context needed to create a new Linear issue. Fetches all connected Linear workspaces, and for each one fetches @@ -99,7 +99,7 @@ class LinearToolMetadataService: Returns a dict with key: workspaces (each entry has id, name, organization_name, teams, priorities). Returns a dict with key 'error' on failure. """ - connectors = await self._get_all_linear_connectors(search_space_id, user_id) + connectors = await self._get_all_linear_connectors(workspace_id, user_id) if not connectors: return {"error": "No Linear account connected"} @@ -155,7 +155,7 @@ class LinearToolMetadataService: return {"workspaces": workspaces} async def get_update_context( - self, search_space_id: int, user_id: str, issue_ref: str + self, workspace_id: int, user_id: str, issue_ref: str ) -> dict: """Return context needed to update an indexed Linear issue. @@ -166,7 +166,7 @@ class LinearToolMetadataService: Returns a dict with keys: workspace, priorities, issue, team. Returns a dict with key 'error' if the issue is not found or API fails. """ - document = await self._resolve_issue(search_space_id, user_id, issue_ref) + document = await self._resolve_issue(workspace_id, user_id, issue_ref) if not document: return { "error": f"Issue '{issue_ref}' not found in your synced Linear issues. " @@ -241,7 +241,7 @@ class LinearToolMetadataService: } async def get_delete_context( - self, search_space_id: int, user_id: str, issue_ref: str + self, workspace_id: int, user_id: str, issue_ref: str ) -> dict: """Return context needed to archive an indexed Linear issue. @@ -250,7 +250,7 @@ class LinearToolMetadataService: Returns a dict with keys: workspace, issue. Returns a dict with key 'error' if the issue is not found. """ - document = await self._resolve_issue(search_space_id, user_id, issue_ref) + document = await self._resolve_issue(workspace_id, user_id, issue_ref) if not document: return { "error": f"Issue '{issue_ref}' not found in your synced Linear issues. " @@ -332,7 +332,7 @@ class LinearToolMetadataService: return result.get("data", {}).get("issue") async def _resolve_issue( - self, search_space_id: int, user_id: str, issue_ref: str + self, workspace_id: int, user_id: str, issue_ref: str ) -> Document | None: """Resolve an issue from the KB using a 3-step fallback. @@ -348,7 +348,7 @@ class LinearToolMetadataService: ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.LINEAR_CONNECTOR, SearchSourceConnector.user_id == user_id, or_( @@ -368,13 +368,13 @@ class LinearToolMetadataService: return result.scalars().first() async def _get_all_linear_connectors( - self, search_space_id: int, user_id: str + self, workspace_id: int, user_id: str ) -> list[SearchSourceConnector]: - """Fetch all Linear connectors for the given search space and user.""" + """Fetch all Linear connectors for the given workspace and user.""" result = await self._db_session.execute( select(SearchSourceConnector).filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR, diff --git a/surfsense_backend/app/services/llm_service.py b/surfsense_backend/app/services/llm_service.py index e535d0150..3991c3e89 100644 --- a/surfsense_backend/app/services/llm_service.py +++ b/surfsense_backend/app/services/llm_service.py @@ -9,7 +9,7 @@ from sqlalchemy.future import select from sqlalchemy.orm import selectinload from app.config import config -from app.db import Model, SearchSpace +from app.db import Model, Workspace from app.services.auto_model_pin_service import ( auto_model_candidates, choose_auto_model_candidate, @@ -150,7 +150,7 @@ def _chat_litellm_from_resolved( async def _get_db_model( session: AsyncSession, model_id: int, - search_space: SearchSpace, + workspace: Workspace, ) -> Model | None: result = await session.execute( select(Model) @@ -161,9 +161,9 @@ async def _get_db_model( if not model or not model.connection or not model.connection.enabled: return None conn = model.connection - if conn.search_space_id and conn.search_space_id != search_space.id: + if conn.workspace_id and conn.workspace_id != workspace.id: return None - if conn.user_id and conn.user_id != search_space.user_id: + if conn.user_id and conn.user_id != workspace.user_id: return None return model @@ -269,48 +269,48 @@ async def validate_llm_config( return False, error_msg -async def get_search_space_llm_instance( +async def get_workspace_llm_instance( session: AsyncSession, - search_space_id: int, + workspace_id: int, role: str, disable_streaming: bool = False, ) -> ChatLiteLLM | ChatLiteLLMRouter | None: """ - Get a ChatLiteLLM instance for a specific search space and role. + Get a ChatLiteLLM instance for a specific workspace and role. - LLM preferences are stored at the search space level and shared by all members. + LLM preferences are stored at the workspace level and shared by all members. If Auto mode (ID 0) is configured, returns a ChatLiteLLMRouter that uses LiteLLM Router for automatic load balancing across available providers. Args: session: Database session - search_space_id: Search Space ID + workspace_id: Workspace ID role: LLM role ('agent') Returns: ChatLiteLLM or ChatLiteLLMRouter instance, or None if not found """ try: - # Get the search space with its LLM preferences + # Get the workspace with its LLM preferences result = await session.execute( - select(SearchSpace).where(SearchSpace.id == search_space_id) + select(Workspace).where(Workspace.id == workspace_id) ) - search_space = result.scalars().first() + workspace = result.scalars().first() - if not search_space: - logger.error(f"Search space {search_space_id} not found") + if not workspace: + logger.error(f"Workspace {workspace_id} not found") return None # Get the appropriate model binding ID based on role if role == LLMRole.AGENT: - llm_config_id = search_space.chat_model_id + llm_config_id = workspace.chat_model_id else: logger.error(f"Invalid LLM role: {role}") return None if llm_config_id is None: - logger.error(f"No {role} LLM configured for search space {search_space_id}") + logger.error(f"No {role} LLM configured for workspace {workspace_id}") return None # Auto mode resolves to one concrete global or BYOK model from the @@ -318,15 +318,15 @@ async def get_search_space_llm_instance( if is_auto_mode(llm_config_id): candidates = await auto_model_candidates( session, - search_space_id=search_space_id, - user_id=search_space.user_id, + workspace_id=workspace_id, + user_id=workspace.user_id, capability="chat", ) if not candidates: logger.error("No chat-capable models available for Auto mode") return None llm_config_id = int( - choose_auto_model_candidate(candidates, search_space_id)["id"] + choose_auto_model_candidate(candidates, workspace_id)["id"] ) # Check if this is a global virtual model (negative ID) @@ -356,10 +356,10 @@ async def get_search_space_llm_instance( return SanitizedChatLiteLLM(**litellm_kwargs) - model = await _get_db_model(session, llm_config_id, search_space) + model = await _get_db_model(session, llm_config_id, workspace) if not model or not _has_capability(model, "chat"): logger.error( - f"Chat model {llm_config_id} not found in search space {search_space_id}" + f"Chat model {llm_config_id} not found in workspace {workspace_id}" ) return None @@ -377,27 +377,27 @@ async def get_search_space_llm_instance( except Exception as e: logger.error( - f"Error getting LLM instance for search space {search_space_id}, role {role}: {e!s}" + f"Error getting LLM instance for workspace {workspace_id}, role {role}: {e!s}" ) return None async def get_agent_llm( - session: AsyncSession, search_space_id: int, disable_streaming: bool = False + session: AsyncSession, workspace_id: int, disable_streaming: bool = False ) -> ChatLiteLLM | ChatLiteLLMRouter | None: - """Get the search space's chat model instance.""" - return await get_search_space_llm_instance( + """Get the workspace's chat model instance.""" + return await get_workspace_llm_instance( session, - search_space_id, + workspace_id, LLMRole.AGENT, disable_streaming=disable_streaming, ) async def get_vision_llm( - session: AsyncSession, search_space_id: int + session: AsyncSession, workspace_id: int ) -> ChatLiteLLM | ChatLiteLLMRouter | None: - """Get the search space's vision LLM instance for screenshot analysis. + """Get the workspace's vision LLM instance for screenshot analysis. Resolves from the new connection/model role bindings: - Auto mode (ID 0): unified global/BYOK model candidate selection @@ -405,7 +405,7 @@ async def get_vision_llm( - DB (positive ID): Model + Connection tables Premium global configs are wrapped in :class:`QuotaCheckedVisionLLM` - so each ``ainvoke`` debits the search-space owner's premium credit + so each ``ainvoke`` debits the workspace owner's premium credit pool. User-owned BYOK configs and free global configs are returned unwrapped — they don't consume premium credit (issue M). """ @@ -413,17 +413,17 @@ async def get_vision_llm( try: result = await session.execute( - select(SearchSpace).where(SearchSpace.id == search_space_id) + select(Workspace).where(Workspace.id == workspace_id) ) - search_space = result.scalars().first() - if not search_space: - logger.error(f"Search space {search_space_id} not found") + workspace = result.scalars().first() + if not workspace: + logger.error(f"Workspace {workspace_id} not found") return None - owner_user_id = search_space.user_id + owner_user_id = workspace.user_id # Prefer the selected chat model when it is vision-capable. - chat_model_id = search_space.chat_model_id + chat_model_id = workspace.chat_model_id if chat_model_id and chat_model_id != AUTO_MODE_ID: if chat_model_id < 0: chat_model = get_global_model(chat_model_id) @@ -442,7 +442,7 @@ async def get_vision_llm( return SanitizedChatLiteLLM(**litellm_kwargs) else: - chat_model = await _get_db_model(session, chat_model_id, search_space) + chat_model = await _get_db_model(session, chat_model_id, workspace) if chat_model and _has_capability(chat_model, "vision"): _, litellm_kwargs = _chat_litellm_from_resolved( conn=chat_model.connection, @@ -454,15 +454,15 @@ async def get_vision_llm( return SanitizedChatLiteLLM(**litellm_kwargs) - config_id = search_space.vision_model_id + config_id = workspace.vision_model_id if config_id is None: - logger.error(f"No vision LLM configured for search space {search_space_id}") + logger.error(f"No vision LLM configured for workspace {workspace_id}") return None if config_id == AUTO_MODE_ID: candidates = await auto_model_candidates( session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=owner_user_id, capability="vision", ) @@ -470,7 +470,7 @@ async def get_vision_llm( logger.error("No vision-capable models available for Auto mode") return None config_id = int( - choose_auto_model_candidate(candidates, search_space_id)["id"] + choose_auto_model_candidate(candidates, workspace_id)["id"] ) if config_id < 0: @@ -504,7 +504,7 @@ async def get_vision_llm( return QuotaCheckedVisionLLM( inner_llm, user_id=owner_user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, billing_tier=billing_tier, base_model=model_string, quota_reserve_tokens=global_model.get("catalog", {}).get( @@ -513,10 +513,10 @@ async def get_vision_llm( ) return inner_llm - model = await _get_db_model(session, config_id, search_space) + model = await _get_db_model(session, config_id, workspace) if not model or not _has_capability(model, "vision"): logger.error( - f"Vision model {config_id} not found in search space {search_space_id}" + f"Vision model {config_id} not found in workspace {workspace_id}" ) return None @@ -533,7 +533,7 @@ async def get_vision_llm( except Exception as e: logger.error( - f"Error getting vision LLM for search space {search_space_id}: {e!s}" + f"Error getting vision LLM for workspace {workspace_id}: {e!s}" ) return None @@ -551,7 +551,7 @@ def get_planner_llm() -> ChatLiteLLM | None: This helper reads from ``config.GLOBAL_LLM_CONFIGS`` (loaded at import time from ``global_llm_config.yaml``) so it has no DB cost and can be called synchronously from middleware/factory code. It returns the same - instance shape as the global path of ``get_search_space_llm_instance``. + instance shape as the global path of ``get_workspace_llm_instance``. Callers MUST fall back to their chat LLM when this returns ``None`` so deployments without a planner config keep working unchanged. diff --git a/surfsense_backend/app/services/memory/service.py b/surfsense_backend/app/services/memory/service.py index feca000c9..31169e55c 100644 --- a/surfsense_backend/app/services/memory/service.py +++ b/surfsense_backend/app/services/memory/service.py @@ -11,7 +11,7 @@ from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.db import SearchSpace, User +from app.db import Workspace, User from app.services.memory.document import parse_memory_document, render_memory_document from app.services.memory.rewrite import forced_rewrite from app.services.memory.schemas import MemoryLimits @@ -90,7 +90,7 @@ async def _load_target( scope: MemoryScope | str, target_id: str | int | UUID, session: AsyncSession, -) -> User | SearchSpace | None: +) -> User | Workspace | None: normalized = _normalize_scope(scope) if normalized is MemoryScope.USER: result = await session.execute( @@ -98,18 +98,18 @@ async def _load_target( ) return result.scalars().first() result = await session.execute( - select(SearchSpace).where(SearchSpace.id == int(target_id)) + select(Workspace).where(Workspace.id == int(target_id)) ) return result.scalars().first() -def _get_memory(target: User | SearchSpace, scope: MemoryScope) -> str: +def _get_memory(target: User | Workspace, scope: MemoryScope) -> str: if scope is MemoryScope.USER: return getattr(target, "memory_md", None) or "" return getattr(target, "shared_memory_md", None) or "" -def _set_memory(target: User | SearchSpace, scope: MemoryScope, content: str) -> None: +def _set_memory(target: User | Workspace, scope: MemoryScope, content: str) -> None: if scope is MemoryScope.USER: target.memory_md = content else: @@ -150,7 +150,7 @@ async def save_memory( status="error", message="User not found." if normalized is MemoryScope.USER - else "Search space not found.", + else "Workspace not found.", ) old_memory = _get_memory(target, normalized) diff --git a/surfsense_backend/app/services/notion/kb_sync_service.py b/surfsense_backend/app/services/notion/kb_sync_service.py index ee85daf41..e9d81ac4f 100644 --- a/surfsense_backend/app/services/notion/kb_sync_service.py +++ b/surfsense_backend/app/services/notion/kb_sync_service.py @@ -25,7 +25,7 @@ class NotionKBSyncService: page_url: str | None, content: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -37,7 +37,7 @@ class NotionKBSyncService: try: unique_hash = generate_unique_identifier_hash( - DocumentType.NOTION_CONNECTOR, page_id, search_space_id + DocumentType.NOTION_CONNECTOR, page_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -57,7 +57,7 @@ class NotionKBSyncService: markdown_content = f"# Notion Page: {page_title}\n\n{indexable_content}" - content_hash = generate_content_hash(markdown_content, search_space_id) + content_hash = generate_content_hash(markdown_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -93,7 +93,7 @@ class NotionKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, updated_at=get_current_timestamp(), created_by_id=user_id, @@ -141,7 +141,7 @@ class NotionKBSyncService: document_id: int, appended_content: str, user_id: str, - search_space_id: int, + workspace_id: int, appended_block_ids: list[str] | None = None, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -233,7 +233,7 @@ class NotionKBSyncService: logger.debug("Updating document fields") document.content = summary_content - document.content_hash = generate_content_hash(full_content, search_space_id) + document.content_hash = generate_content_hash(full_content, workspace_id) document.embedding = summary_embedding document.document_metadata = { **document.document_metadata, diff --git a/surfsense_backend/app/services/notion/tool_metadata_service.py b/surfsense_backend/app/services/notion/tool_metadata_service.py index 19dc1fd89..efb09c7c2 100644 --- a/surfsense_backend/app/services/notion/tool_metadata_service.py +++ b/surfsense_backend/app/services/notion/tool_metadata_service.py @@ -74,8 +74,8 @@ class NotionToolMetadataService: def __init__(self, db_session: AsyncSession): self._db_session = db_session - async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: - accounts = await self._get_notion_accounts(search_space_id, user_id) + async def get_creation_context(self, workspace_id: int, user_id: str) -> dict: + accounts = await self._get_notion_accounts(workspace_id, user_id) if not accounts: return { @@ -85,7 +85,7 @@ class NotionToolMetadataService: } parent_pages = await self._get_parent_pages_by_account( - search_space_id, accounts + workspace_id, accounts ) accounts_with_status = [] @@ -123,7 +123,7 @@ class NotionToolMetadataService: } async def get_update_context( - self, search_space_id: int, user_id: str, page_title: str + self, workspace_id: int, user_id: str, page_title: str ) -> dict: result = await self._db_session.execute( select(Document) @@ -132,7 +132,7 @@ class NotionToolMetadataService: ) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.NOTION_CONNECTOR, func.lower(Document.title) == func.lower(page_title), SearchSourceConnector.user_id == user_id, @@ -202,18 +202,18 @@ class NotionToolMetadataService: } async def get_delete_context( - self, search_space_id: int, user_id: str, page_title: str + self, workspace_id: int, user_id: str, page_title: str ) -> dict: - return await self.get_update_context(search_space_id, user_id, page_title) + return await self.get_update_context(workspace_id, user_id, page_title) async def _get_notion_accounts( - self, search_space_id: int, user_id: str + self, workspace_id: int, user_id: str ) -> list[NotionAccount]: result = await self._db_session.execute( select(SearchSourceConnector) .filter( and_( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.NOTION_CONNECTOR, @@ -243,7 +243,7 @@ class NotionToolMetadataService: return True async def _get_parent_pages_by_account( - self, search_space_id: int, accounts: list[NotionAccount] + self, workspace_id: int, accounts: list[NotionAccount] ) -> dict: parent_pages = {} @@ -252,7 +252,7 @@ class NotionToolMetadataService: select(Document) .filter( and_( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.connector_id == account.id, Document.document_type == DocumentType.NOTION_CONNECTOR, ) diff --git a/surfsense_backend/app/services/obsidian_plugin_indexer.py b/surfsense_backend/app/services/obsidian_plugin_indexer.py index cd05d7935..2462093f8 100644 --- a/surfsense_backend/app/services/obsidian_plugin_indexer.py +++ b/surfsense_backend/app/services/obsidian_plugin_indexer.py @@ -24,7 +24,7 @@ Design notes The plugin's content hash and the backend's ``content_hash`` are computed differently (plugin uses raw SHA-256 of the markdown body; backend salts -with ``search_space_id``). We persist the plugin's hash in +with ``workspace_id``). We persist the plugin's hash in ``document_metadata['plugin_content_hash']`` so the manifest endpoint can return what the plugin sent — that's the only number the plugin can compare without re-downloading content. @@ -217,7 +217,7 @@ async def _resolve_attachment_vision_llm( session: AsyncSession, *, connector: SearchSourceConnector, - search_space_id: int, + workspace_id: int, payload: NotePayload, ): """Match connector indexers: only fetch vision LLM for image attachments @@ -231,7 +231,7 @@ async def _resolve_attachment_vision_llm( from app.services.llm_service import get_vision_llm - return await get_vision_llm(session, search_space_id) + return await get_vision_llm(session, workspace_id) def _require_extracted_attachment_content( @@ -253,7 +253,7 @@ def _require_extracted_attachment_content( async def _find_existing_document( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, vault_id: str, path: str, ) -> Document | None: @@ -261,7 +261,7 @@ async def _find_existing_document( uid_hash = generate_unique_identifier_hash( DocumentType.OBSIDIAN_CONNECTOR, unique_id, - search_space_id, + workspace_id, ) result = await session.execute( select(Document).where(Document.unique_identifier_hash == uid_hash) @@ -287,11 +287,11 @@ async def upsert_note( a skip-because-unchanged hit). """ vault_name: str = (connector.config or {}).get("vault_name") or "Vault" - search_space_id = connector.search_space_id + workspace_id = connector.workspace_id existing = await _find_existing_document( session, - search_space_id=search_space_id, + workspace_id=workspace_id, vault_id=payload.vault_id, path=payload.path, ) @@ -324,7 +324,7 @@ async def upsert_note( vision_llm = await _resolve_attachment_vision_llm( session, connector=connector, - search_space_id=search_space_id, + workspace_id=workspace_id, payload=payload, ) content_for_index, etl_meta = await _extract_binary_attachment_markdown( @@ -353,7 +353,7 @@ async def upsert_note( source_markdown=document_string, unique_id=_vault_path_unique_id(payload.vault_id, payload.path), document_type=DocumentType.OBSIDIAN_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector.id, created_by_id=str(user_id), metadata=metadata, @@ -387,11 +387,11 @@ async def rename_note( the new path). """ vault_name: str = (connector.config or {}).get("vault_name") or "Vault" - search_space_id = connector.search_space_id + workspace_id = connector.workspace_id existing = await _find_existing_document( session, - search_space_id=search_space_id, + workspace_id=workspace_id, vault_id=vault_id, path=old_path, ) @@ -402,7 +402,7 @@ async def rename_note( new_uid_hash = generate_unique_identifier_hash( DocumentType.OBSIDIAN_CONNECTOR, new_unique_id, - search_space_id, + workspace_id, ) collision = await session.execute( @@ -461,7 +461,7 @@ async def delete_note( """ existing = await _find_existing_document( session, - search_space_id=connector.search_space_id, + workspace_id=connector.workspace_id, vault_id=vault_id, path=path, ) @@ -500,7 +500,7 @@ async def merge_obsidian_connectors( return target_vault_id = (target.config or {}).get("vault_id") - target_search_space_id = target.search_space_id + target_workspace_id = target.workspace_id if not target_vault_id: raise RuntimeError("merge target is missing vault_id") @@ -539,13 +539,13 @@ async def merge_obsidian_connectors( new_uid_hash = generate_unique_identifier_hash( DocumentType.OBSIDIAN_CONNECTOR, new_unique_id, - target_search_space_id, + target_workspace_id, ) meta["vault_id"] = target_vault_id meta["connector_id"] = target.id doc.document_metadata = meta doc.connector_id = target.id - doc.search_space_id = target_search_space_id + doc.workspace_id = target_workspace_id doc.unique_identifier_hash = new_uid_hash target_paths.add(path) @@ -571,7 +571,7 @@ async def get_manifest( result = await session.execute( select(Document).where( and_( - Document.search_space_id == connector.search_space_id, + Document.workspace_id == connector.workspace_id, Document.connector_id == connector.id, Document.document_type == DocumentType.OBSIDIAN_CONNECTOR, ) diff --git a/surfsense_backend/app/services/onedrive/kb_sync_service.py b/surfsense_backend/app/services/onedrive/kb_sync_service.py index 2bfea6ef4..468253188 100644 --- a/surfsense_backend/app/services/onedrive/kb_sync_service.py +++ b/surfsense_backend/app/services/onedrive/kb_sync_service.py @@ -27,7 +27,7 @@ class OneDriveKBSyncService: web_url: str | None, content: str | None, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> dict: from app.tasks.connector_indexers.base import ( @@ -39,7 +39,7 @@ class OneDriveKBSyncService: try: unique_hash = compute_identifier_hash( - DocumentType.ONEDRIVE_FILE.value, file_id, search_space_id + DocumentType.ONEDRIVE_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier( @@ -57,7 +57,7 @@ class OneDriveKBSyncService: if not indexable_content: indexable_content = f"OneDrive file: {file_name} (type: {mime_type})" - content_hash = generate_content_hash(indexable_content, search_space_id) + content_hash = generate_content_hash(indexable_content, workspace_id) with self.db_session.no_autoflush: dup = await check_duplicate_document_by_hash( @@ -94,7 +94,7 @@ class OneDriveKBSyncService: content_hash=content_hash, unique_identifier_hash=unique_hash, embedding=summary_embedding, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, source_markdown=content, updated_at=get_current_timestamp(), diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py index 11c57e969..303c9abe8 100644 --- a/surfsense_backend/app/services/public_chat_service.py +++ b/surfsense_backend/app/services/public_chat_service.py @@ -31,7 +31,7 @@ from app.db import ( PodcastStatus, PublicChatSnapshot, Report, - SearchSpaceMembership, + WorkspaceMembership, User, VideoPresentation, VideoPresentationStatus, @@ -189,7 +189,7 @@ async def create_snapshot( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.PUBLIC_SHARING_CREATE.value, "You don't have permission to create public share links", ) @@ -450,7 +450,7 @@ async def list_snapshots_for_thread( await check_permission( session, auth, - thread.search_space_id, + thread.workspace_id, Permission.PUBLIC_SHARING_VIEW.value, "You don't have permission to view public share links", ) @@ -476,18 +476,18 @@ async def list_snapshots_for_thread( ] -async def list_snapshots_for_search_space( +async def list_snapshots_for_workspace( session: AsyncSession, - search_space_id: int, + workspace_id: int, auth: AuthContext, ) -> list[dict]: - """List all public snapshots for a search space.""" + """List all public snapshots for a workspace.""" from app.config import config await check_permission( session, auth, - search_space_id, + workspace_id, Permission.PUBLIC_SHARING_VIEW.value, "You don't have permission to view public share links", ) @@ -495,7 +495,7 @@ async def list_snapshots_for_search_space( result = await session.execute( select(PublicChatSnapshot) .join(NewChatThread, PublicChatSnapshot.thread_id == NewChatThread.id) - .filter(NewChatThread.search_space_id == search_space_id) + .filter(NewChatThread.workspace_id == workspace_id) .order_by(PublicChatSnapshot.created_at.desc()) ) snapshots = result.scalars().all() @@ -556,7 +556,7 @@ async def delete_snapshot( await check_permission( session, auth, - snapshot.thread.search_space_id, + snapshot.thread.workspace_id, Permission.PUBLIC_SHARING_DELETE.value, "You don't have permission to delete public share links", ) @@ -603,27 +603,27 @@ async def delete_affected_snapshots( # ============================================================================= -async def get_user_default_search_space( +async def get_user_default_workspace( session: AsyncSession, user_id: UUID, ) -> int | None: """ - Get user's default search space for cloning. + Get user's default workspace for cloning. - Returns the first search space where user is owner, or None if not found. + Returns the first workspace where user is owner, or None if not found. """ result = await session.execute( - select(SearchSpaceMembership) + select(WorkspaceMembership) .filter( - SearchSpaceMembership.user_id == user_id, - SearchSpaceMembership.is_owner.is_(True), + WorkspaceMembership.user_id == user_id, + WorkspaceMembership.is_owner.is_(True), ) .limit(1) ) membership = result.scalars().first() if membership: - return membership.search_space_id + return membership.workspace_id return None @@ -650,10 +650,10 @@ async def clone_from_snapshot( status_code=404, detail="Chat not found or no longer public" ) - target_search_space_id = await get_user_default_search_space(session, user.id) + target_workspace_id = await get_user_default_workspace(session, user.id) - if target_search_space_id is None: - raise HTTPException(status_code=400, detail="No search space found for user") + if target_workspace_id is None: + raise HTTPException(status_code=400, detail="No workspace found for user") data = snapshot.snapshot_data messages_data = data.get("messages", []) @@ -664,7 +664,7 @@ async def clone_from_snapshot( title=data.get("title", "Cloned Chat"), archived=False, visibility=ChatVisibility.PRIVATE, - search_space_id=target_search_space_id, + workspace_id=target_workspace_id, created_by_id=user.id, cloned_from_thread_id=snapshot.thread_id, cloned_from_snapshot_id=snapshot.id, @@ -726,7 +726,7 @@ async def clone_from_snapshot( storage_key=podcast_info.get("storage_key"), file_location=podcast_info.get("file_path"), status=PodcastStatus.READY, - search_space_id=target_search_space_id, + workspace_id=target_workspace_id, thread_id=new_thread.id, ) session.add(new_podcast) @@ -754,7 +754,7 @@ async def clone_from_snapshot( title=report_info.get("title", "Cloned Report"), content=report_info.get("content"), report_metadata=report_info.get("report_metadata"), - search_space_id=target_search_space_id, + workspace_id=target_workspace_id, thread_id=new_thread.id, ) session.add(new_report) @@ -783,7 +783,7 @@ async def clone_from_snapshot( return { "thread_id": new_thread.id, - "search_space_id": target_search_space_id, + "workspace_id": target_workspace_id, } diff --git a/surfsense_backend/app/services/quota_checked_vision_llm.py b/surfsense_backend/app/services/quota_checked_vision_llm.py index 0040e5a5b..99fa0c97c 100644 --- a/surfsense_backend/app/services/quota_checked_vision_llm.py +++ b/surfsense_backend/app/services/quota_checked_vision_llm.py @@ -18,7 +18,7 @@ Why a wrapper instead of plumbing ``user_id`` through every caller: Dropbox, local-folder, file-processor, ETL pipeline) each calling ``parse_with_vision_llm(...)``. Adding a ``user_id`` argument to each is invasive, error-prone, and easy for a future indexer to forget. -* Per the design (issue M), we always debit the *search-space owner*, not +* Per the design (issue M), we always debit the *workspace owner*, not the triggering user, so ``user_id`` is fully derivable from the search space the caller is already operating on. The wrapper captures it once at construction time. @@ -56,7 +56,7 @@ class QuotaCheckedVisionLLM: inner_llm: Any, *, user_id: UUID, - search_space_id: int, + workspace_id: int, billing_tier: str, base_model: str, quota_reserve_tokens: int | None, @@ -64,7 +64,7 @@ class QuotaCheckedVisionLLM: ) -> None: self._inner = inner_llm self._user_id = user_id - self._search_space_id = search_space_id + self._workspace_id = workspace_id self._billing_tier = billing_tier self._base_model = base_model self._quota_reserve_tokens = quota_reserve_tokens @@ -81,7 +81,7 @@ class QuotaCheckedVisionLLM: """ async with billable_call( user_id=self._user_id, - search_space_id=self._search_space_id, + workspace_id=self._workspace_id, billing_tier=self._billing_tier, base_model=self._base_model, quota_reserve_tokens=self._quota_reserve_tokens, diff --git a/surfsense_backend/app/services/revert_service.py b/surfsense_backend/app/services/revert_service.py index 0cb6cd092..3d2faf44e 100644 --- a/surfsense_backend/app/services/revert_service.py +++ b/surfsense_backend/app/services/revert_service.py @@ -121,7 +121,7 @@ def can_revert( """Return True iff the requester is allowed to revert this action. The plan's rule: "requester must be the original `user_id` on the - action, or hold the search-space admin role." Anonymous actions + action, or hold the workspace admin role." Anonymous actions (``action.user_id is None``) can only be reverted by admins. """ if is_admin: @@ -215,7 +215,7 @@ async def _restore_in_place_document( if isinstance(revision.content_before, str): doc.content_hash = generate_content_hash( - revision.content_before, doc.search_space_id + revision.content_before, doc.workspace_id ) virtual_path = await _virtual_path_from_snapshot(session, revision) @@ -223,7 +223,7 @@ async def _restore_in_place_document( doc.unique_identifier_hash = generate_unique_identifier_hash( DocumentType.NOTE, virtual_path, - doc.search_space_id, + doc.workspace_id, ) chunks_before = revision.chunks_before @@ -285,15 +285,15 @@ async def _reinsert_document_from_revision( ), ) - search_space_id = revision.search_space_id + workspace_id = revision.workspace_id unique_identifier_hash = generate_unique_identifier_hash( DocumentType.NOTE, virtual_path, - search_space_id, + workspace_id, ) collision = await session.execute( select(Document.id).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.unique_identifier_hash == unique_identifier_hash, ) ) @@ -318,10 +318,10 @@ async def _reinsert_document_from_revision( document_type=DocumentType.NOTE, document_metadata=metadata, content=content, - content_hash=generate_content_hash(content, search_space_id), + content_hash=generate_content_hash(content, workspace_id), unique_identifier_hash=unique_identifier_hash, source_markdown=content, - search_space_id=search_space_id, + workspace_id=workspace_id, folder_id=revision.folder_id_before, updated_at=datetime.now(UTC), ) @@ -451,7 +451,7 @@ async def _reinsert_folder_from_revision( name=revision.name_before, parent_id=revision.parent_id_before, position=revision.position_before, - search_space_id=revision.search_space_id, + workspace_id=revision.workspace_id, updated_at=datetime.now(UTC), ) session.add(new_folder) @@ -608,7 +608,7 @@ async def revert_action( new_row = AgentActionLog( thread_id=action.thread_id, user_id=requester_user_id, - search_space_id=action.search_space_id, + workspace_id=action.workspace_id, turn_id=None, message_id=None, tool_name=f"_revert:{action.tool_name}", diff --git a/surfsense_backend/app/services/task_dispatcher.py b/surfsense_backend/app/services/task_dispatcher.py index 43957be03..b525f877b 100644 --- a/surfsense_backend/app/services/task_dispatcher.py +++ b/surfsense_backend/app/services/task_dispatcher.py @@ -16,7 +16,7 @@ class TaskDispatcher(Protocol): document_id: int, temp_path: str, filename: str, - search_space_id: int, + workspace_id: int, user_id: str, use_vision_llm: bool = False, processing_mode: str = "basic", @@ -32,7 +32,7 @@ class CeleryTaskDispatcher: document_id: int, temp_path: str, filename: str, - search_space_id: int, + workspace_id: int, user_id: str, use_vision_llm: bool = False, processing_mode: str = "basic", @@ -45,7 +45,7 @@ class CeleryTaskDispatcher: document_id=document_id, temp_path=temp_path, filename=filename, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, use_vision_llm=use_vision_llm, processing_mode=processing_mode, diff --git a/surfsense_backend/app/services/task_logging_service.py b/surfsense_backend/app/services/task_logging_service.py index 6ba9d0432..9b1ae23ab 100644 --- a/surfsense_backend/app/services/task_logging_service.py +++ b/surfsense_backend/app/services/task_logging_service.py @@ -13,9 +13,9 @@ logger = logging.getLogger(__name__) class TaskLoggingService: """Service for logging background tasks using the database Log model""" - def __init__(self, session: AsyncSession, search_space_id: int): + def __init__(self, session: AsyncSession, workspace_id: int): self.session = session - self.search_space_id = search_space_id + self.workspace_id = workspace_id async def log_task_start( self, @@ -47,7 +47,7 @@ class TaskLoggingService: message=message, source=source, log_metadata=log_metadata, - search_space_id=self.search_space_id, + workspace_id=self.workspace_id, ) self.session.add(log_entry) @@ -232,7 +232,7 @@ class TaskLoggingService: message=message, source=source, log_metadata=metadata or {}, - search_space_id=self.search_space_id, + workspace_id=self.workspace_id, ) self.session.add(log_entry) diff --git a/surfsense_backend/app/services/token_tracking_service.py b/surfsense_backend/app/services/token_tracking_service.py index d1a29b82a..2b4ec4273 100644 --- a/surfsense_backend/app/services/token_tracking_service.py +++ b/surfsense_backend/app/services/token_tracking_service.py @@ -518,7 +518,7 @@ async def record_token_usage( session: AsyncSession, *, usage_type: str, - search_space_id: int, + workspace_id: int, user_id: UUID, prompt_tokens: int = 0, completion_tokens: int = 0, @@ -546,7 +546,7 @@ async def record_token_usage( call_details=call_details, thread_id=thread_id, message_id=message_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) session.add(record) diff --git a/surfsense_backend/app/services/user_tool_allowlist.py b/surfsense_backend/app/services/user_tool_allowlist.py index 9b87fbdea..c0cf34394 100644 --- a/surfsense_backend/app/services/user_tool_allowlist.py +++ b/surfsense_backend/app/services/user_tool_allowlist.py @@ -1,6 +1,6 @@ """User-scoped trusted-tools list backed by ``SearchSourceConnector.config``. -Storage is per ``(user_id, search_space_id, connector_id)`` under +Storage is per ``(user_id, workspace_id, connector_id)`` under ``connector.config['trusted_tools']``. The list only ever encodes ``allow`` decisions; coded ``deny`` rules cannot be overridden here. """ @@ -108,7 +108,7 @@ async def fetch_user_allowlist_rulesets( session: AsyncSession, *, user_id: uuid.UUID, - search_space_id: int, + workspace_id: int, ) -> dict[str, Ruleset]: """Project the user's trusted tools into per-subagent ``allow`` rulesets. @@ -122,7 +122,7 @@ async def fetch_user_allowlist_rulesets( SearchSourceConnector.config, ).where( SearchSourceConnector.user_id == user_id, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, ) ) diff --git a/surfsense_backend/app/session_events.py b/surfsense_backend/app/session_events.py index 048df2b46..31a03e9f3 100644 --- a/surfsense_backend/app/session_events.py +++ b/surfsense_backend/app/session_events.py @@ -38,7 +38,7 @@ def _after_flush(session: Session, flush_context: object) -> None: result = payload_if_entered_folder( document_id=obj.id, - search_space_id=obj.search_space_id, + workspace_id=obj.workspace_id, new_folder_id=new_folder_id, previous_folder_id=previous_folder_id, folder_id_changed=True, @@ -72,7 +72,7 @@ def _after_commit(session: Session) -> None: default_bus.publish( item["event_type"], item["payload"], - search_space_id=item["search_space_id"], + workspace_id=item["workspace_id"], ) ) for item in pending diff --git a/surfsense_backend/app/tasks/celery_tasks/connector_tasks.py b/surfsense_backend/app/tasks/celery_tasks/connector_tasks.py index 50f757473..a3490e8b4 100644 --- a/surfsense_backend/app/tasks/celery_tasks/connector_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/connector_tasks.py @@ -64,7 +64,7 @@ def _handle_greenlet_error(e: Exception, task_name: str, connector_id: int) -> N f"GREENLET ERROR in {task_name} for connector {connector_id}: {error_str}\n" f"This error typically occurs when SQLAlchemy tries to lazy-load a relationship " f"outside of an async context. Check for:\n" - f"1. Accessing relationship attributes (e.g., document.chunks, connector.search_space) " + f"1. Accessing relationship attributes (e.g., document.chunks, connector.workspace) " f"without using selectinload() or joinedload()\n" f"2. Accessing model attributes after the session is closed\n" f"3. Passing ORM objects between different async contexts\n" @@ -81,7 +81,7 @@ def _handle_greenlet_error(e: Exception, task_name: str, connector_id: int) -> N def index_notion_pages_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -90,7 +90,7 @@ def index_notion_pages_task( try: return run_async_celery_task( lambda: _index_notion_pages( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) except Exception as e: @@ -100,7 +100,7 @@ def index_notion_pages_task( async def _index_notion_pages( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -112,7 +112,7 @@ async def _index_notion_pages( async with get_celery_session_maker()() as session: await run_notion_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -120,7 +120,7 @@ async def _index_notion_pages( def index_github_repos_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -128,14 +128,14 @@ def index_github_repos_task( """Celery task to index GitHub repositories.""" return run_async_celery_task( lambda: _index_github_repos( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) async def _index_github_repos( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -147,7 +147,7 @@ async def _index_github_repos( async with get_celery_session_maker()() as session: await run_github_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -155,7 +155,7 @@ async def _index_github_repos( def index_confluence_pages_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -163,14 +163,14 @@ def index_confluence_pages_task( """Celery task to index Confluence pages.""" return run_async_celery_task( lambda: _index_confluence_pages( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) async def _index_confluence_pages( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -182,7 +182,7 @@ async def _index_confluence_pages( async with get_celery_session_maker()() as session: await run_confluence_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -190,7 +190,7 @@ async def _index_confluence_pages( def index_google_calendar_events_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -199,7 +199,7 @@ def index_google_calendar_events_task( try: return run_async_celery_task( lambda: _index_google_calendar_events( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) except Exception as e: @@ -209,7 +209,7 @@ def index_google_calendar_events_task( async def _index_google_calendar_events( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -221,7 +221,7 @@ async def _index_google_calendar_events( async with get_celery_session_maker()() as session: await run_google_calendar_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -229,7 +229,7 @@ async def _index_google_calendar_events( def index_google_gmail_messages_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -237,14 +237,14 @@ def index_google_gmail_messages_task( """Celery task to index Google Gmail messages.""" return run_async_celery_task( lambda: _index_google_gmail_messages( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) async def _index_google_gmail_messages( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -256,7 +256,7 @@ async def _index_google_gmail_messages( async with get_celery_session_maker()() as session: await run_google_gmail_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -264,7 +264,7 @@ async def _index_google_gmail_messages( def index_google_drive_files_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, # Dictionary with 'folders', 'files', and 'indexing_options' ): @@ -272,7 +272,7 @@ def index_google_drive_files_task( return run_async_celery_task( lambda: _index_google_drive_files( connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -281,7 +281,7 @@ def index_google_drive_files_task( async def _index_google_drive_files( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, # Dictionary with 'folders', 'files', and 'indexing_options' ): @@ -294,7 +294,7 @@ async def _index_google_drive_files( await run_google_drive_indexing( session, connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -304,7 +304,7 @@ async def _index_google_drive_files( def index_onedrive_files_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ): @@ -312,7 +312,7 @@ def index_onedrive_files_task( return run_async_celery_task( lambda: _index_onedrive_files( connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -321,7 +321,7 @@ def index_onedrive_files_task( async def _index_onedrive_files( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ): @@ -334,7 +334,7 @@ async def _index_onedrive_files( await run_onedrive_indexing( session, connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -344,7 +344,7 @@ async def _index_onedrive_files( def index_dropbox_files_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ): @@ -352,7 +352,7 @@ def index_dropbox_files_task( return run_async_celery_task( lambda: _index_dropbox_files( connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -361,7 +361,7 @@ def index_dropbox_files_task( async def _index_dropbox_files( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ): @@ -374,7 +374,7 @@ async def _index_dropbox_files( await run_dropbox_indexing( session, connector_id, - search_space_id, + workspace_id, user_id, items_dict, ) @@ -384,7 +384,7 @@ async def _index_dropbox_files( def index_elasticsearch_documents_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -392,14 +392,14 @@ def index_elasticsearch_documents_task( """Celery task to index Elasticsearch documents.""" return run_async_celery_task( lambda: _index_elasticsearch_documents( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) async def _index_elasticsearch_documents( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -411,7 +411,7 @@ async def _index_elasticsearch_documents( async with get_celery_session_maker()() as session: await run_elasticsearch_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -419,7 +419,7 @@ async def _index_elasticsearch_documents( def index_crawled_urls_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -428,7 +428,7 @@ def index_crawled_urls_task( try: return run_async_celery_task( lambda: _index_crawled_urls( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) except Exception as e: @@ -438,7 +438,7 @@ def index_crawled_urls_task( async def _index_crawled_urls( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -450,7 +450,7 @@ async def _index_crawled_urls( async with get_celery_session_maker()() as session: await run_web_page_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -458,7 +458,7 @@ async def _index_crawled_urls( def index_bookstack_pages_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -466,14 +466,14 @@ def index_bookstack_pages_task( """Celery task to index BookStack pages.""" return run_async_celery_task( lambda: _index_bookstack_pages( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) async def _index_bookstack_pages( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -485,7 +485,7 @@ async def _index_bookstack_pages( async with get_celery_session_maker()() as session: await run_bookstack_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) @@ -493,7 +493,7 @@ async def _index_bookstack_pages( def index_composio_connector_task( self, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None, end_date: str | None, @@ -501,14 +501,14 @@ def index_composio_connector_task( """Celery task to index Composio connector content (Google Drive, Gmail, Calendar via Composio).""" return run_async_celery_task( lambda: _index_composio_connector( - connector_id, search_space_id, user_id, start_date, end_date + connector_id, workspace_id, user_id, start_date, end_date ) ) async def _index_composio_connector( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None, end_date: str | None, @@ -521,5 +521,5 @@ async def _index_composio_connector( async with get_celery_session_maker()() as session: await run_composio_indexing( - session, connector_id, search_space_id, user_id, start_date, end_date + session, connector_id, workspace_id, user_id, start_date, end_date ) diff --git a/surfsense_backend/app/tasks/celery_tasks/document_reindex_tasks.py b/surfsense_backend/app/tasks/celery_tasks/document_reindex_tasks.py index d36a7c05f..240e0b69c 100644 --- a/surfsense_backend/app/tasks/celery_tasks/document_reindex_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/document_reindex_tasks.py @@ -41,7 +41,7 @@ async def _reindex_document(document_id: int, user_id: str): logger.error(f"Document {document_id} not found") return - task_logger = TaskLoggingService(session, document.search_space_id) + task_logger = TaskLoggingService(session, document.workspace_id) log_entry = await task_logger.log_task_start( task_name="document_reindex", diff --git a/surfsense_backend/app/tasks/celery_tasks/document_tasks.py b/surfsense_backend/app/tasks/celery_tasks/document_tasks.py index 4d71d6c9a..ba8a49f67 100644 --- a/surfsense_backend/app/tasks/celery_tasks/document_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/document_tasks.py @@ -210,18 +210,18 @@ async def _delete_folder_documents( retry_backoff_max=300, max_retries=5, ) -def delete_search_space_task(self, search_space_id: int): - """Celery task to delete a search space and heavy child rows in batches.""" +def delete_workspace_task(self, workspace_id: int): + """Celery task to delete a workspace and heavy child rows in batches.""" return run_async_celery_task( - lambda: _delete_search_space_background(search_space_id) + lambda: _delete_workspace_background(workspace_id) ) -async def _delete_search_space_background(search_space_id: int) -> None: - """Delete chunks/docs in batches first, then delete the search space.""" +async def _delete_workspace_background(workspace_id: int) -> None: + """Delete chunks/docs in batches first, then delete the workspace.""" from sqlalchemy import delete as sa_delete, select - from app.db import Chunk, Document, SearchSpace + from app.db import Chunk, Document, Workspace from app.file_storage.service import purge_document_blobs async with get_celery_session_maker()() as session: @@ -231,7 +231,7 @@ async def _delete_search_space_background(search_space_id: int) -> None: chunk_ids_result = await session.execute( select(Chunk.id) .join(Document, Chunk.document_id == Document.id) - .where(Document.search_space_id == search_space_id) + .where(Document.workspace_id == workspace_id) .limit(batch_size) ) chunk_ids = chunk_ids_result.scalars().all() @@ -243,7 +243,7 @@ async def _delete_search_space_background(search_space_id: int) -> None: while True: doc_ids_result = await session.execute( select(Document.id) - .where(Document.search_space_id == search_space_id) + .where(Document.workspace_id == workspace_id) .limit(batch_size) ) doc_ids = doc_ids_result.scalars().all() @@ -254,7 +254,7 @@ async def _delete_search_space_background(search_space_id: int) -> None: await session.execute(sa_delete(Document).where(Document.id.in_(doc_ids))) await session.commit() - space = await session.get(SearchSpace, search_space_id) + space = await session.get(Workspace, workspace_id) if space: await session.delete(space) await session.commit() @@ -262,25 +262,25 @@ async def _delete_search_space_background(search_space_id: int) -> None: @celery_app.task(name="process_extension_document", bind=True) def process_extension_document_task( - self, individual_document_dict, search_space_id: int, user_id: str + self, individual_document_dict, workspace_id: int, user_id: str ): """ Celery task to process extension document. Args: individual_document_dict: Document data as dictionary - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user """ return run_async_celery_task( lambda: _process_extension_document( - individual_document_dict, search_space_id, user_id + individual_document_dict, workspace_id, user_id ) ) async def _process_extension_document( - individual_document_dict, search_space_id: int, user_id: str + individual_document_dict, workspace_id: int, user_id: str ): """Process extension document with new session.""" from pydantic import BaseModel, ConfigDict, Field @@ -303,7 +303,7 @@ async def _process_extension_document( individual_document = IndividualDocument(**individual_document_dict) async with get_celery_session_maker()() as session: - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Truncate title for notification display page_title = individual_document.metadata.VisitedWebPageTitle[:50] @@ -317,7 +317,7 @@ async def _process_extension_document( user_id=UUID(user_id), document_type="EXTENSION", document_name=page_title, - search_space_id=search_space_id, + workspace_id=workspace_id, ) ) @@ -343,7 +343,7 @@ async def _process_extension_document( ) result = await add_extension_received_document( - session, individual_document, search_space_id, user_id + session, individual_document, workspace_id, user_id ) if result: @@ -406,24 +406,24 @@ async def _process_extension_document( @celery_app.task(name="process_youtube_video", bind=True) -def process_youtube_video_task(self, url: str, search_space_id: int, user_id: str): +def process_youtube_video_task(self, url: str, workspace_id: int, user_id: str): """ Celery task to process YouTube video. Args: url: YouTube video URL - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user """ return run_async_celery_task( - lambda: _process_youtube_video(url, search_space_id, user_id) + lambda: _process_youtube_video(url, workspace_id, user_id) ) -async def _process_youtube_video(url: str, search_space_id: int, user_id: str): +async def _process_youtube_video(url: str, workspace_id: int, user_id: str): """Process YouTube video with new session.""" async with get_celery_session_maker()() as session: - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Extract video title from URL for notification (will be updated later) video_name = url.split("v=")[-1][:11] if "v=" in url else url @@ -435,7 +435,7 @@ async def _process_youtube_video(url: str, search_space_id: int, user_id: str): user_id=UUID(user_id), document_type="YOUTUBE_VIDEO", document_name=f"YouTube: {video_name}", - search_space_id=search_space_id, + workspace_id=workspace_id, ) ) @@ -460,7 +460,7 @@ async def _process_youtube_video(url: str, search_space_id: int, user_id: str): ) result = await add_youtube_video_document( - session, url, search_space_id, user_id, notification=notification + session, url, workspace_id, user_id, notification=notification ) if result: @@ -532,7 +532,7 @@ async def _process_youtube_video(url: str, search_space_id: int, user_id: str): @celery_app.task(name="process_file_upload", bind=True) def process_file_upload_task( - self, file_path: str, filename: str, search_space_id: int, user_id: str + self, file_path: str, filename: str, workspace_id: int, user_id: str ): """ Celery task to process uploaded file. @@ -540,14 +540,14 @@ def process_file_upload_task( Args: file_path: Path to the uploaded file filename: Original filename - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user """ import traceback logger.info( f"[process_file_upload] Task started - file: {filename}, " - f"search_space_id: {search_space_id}, user_id: {user_id}" + f"workspace_id: {workspace_id}, user_id: {user_id}" ) logger.info(f"[process_file_upload] File path: {file_path}") @@ -567,7 +567,7 @@ def process_file_upload_task( try: run_async_celery_task( - lambda: _process_file_upload(file_path, filename, search_space_id, user_id) + lambda: _process_file_upload(file_path, filename, workspace_id, user_id) ) logger.info( f"[process_file_upload] Task completed successfully for: {filename}" @@ -581,7 +581,7 @@ def process_file_upload_task( async def _process_file_upload( - file_path: str, filename: str, search_space_id: int, user_id: str + file_path: str, filename: str, workspace_id: int, user_id: str ): """Process file upload with new session.""" from app.tasks.document_processors.file_processors import process_file_in_background @@ -590,7 +590,7 @@ async def _process_file_upload( async with get_celery_session_maker()() as session: logger.info(f"[_process_file_upload] Database session created for: {filename}") - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Get file size for notification metadata try: @@ -611,7 +611,7 @@ async def _process_file_upload( user_id=UUID(user_id), document_type="FILE", document_name=filename, - search_space_id=search_space_id, + workspace_id=workspace_id, file_size=file_size, ) ) @@ -642,7 +642,7 @@ async def _process_file_upload( result = await process_file_in_background( file_path, filename, - search_space_id, + workspace_id, user_id, session, task_logger, @@ -709,7 +709,7 @@ async def _process_file_upload( user_id=UUID(user_id), document_name=filename, document_type="FILE", - search_space_id=search_space_id, + workspace_id=workspace_id, balance_micros=credit_error.balance_micros, required_micros=credit_error.required_micros, ) @@ -770,7 +770,7 @@ def process_file_upload_with_document_task( document_id: int, temp_path: str, filename: str, - search_space_id: int, + workspace_id: int, user_id: str, use_vision_llm: bool = False, processing_mode: str = "basic", @@ -786,14 +786,14 @@ def process_file_upload_with_document_task( document_id: ID of the pending document created in Phase 1 temp_path: Path to the uploaded file filename: Original filename - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user """ import traceback logger.info( f"[process_file_upload_with_document] Task started - document_id: {document_id}, " - f"file: {filename}, search_space_id: {search_space_id}" + f"file: {filename}, workspace_id: {workspace_id}" ) # Check if file exists and is accessible @@ -817,7 +817,7 @@ def process_file_upload_with_document_task( document_id, temp_path, filename, - search_space_id, + workspace_id, user_id, use_vision_llm=use_vision_llm, processing_mode=processing_mode, @@ -852,7 +852,7 @@ async def _process_file_with_document( document_id: int, temp_path: str, filename: str, - search_space_id: int, + workspace_id: int, user_id: str, use_vision_llm: bool = False, processing_mode: str = "basic", @@ -879,7 +879,7 @@ async def _process_file_with_document( logger.info( f"[_process_file_with_document] Database session created for: {filename}" ) - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Get the document document = await session.get(Document, document_id) @@ -910,7 +910,7 @@ async def _process_file_with_document( user_id=UUID(user_id), document_type="FILE", document_name=filename, - search_space_id=search_space_id, + workspace_id=workspace_id, file_size=file_size, ) ) @@ -958,7 +958,7 @@ async def _process_file_with_document( document=document, file_path=temp_path, filename=filename, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, session=session, task_logger=task_logger, @@ -1033,7 +1033,7 @@ async def _process_file_with_document( user_id=UUID(user_id), document_name=filename, document_type="FILE", - search_space_id=search_space_id, + workspace_id=workspace_id, balance_micros=credit_error.balance_micros, required_micros=credit_error.required_micros, ) @@ -1092,7 +1092,7 @@ def process_circleback_meeting_task( meeting_name: str, markdown_content: str, metadata: dict, - search_space_id: int, + workspace_id: int, connector_id: int | None = None, ): """ @@ -1103,7 +1103,7 @@ def process_circleback_meeting_task( meeting_name: Name of the meeting markdown_content: Meeting content formatted as markdown metadata: Meeting metadata dictionary - search_space_id: ID of the search space + workspace_id: ID of the workspace connector_id: ID of the Circleback connector (for deletion support) """ return run_async_celery_task( @@ -1112,7 +1112,7 @@ def process_circleback_meeting_task( meeting_name, markdown_content, metadata, - search_space_id, + workspace_id, connector_id, ) ) @@ -1123,7 +1123,7 @@ async def _process_circleback_meeting( meeting_name: str, markdown_content: str, metadata: dict, - search_space_id: int, + workspace_id: int, connector_id: int | None = None, ): """Process Circleback meeting with new session.""" @@ -1132,7 +1132,7 @@ async def _process_circleback_meeting( ) async with get_celery_session_maker()() as session: - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Get user_id from metadata if available user_id = metadata.get("user_id") @@ -1147,7 +1147,7 @@ async def _process_circleback_meeting( user_id=UUID(user_id), document_type="CIRCLEBACK", document_name=f"Meeting: {meeting_name[:40]}", - search_space_id=search_space_id, + workspace_id=workspace_id, ) ) @@ -1185,7 +1185,7 @@ async def _process_circleback_meeting( meeting_name=meeting_name, markdown_content=markdown_content, metadata=metadata, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, ) @@ -1261,7 +1261,7 @@ async def _process_circleback_meeting( @celery_app.task(name="index_local_folder", bind=True) def index_local_folder_task( self, - search_space_id: int, + workspace_id: int, user_id: str, folder_path: str, folder_name: str, @@ -1273,7 +1273,7 @@ def index_local_folder_task( """Celery task to index a local folder. Config is passed directly — no connector row.""" return run_async_celery_task( lambda: _index_local_folder_async( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_path=folder_path, folder_name=folder_name, @@ -1286,7 +1286,7 @@ def index_local_folder_task( async def _index_local_folder_async( - search_space_id: int, + workspace_id: int, user_id: str, folder_path: str, folder_name: str, @@ -1317,7 +1317,7 @@ async def _index_local_folder_async( user_id=UUID(user_id), document_type="LOCAL_FOLDER_FILE", document_name=doc_name, - search_space_id=search_space_id, + workspace_id=workspace_id, ) ) notification_id = notification.id @@ -1343,7 +1343,7 @@ async def _index_local_folder_async( try: _indexed, _skipped_or_failed, _rfid, err = await index_local_folder( session=session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_path=folder_path, folder_name=folder_name, @@ -1402,7 +1402,7 @@ async def _index_local_folder_async( @celery_app.task(name="index_uploaded_folder_files", bind=True) def index_uploaded_folder_files_task( self, - search_space_id: int, + workspace_id: int, user_id: str, folder_name: str, root_folder_id: int, @@ -1413,7 +1413,7 @@ def index_uploaded_folder_files_task( """Celery task to index files uploaded from the desktop app.""" return run_async_celery_task( lambda: _index_uploaded_folder_files_async( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_name=folder_name, root_folder_id=root_folder_id, @@ -1425,7 +1425,7 @@ def index_uploaded_folder_files_task( async def _index_uploaded_folder_files_async( - search_space_id: int, + workspace_id: int, user_id: str, folder_name: str, root_folder_id: int, @@ -1449,7 +1449,7 @@ async def _index_uploaded_folder_files_async( user_id=UUID(user_id), document_type="LOCAL_FOLDER_FILE", document_name=doc_name, - search_space_id=search_space_id, + workspace_id=workspace_id, ) ) notification_id = notification.id @@ -1474,7 +1474,7 @@ async def _index_uploaded_folder_files_async( try: _indexed, _failed, err = await index_uploaded_files( session=session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_name=folder_name, root_folder_id=root_folder_id, @@ -1539,26 +1539,26 @@ def _get_ai_sort_redis(): return _ai_sort_redis -def _ai_sort_lock_key(search_space_id: int) -> str: - return f"ai_sort:search_space:{search_space_id}:lock" +def _ai_sort_lock_key(workspace_id: int) -> str: + return f"ai_sort:workspace:{workspace_id}:lock" @celery_app.task(name="ai_sort_search_space", bind=True, max_retries=1) -def ai_sort_search_space_task(self, search_space_id: int, user_id: str): - """Full AI sort for all documents in a search space.""" +def ai_sort_workspace_task(self, workspace_id: int, user_id: str): + """Full AI sort for all documents in a workspace.""" return run_async_celery_task( - lambda: _ai_sort_search_space_async(search_space_id, user_id) + lambda: _ai_sort_workspace_async(workspace_id, user_id) ) -async def _ai_sort_search_space_async(search_space_id: int, user_id: str): +async def _ai_sort_workspace_async(workspace_id: int, user_id: str): r = _get_ai_sort_redis() - lock_key = _ai_sort_lock_key(search_space_id) + lock_key = _ai_sort_lock_key(workspace_id) if not r.set(lock_key, "running", nx=True, ex=AI_SORT_LOCK_TTL_SECONDS): logger.info( - "AI sort already running for search_space=%d, skipping", - search_space_id, + "AI sort already running for workspace=%d, skipping", + workspace_id, ) return @@ -1568,21 +1568,21 @@ async def _ai_sort_search_space_async(search_space_id: int, user_id: str): from app.services.llm_service import get_agent_llm async with get_celery_session_maker()() as session: - llm = await get_agent_llm(session, search_space_id, disable_streaming=True) + llm = await get_agent_llm(session, workspace_id, disable_streaming=True) if llm is None: logger.warning( - "No LLM configured for search_space=%d, skipping AI sort", - search_space_id, + "No LLM configured for workspace=%d, skipping AI sort", + workspace_id, ) return sorted_count, failed_count = await ai_sort_all_documents( - session, search_space_id, llm + session, workspace_id, llm ) elapsed = time.perf_counter() - t_start logger.info( - "AI sort search_space=%d done in %.1fs: sorted=%d failed=%d", - search_space_id, + "AI sort workspace=%d done in %.1fs: sorted=%d failed=%d", + workspace_id, elapsed, sorted_count, failed_count, @@ -1594,14 +1594,14 @@ async def _ai_sort_search_space_async(search_space_id: int, user_id: str): @celery_app.task( name="ai_sort_document", bind=True, max_retries=2, default_retry_delay=10 ) -def ai_sort_document_task(self, search_space_id: int, user_id: str, document_id: int): +def ai_sort_document_task(self, workspace_id: int, user_id: str, document_id: int): """Incremental AI sort for a single document after indexing.""" return run_async_celery_task( - lambda: _ai_sort_document_async(search_space_id, user_id, document_id) + lambda: _ai_sort_document_async(workspace_id, user_id, document_id) ) -async def _ai_sort_document_async(search_space_id: int, user_id: str, document_id: int): +async def _ai_sort_document_async(workspace_id: int, user_id: str, document_id: int): from app.db import Document from app.services.ai_file_sort_service import ai_sort_document from app.services.llm_service import get_agent_llm @@ -1612,11 +1612,11 @@ async def _ai_sort_document_async(search_space_id: int, user_id: str, document_i logger.warning("Document %d not found, skipping AI sort", document_id) return - llm = await get_agent_llm(session, search_space_id, disable_streaming=True) + llm = await get_agent_llm(session, workspace_id, disable_streaming=True) if llm is None: logger.warning( - "No LLM for search_space=%d, skipping AI sort of doc=%d", - search_space_id, + "No LLM for workspace=%d, skipping AI sort of doc=%d", + workspace_id, document_id, ) return @@ -1624,7 +1624,7 @@ async def _ai_sort_document_async(search_space_id: int, user_id: str, document_i await ai_sort_document(session, document, llm) await session.commit() logger.info( - "AI sorted document=%d into search_space=%d", + "AI sorted document=%d into workspace=%d", document_id, - search_space_id, + workspace_id, ) diff --git a/surfsense_backend/app/tasks/celery_tasks/schedule_checker_task.py b/surfsense_backend/app/tasks/celery_tasks/schedule_checker_task.py index e88fb58b9..7243522d7 100644 --- a/surfsense_backend/app/tasks/celery_tasks/schedule_checker_task.py +++ b/surfsense_backend/app/tasks/celery_tasks/schedule_checker_task.py @@ -142,7 +142,7 @@ async def _check_and_trigger_schedules(): if selected_folders or selected_files: task.delay( connector.id, - connector.search_space_id, + connector.workspace_id, str(connector.user_id), { "folders": selected_folders, @@ -180,7 +180,7 @@ async def _check_and_trigger_schedules(): if urls: task.delay( connector.id, - connector.search_space_id, + connector.workspace_id, str(connector.user_id), None, # start_date None, # end_date @@ -202,7 +202,7 @@ async def _check_and_trigger_schedules(): else: task.delay( connector.id, - connector.search_space_id, + connector.workspace_id, str(connector.user_id), None, # start_date - uses last_indexed_at None, # end_date - uses now diff --git a/surfsense_backend/app/tasks/celery_tasks/video_presentation_tasks.py b/surfsense_backend/app/tasks/celery_tasks/video_presentation_tasks.py index c6ce0b350..d46bee689 100644 --- a/surfsense_backend/app/tasks/celery_tasks/video_presentation_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/video_presentation_tasks.py @@ -15,7 +15,7 @@ from app.db import VideoPresentation, VideoPresentationStatus from app.services.billable_calls import ( BillingSettlementError, QuotaInsufficientError, - _resolve_agent_billing_for_search_space, + _resolve_agent_billing_for_workspace, billable_call, ) from app.tasks.celery_tasks import get_celery_session_maker, run_async_celery_task @@ -43,7 +43,7 @@ def generate_video_presentation_task( self, video_presentation_id: int, source_content: str, - search_space_id: int, + workspace_id: int, user_prompt: str | None = None, ) -> dict: """ @@ -55,7 +55,7 @@ def generate_video_presentation_task( lambda: _generate_video_presentation( video_presentation_id, source_content, - search_space_id, + workspace_id, user_prompt, ) ) @@ -96,7 +96,7 @@ async def _mark_video_presentation_failed(video_presentation_id: int) -> None: async def _generate_video_presentation( video_presentation_id: int, source_content: str, - search_space_id: int, + workspace_id: int, user_prompt: str | None = None, ) -> dict: """Generate video presentation and update existing record.""" @@ -120,17 +120,17 @@ async def _generate_video_presentation( owner_user_id, billing_tier, base_model, - ) = await _resolve_agent_billing_for_search_space( + ) = await _resolve_agent_billing_for_workspace( session, - search_space_id, + workspace_id, thread_id=video_pres.thread_id, ) except ValueError as resolve_err: logger.error( "VideoPresentation %s: cannot resolve billing for " - "search_space=%s: %s", + "workspace=%s: %s", video_pres.id, - search_space_id, + workspace_id, resolve_err, ) video_pres.status = VideoPresentationStatus.FAILED @@ -144,7 +144,7 @@ async def _generate_video_presentation( graph_config = { "configurable": { "video_title": video_pres.title, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "user_prompt": user_prompt, } } @@ -157,7 +157,7 @@ async def _generate_video_presentation( try: async with billable_call( user_id=owner_user_id, - search_space_id=search_space_id, + workspace_id=workspace_id, billing_tier=billing_tier, base_model=base_model, quota_reserve_micros_override=app_config.QUOTA_DEFAULT_VIDEO_PRESENTATION_RESERVE_MICROS, diff --git a/surfsense_backend/app/tasks/chat/persistence.py b/surfsense_backend/app/tasks/chat/persistence.py index 8840ec995..f78849607 100644 --- a/surfsense_backend/app/tasks/chat/persistence.py +++ b/surfsense_backend/app/tasks/chat/persistence.py @@ -412,7 +412,7 @@ async def finalize_assistant_turn( *, message_id: int, chat_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, turn_id: str, content: list[dict[str, Any]], @@ -517,7 +517,7 @@ async def finalize_assistant_turn( call_details={"calls": accumulator.serialized_calls()}, thread_id=chat_id, message_id=message_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_uuid, ) .on_conflict_do_nothing( diff --git a/surfsense_backend/app/tasks/chat/streaming/agent/builder.py b/surfsense_backend/app/tasks/chat/streaming/agent/builder.py index 9d7d1b0c5..49c442695 100644 --- a/surfsense_backend/app/tasks/chat/streaming/agent/builder.py +++ b/surfsense_backend/app/tasks/chat/streaming/agent/builder.py @@ -22,7 +22,7 @@ async def build_main_agent_for_thread( agent_factory: Any, *, llm: Any, - search_space_id: int, + workspace_id: int, db_session: Any, connector_service: ConnectorService, checkpointer: Any, @@ -38,7 +38,7 @@ async def build_main_agent_for_thread( ) -> Any: return await agent_factory( llm=llm, - search_space_id=search_space_id, + workspace_id=workspace_id, db_session=db_session, connector_service=connector_service, checkpointer=checkpointer, diff --git a/surfsense_backend/app/tasks/chat/streaming/agent/event_loop.py b/surfsense_backend/app/tasks/chat/streaming/agent/event_loop.py index 5ffe46280..ed3a59893 100644 --- a/surfsense_backend/app/tasks/chat/streaming/agent/event_loop.py +++ b/surfsense_backend/app/tasks/chat/streaming/agent/event_loop.py @@ -46,7 +46,7 @@ async def stream_agent_events( initial_step_title: str = "", initial_step_items: list[str] | None = None, *, - fallback_commit_search_space_id: int | None = None, + fallback_commit_workspace_id: int | None = None, fallback_commit_created_by_id: str | None = None, fallback_commit_filesystem_mode: FilesystemMode = FilesystemMode.CLOUD, fallback_commit_thread_id: int | None = None, @@ -92,7 +92,7 @@ async def stream_agent_events( # so reducers fire as if the after_agent hook produced it. if ( fallback_commit_filesystem_mode == FilesystemMode.CLOUD - and fallback_commit_search_space_id is not None + and fallback_commit_workspace_id is not None and ( (state_values.get("dirty_paths") or []) or (state_values.get("staged_dirs") or []) @@ -104,7 +104,7 @@ async def stream_agent_events( try: delta = await commit_staged_filesystem_state( state_values, - search_space_id=fallback_commit_search_space_id, + workspace_id=fallback_commit_workspace_id, created_by_id=fallback_commit_created_by_id, filesystem_mode=fallback_commit_filesystem_mode, thread_id=fallback_commit_thread_id, diff --git a/surfsense_backend/app/tasks/chat/streaming/errors/classifier.py b/surfsense_backend/app/tasks/chat/streaming/errors/classifier.py index 3fc5918ee..8aa703489 100644 --- a/surfsense_backend/app/tasks/chat/streaming/errors/classifier.py +++ b/surfsense_backend/app/tasks/chat/streaming/errors/classifier.py @@ -37,7 +37,7 @@ def log_chat_stream_error( is_expected: bool, request_id: str | None, thread_id: int | None, - search_space_id: int | None, + workspace_id: int | None, user_id: str | None, message: str, extra: dict[str, Any] | None = None, @@ -51,7 +51,7 @@ def log_chat_stream_error( "is_expected": is_expected, "request_id": request_id or "unknown", "thread_id": thread_id, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "user_id": user_id, "message": message, } diff --git a/surfsense_backend/app/tasks/chat/streaming/errors/emitter.py b/surfsense_backend/app/tasks/chat/streaming/errors/emitter.py index 95806ab87..c2b874ba0 100644 --- a/surfsense_backend/app/tasks/chat/streaming/errors/emitter.py +++ b/surfsense_backend/app/tasks/chat/streaming/errors/emitter.py @@ -13,7 +13,7 @@ def emit_stream_terminal_error( flow: Literal["new", "resume", "regenerate"], request_id: str | None, thread_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, message: str, error_kind: str = "server_error", @@ -30,7 +30,7 @@ def emit_stream_terminal_error( is_expected=is_expected, request_id=request_id, thread_id=thread_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, message=message, extra=extra, diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py index dbb8ee2e4..1e5de2189 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py @@ -1,7 +1,7 @@ """Resolve the auto-pin for the *initial* turn config. Auto-pin (``selected_llm_config_id=0``) picks the best eligible LLM config for -this thread / search space / user, optionally filtered to vision-capable +this thread / workspace / user, optionally filtered to vision-capable configs when the turn carries images. Errors classified here: @@ -45,7 +45,7 @@ async def resolve_initial_auto_pin( session: AsyncSession, *, chat_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, selected_llm_config_id: int, requires_image_input: bool, @@ -62,7 +62,7 @@ async def resolve_initial_auto_pin( pinned = await resolve_or_get_pinned_llm_config_id( session, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, selected_llm_config_id=selected_llm_config_id, requires_image_input=requires_image_input, diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py index 7be84c992..18b3b8152 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py @@ -64,7 +64,7 @@ async def build_new_chat_input_state( session: AsyncSession, *, chat_id: int, - search_space_id: int, + workspace_id: int, user_query: str, user_image_data_urls: list[str] | None, mentioned_document_ids: list[int] | None, @@ -110,7 +110,7 @@ async def build_new_chat_input_state( agent_user_query, accepted_folder_ids = await _resolve_mentions_for_query( session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_query=user_query, filesystem_mode=filesystem_mode, mentioned_document_ids=mentioned_document_ids, @@ -122,7 +122,7 @@ async def build_new_chat_input_state( # filesystem mode (unlike the doc/folder mention substitution above). referenced_chats = await resolve_referenced_chats( session, - search_space_id=search_space_id, + workspace_id=workspace_id, requesting_user_id=requesting_user_id, current_chat_id=chat_id, mentioned_thread_ids=mentioned_thread_ids, @@ -146,7 +146,7 @@ async def build_new_chat_input_state( input_state = { "messages": langchain_messages, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "request_id": request_id or "unknown", "turn_id": turn_id, } @@ -160,7 +160,7 @@ async def build_new_chat_input_state( async def _resolve_mentions_for_query( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, user_query: str, filesystem_mode: str, mentioned_document_ids: list[int] | None, @@ -206,7 +206,7 @@ async def _resolve_mentions_for_query( resolved = await resolve_mentions( session, - search_space_id=search_space_id, + workspace_id=workspace_id, mentioned_documents=chip_objs, mentioned_document_ids=mentioned_document_ids, mentioned_folder_ids=mentioned_folder_ids, diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py index 0e49af249..093b5bdbe 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py @@ -120,7 +120,7 @@ _background_tasks: set[asyncio.Task] = set() async def stream_new_chat( user_query: str, - search_space_id: int, + workspace_id: int, chat_id: int, user_id: str | None = None, llm_config_id: int = -1, @@ -165,7 +165,7 @@ async def stream_new_chat( chat_error_category: str | None = None chat_span_cm, chat_span = open_chat_request_span( chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, flow=flow, request_id=request_id, turn_id=stream_result.turn_id, @@ -194,7 +194,7 @@ async def stream_new_chat( flow=flow, request_id=request_id, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -217,7 +217,7 @@ async def stream_new_chat( pin_result = await resolve_initial_auto_pin( session, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, selected_llm_config_id=llm_config_id, requires_image_input=requires_image_input, @@ -233,7 +233,7 @@ async def stream_new_chat( llm_config_id = pin_result.llm_config_id # type: ignore[assignment] llm, agent_config, llm_load_error = await load_llm_bundle( - session, config_id=llm_config_id, search_space_id=search_space_id + session, config_id=llm_config_id, workspace_id=workspace_id ) if llm_load_error: yield emit_stream_error( @@ -273,7 +273,7 @@ async def stream_new_chat( pin_fallback = await resolve_initial_auto_pin( session, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, selected_llm_config_id=0, requires_image_input=requires_image_input, @@ -300,7 +300,7 @@ async def stream_new_chat( llm, agent_config, llm_load_error = await load_llm_bundle( session, config_id=llm_config_id, - search_space_id=search_space_id, + workspace_id=workspace_id, ) if llm_load_error: yield emit_stream_error( @@ -325,7 +325,7 @@ async def stream_new_chat( is_expected=True, request_id=request_id, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, message=( "Premium quota exhausted on pinned model; " @@ -377,7 +377,7 @@ async def stream_new_chat( _t0 = time.perf_counter() connector_service, firecrawl_api_key = await setup_connector_and_firecrawl( - session, search_space_id=search_space_id + session, workspace_id=workspace_id ) _perf_log.info( "[stream_new_chat] Connector service + firecrawl key in %.3fs", @@ -403,7 +403,7 @@ async def stream_new_chat( agent = await build_main_agent_for_thread( agent_factory, llm=llm, - search_space_id=search_space_id, + workspace_id=workspace_id, db_session=session, connector_service=connector_service, checkpointer=checkpointer, @@ -427,7 +427,7 @@ async def stream_new_chat( assembled = await build_new_chat_input_state( session, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_query=user_query, user_image_data_urls=user_image_data_urls, mentioned_document_ids=mentioned_document_ids, @@ -592,7 +592,7 @@ async def stream_new_chat( title_emitted = False runtime_context = build_new_chat_runtime_context( - search_space_id=search_space_id, + workspace_id=workspace_id, mentioned_document_ids=mentioned_document_ids, accepted_folder_ids=accepted_folder_ids, mentioned_folder_ids=mentioned_folder_ids, @@ -632,13 +632,13 @@ async def stream_new_chat( llm_config_id = await reroute_to_next_auto_pin( session, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, current_llm_config_id=llm_config_id, requires_image_input=requires_image_input, ) new_llm, new_agent_config, llm_load_err = await load_llm_bundle( - session, config_id=llm_config_id, search_space_id=search_space_id + session, config_id=llm_config_id, workspace_id=workspace_id ) if llm_load_err: # Re-raise the original so the terminal-error path classifies @@ -658,7 +658,7 @@ async def stream_new_chat( new_agent = await build_main_agent_for_thread( agent_factory, llm=llm, - search_space_id=search_space_id, + workspace_id=workspace_id, db_session=session, connector_service=connector_service, checkpointer=checkpointer, @@ -683,7 +683,7 @@ async def stream_new_chat( flow=flow, request_id=request_id, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, previous_config_id=previous_config_id, new_config_id=llm_config_id, @@ -700,7 +700,7 @@ async def stream_new_chat( initial_step_id=initial_step_id, initial_step_title=initial_step_title, initial_step_items=initial_step_items, - fallback_commit_search_space_id=search_space_id, + fallback_commit_workspace_id=workspace_id, fallback_commit_created_by_id=user_id, fallback_commit_filesystem_mode=( filesystem_selection.mode @@ -793,7 +793,7 @@ async def stream_new_chat( streaming_service=streaming_service, request_id=request_id, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, chat_span=chat_span, ) @@ -826,7 +826,7 @@ async def stream_new_chat( await finalize_assistant_message( stream_result=stream_result, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, accumulator=accumulator, log_prefix="stream_new_chat", diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py index 5ef2b8ad1..2b6c2b545 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py @@ -13,7 +13,7 @@ from app.agents.chat.shared.context import SurfSenseContextSchema def build_new_chat_runtime_context( *, - search_space_id: int, + workspace_id: int, mentioned_document_ids: list[int] | None, accepted_folder_ids: list[int], mentioned_folder_ids: list[int] | None, @@ -34,7 +34,7 @@ def build_new_chat_runtime_context( middleware reads them yet. """ return SurfSenseContextSchema( - search_space_id=search_space_id, + workspace_id=workspace_id, mentioned_document_ids=list(mentioned_document_ids or []), mentioned_folder_ids=list(accepted_folder_ids or mentioned_folder_ids or []), mentioned_connector_ids=list(mentioned_connector_ids or []), diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py index 33fcee3da..2bd0572ef 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py @@ -95,7 +95,7 @@ _perf_log = get_perf_logger() async def stream_resume_chat( chat_id: int, - search_space_id: int, + workspace_id: int, decisions: list[dict], user_id: str | None = None, llm_config_id: int = -1, @@ -127,7 +127,7 @@ async def stream_resume_chat( chat_error_category: str | None = None chat_span_cm, chat_span = open_chat_request_span( chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, flow="resume", request_id=request_id, turn_id=stream_result.turn_id, @@ -155,7 +155,7 @@ async def stream_resume_chat( flow="resume", request_id=request_id, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -177,7 +177,7 @@ async def stream_resume_chat( pinned = await resolve_or_get_pinned_llm_config_id( session, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, selected_llm_config_id=llm_config_id, ) @@ -200,7 +200,7 @@ async def stream_resume_chat( return llm, agent_config, llm_load_error = await load_llm_bundle( - session, config_id=llm_config_id, search_space_id=search_space_id + session, config_id=llm_config_id, workspace_id=workspace_id ) if llm_load_error: yield emit_stream_error( @@ -226,7 +226,7 @@ async def stream_resume_chat( pinned_fb = await resolve_or_get_pinned_llm_config_id( session, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, selected_llm_config_id=0, force_repin_free=True, @@ -250,7 +250,7 @@ async def stream_resume_chat( llm, agent_config, llm_load_error = await load_llm_bundle( session, config_id=llm_config_id, - search_space_id=search_space_id, + workspace_id=workspace_id, ) if llm_load_error: yield emit_stream_error( @@ -273,7 +273,7 @@ async def stream_resume_chat( is_expected=True, request_id=request_id, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, message=( "Premium quota exhausted on pinned model; " @@ -315,7 +315,7 @@ async def stream_resume_chat( _t0 = time.perf_counter() connector_service, firecrawl_api_key = await setup_connector_and_firecrawl( - session, search_space_id=search_space_id + session, workspace_id=workspace_id ) _perf_log.info( "[stream_resume] Connector service + firecrawl key in %.3fs", @@ -337,7 +337,7 @@ async def stream_resume_chat( agent = await build_main_agent_for_thread( agent_factory, llm=llm, - search_space_id=search_space_id, + workspace_id=workspace_id, db_session=session, connector_service=connector_service, checkpointer=checkpointer, @@ -423,7 +423,7 @@ async def stream_resume_chat( stream_result.content_builder = AssistantContentBuilder() runtime_context = build_resume_chat_runtime_context( - search_space_id=search_space_id, + workspace_id=workspace_id, request_id=request_id, turn_id=stream_result.turn_id, ) @@ -456,13 +456,13 @@ async def stream_resume_chat( llm_config_id = await reroute_to_next_auto_pin( session, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, current_llm_config_id=llm_config_id, requires_image_input=False, ) new_llm, new_agent_config, llm_load_err = await load_llm_bundle( - session, config_id=llm_config_id, search_space_id=search_space_id + session, config_id=llm_config_id, workspace_id=workspace_id ) if llm_load_err: return None @@ -473,7 +473,7 @@ async def stream_resume_chat( new_agent = await build_main_agent_for_thread( agent_factory, llm=llm, - search_space_id=search_space_id, + workspace_id=workspace_id, db_session=session, connector_service=connector_service, checkpointer=checkpointer, @@ -497,7 +497,7 @@ async def stream_resume_chat( flow="resume", request_id=request_id, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, previous_config_id=previous_config_id, new_config_id=llm_config_id, @@ -511,7 +511,7 @@ async def stream_resume_chat( input_data=Command(resume=routing.lg_resume_map), stream_result=stream_result, step_prefix=resume_step_prefix(stream_result.turn_id), - fallback_commit_search_space_id=search_space_id, + fallback_commit_workspace_id=workspace_id, fallback_commit_created_by_id=user_id, fallback_commit_filesystem_mode=( filesystem_selection.mode @@ -572,7 +572,7 @@ async def stream_resume_chat( streaming_service=streaming_service, request_id=request_id, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, chat_span=chat_span, ) @@ -595,7 +595,7 @@ async def stream_resume_chat( await finalize_assistant_message( stream_result=stream_result, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, accumulator=accumulator, log_prefix="stream_resume", diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/runtime_context.py b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/runtime_context.py index 54f0dfba0..a1be8c36b 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/runtime_context.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/runtime_context.py @@ -12,12 +12,12 @@ from app.agents.chat.shared.context import SurfSenseContextSchema def build_resume_chat_runtime_context( *, - search_space_id: int, + workspace_id: int, request_id: str | None, turn_id: str, ) -> SurfSenseContextSchema: return SurfSenseContextSchema( - search_space_id=search_space_id, + workspace_id=workspace_id, request_id=request_id, turn_id=turn_id, ) diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py index c59c2dcda..5622a42b8 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py @@ -70,7 +70,7 @@ async def finalize_assistant_message( *, stream_result: StreamResult | None, chat_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, accumulator: TokenAccumulator, log_prefix: str, @@ -140,7 +140,7 @@ async def finalize_assistant_message( await finalize_assistant_turn( message_id=stream_result.assistant_message_id, chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, turn_id=stream_result.turn_id, content=content_payload, diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/llm_bundle.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/llm_bundle.py index 6f905e8f4..a66a9e12a 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/llm_bundle.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/llm_bundle.py @@ -21,7 +21,7 @@ from app.agents.chat.runtime.llm_config import ( SanitizedChatLiteLLM, ) from app.config import config -from app.db import Model, SearchSpace +from app.db import Model, Workspace from app.services.model_capabilities import has_capability from app.services.model_resolver import to_litellm from app.services.token_tracking_service import register_model_usage_metadata @@ -55,11 +55,11 @@ def _agent_config_from_resolved( ) -async def _load_search_space( - session: AsyncSession, search_space_id: int -) -> SearchSpace | None: +async def _load_workspace( + session: AsyncSession, workspace_id: int +) -> Workspace | None: result = await session.execute( - select(SearchSpace).where(SearchSpace.id == search_space_id) + select(Workspace).where(Workspace.id == workspace_id) ) return result.scalars().first() @@ -68,7 +68,7 @@ async def _load_db_model( session: AsyncSession, *, model_id: int, - search_space: SearchSpace, + workspace: Workspace, ) -> Model | None: result = await session.execute( select(Model) @@ -79,9 +79,9 @@ async def _load_db_model( if not model or not model.connection or not model.connection.enabled: return None conn = model.connection - if conn.search_space_id is not None and conn.search_space_id != search_space.id: + if conn.workspace_id is not None and conn.workspace_id != workspace.id: return None - if conn.user_id is not None and conn.user_id != search_space.user_id: + if conn.user_id is not None and conn.user_id != workspace.user_id: return None return model @@ -90,17 +90,17 @@ async def load_llm_bundle( session: AsyncSession, *, config_id: int, - search_space_id: int, + workspace_id: int, ) -> tuple[Any, AgentConfig | None, str | None]: - search_space = await _load_search_space(session, search_space_id) - if not search_space: - return None, None, f"Search space {search_space_id} not found" + workspace = await _load_workspace(session, workspace_id) + if not workspace: + return None, None, f"Workspace {workspace_id} not found" if config_id > 0: model = await _load_db_model( session, model_id=config_id, - search_space=search_space, + workspace=workspace, ) if not model or not has_capability(model, "chat"): return ( diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/pre_stream_setup.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/pre_stream_setup.py index f717cb325..8c27aa935 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/pre_stream_setup.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/pre_stream_setup.py @@ -12,7 +12,7 @@ from app.services.connector_service import ConnectorService async def setup_connector_and_firecrawl( session: AsyncSession, *, - search_space_id: int, + workspace_id: int, ) -> tuple[ConnectorService, str | None]: """Build the per-turn connector service and pull the firecrawl API key. @@ -20,10 +20,10 @@ async def setup_connector_and_firecrawl( ``None`` when no web-crawler connector is configured (the agent simply skips firecrawl-backed tools in that case). """ - connector_service = ConnectorService(session, search_space_id=search_space_id) + connector_service = ConnectorService(session, workspace_id=workspace_id) firecrawl_api_key: str | None = None webcrawler_connector = await connector_service.get_connector_by_type( - SearchSourceConnectorType.WEBCRAWLER_CONNECTOR, search_space_id + SearchSourceConnectorType.WEBCRAWLER_CONNECTOR, workspace_id ) if webcrawler_connector and webcrawler_connector.config: firecrawl_api_key = webcrawler_connector.config.get("FIRECRAWL_API_KEY") diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/rate_limit_recovery.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/rate_limit_recovery.py index 29018fe07..4a0a15f54 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/rate_limit_recovery.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/rate_limit_recovery.py @@ -62,7 +62,7 @@ async def reroute_to_next_auto_pin( session: AsyncSession, *, chat_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, current_llm_config_id: int, requires_image_input: bool, @@ -79,7 +79,7 @@ async def reroute_to_next_auto_pin( pinned = await resolve_or_get_pinned_llm_config_id( session, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, selected_llm_config_id=0, exclude_config_ids={current_llm_config_id}, @@ -93,7 +93,7 @@ def log_rate_limit_recovered( flow: Literal["new", "regenerate", "resume"], request_id: str | None, chat_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, previous_config_id: int, new_config_id: int, @@ -115,7 +115,7 @@ def log_rate_limit_recovered( is_expected=True, request_id=request_id, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, message=( "Auto-pinned model hit runtime rate limit; switched to " diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py index 74b9682ed..18215c8f2 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py @@ -12,7 +12,7 @@ from app.observability import metrics as ot_metrics, otel as ot def open_chat_request_span( *, chat_id: int, - search_space_id: int, + workspace_id: int, flow: Literal["new", "regenerate", "resume"], request_id: str | None, turn_id: str, @@ -23,7 +23,7 @@ def open_chat_request_span( """Open the per-request span; returns ``(span_cm, span)`` for finally-close.""" span_cm = ot.chat_request_span( chat_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, flow=flow, request_id=request_id, turn_id=turn_id, diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/stream_loop.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/stream_loop.py index f455a8ffd..ff830fe1b 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/stream_loop.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/stream_loop.py @@ -37,7 +37,7 @@ async def run_stream_loop( initial_step_id: str | None = None, initial_step_title: str = "", initial_step_items: list[str] | None = None, - fallback_commit_search_space_id: int | None, + fallback_commit_workspace_id: int | None, fallback_commit_created_by_id: str | None, fallback_commit_filesystem_mode: FilesystemMode, fallback_commit_thread_id: int | None, @@ -64,7 +64,7 @@ async def run_stream_loop( initial_step_id=initial_step_id, initial_step_title=initial_step_title, initial_step_items=initial_step_items, - fallback_commit_search_space_id=fallback_commit_search_space_id, + fallback_commit_workspace_id=fallback_commit_workspace_id, fallback_commit_created_by_id=fallback_commit_created_by_id, fallback_commit_filesystem_mode=fallback_commit_filesystem_mode, fallback_commit_thread_id=fallback_commit_thread_id, diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py index 126149cc1..1e927d5d5 100644 --- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py +++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py @@ -34,7 +34,7 @@ def handle_terminal_exception( streaming_service: VercelStreamingService, request_id: str | None, chat_id: int, - search_space_id: int, + workspace_id: int, user_id: str | None, chat_span: Any, ) -> tuple[Iterator[str], dict[str, Any]]: @@ -87,7 +87,7 @@ def handle_terminal_exception( flow=flow, request_id=request_id, thread_id=chat_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, message=user_message, error_kind=error_kind, diff --git a/surfsense_backend/app/tasks/composio_indexer.py b/surfsense_backend/app/tasks/composio_indexer.py index 0518ad2a6..157988810 100644 --- a/surfsense_backend/app/tasks/composio_indexer.py +++ b/surfsense_backend/app/tasks/composio_indexer.py @@ -84,7 +84,7 @@ def get_indexer_function(toolkit_id: str): async def index_composio_connector( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -101,7 +101,7 @@ async def index_composio_connector( Args: session: Database session connector_id: ID of the Composio connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for filtering (YYYY-MM-DD format) end_date: End date for filtering (YYYY-MM-DD format) @@ -112,7 +112,7 @@ async def index_composio_connector( Returns: Tuple of (number_of_indexed_items, number_of_skipped_items, error_message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -180,7 +180,7 @@ async def index_composio_connector( "session": session, "connector": connector, "connector_id": connector_id, - "search_space_id": search_space_id, + "workspace_id": workspace_id, "user_id": user_id, "task_logger": task_logger, "log_entry": log_entry, diff --git a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py index e2a1b109a..9958031b1 100644 --- a/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/airtable_indexer.py @@ -44,7 +44,7 @@ HEARTBEAT_INTERVAL_SECONDS = 30 async def index_airtable_records( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -58,7 +58,7 @@ async def index_airtable_records( Args: session: Database session connector_id: ID of the Airtable connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: ID of the user start_date: Start date for filtering records (YYYY-MM-DD) end_date: End date for filtering records (YYYY-MM-DD) @@ -69,7 +69,7 @@ async def index_airtable_records( Returns: Tuple of (number_of_documents_processed, error_message) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="airtable_indexing", source="connector_indexing_task", @@ -259,12 +259,12 @@ async def index_airtable_records( unique_identifier_hash = generate_unique_identifier_hash( DocumentType.AIRTABLE_CONNECTOR, record_id, - search_space_id, + workspace_id, ) # Generate content hash content_hash = generate_content_hash( - markdown_content, search_space_id + markdown_content, workspace_id ) # Check if document with this unique identifier already exists @@ -323,7 +323,7 @@ async def index_airtable_records( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=record_id, document_type=DocumentType.AIRTABLE_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/base.py b/surfsense_backend/app/tasks/connector_indexers/base.py index 9408874ca..3a982cc4c 100644 --- a/surfsense_backend/app/tasks/connector_indexers/base.py +++ b/surfsense_backend/app/tasks/connector_indexers/base.py @@ -137,7 +137,7 @@ async def mark_connector_documents_failed( session: AsyncSession, *, document_type: DocumentType, - search_space_id: int, + workspace_id: int, failures: list[tuple[str, str]], ) -> int: """Transition placeholder/in-progress documents to ``failed`` by source id. @@ -159,7 +159,7 @@ async def mark_connector_documents_failed( if not unique_id: continue uid_hash = compute_identifier_hash( - document_type.value, unique_id, search_space_id + document_type.value, unique_id, workspace_id ) existing = await check_document_by_unique_identifier(session, uid_hash) if existing is None: diff --git a/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py index 6471ffb00..e00936cac 100644 --- a/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/bookstack_indexer.py @@ -45,7 +45,7 @@ HEARTBEAT_INTERVAL_SECONDS = 30 async def index_bookstack_pages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -58,7 +58,7 @@ async def index_bookstack_pages( Args: session: Database session connector_id: ID of the BookStack connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: User ID start_date: Start date for indexing (YYYY-MM-DD format) end_date: End date for indexing (YYYY-MM-DD format) @@ -68,7 +68,7 @@ async def index_bookstack_pages( Returns: Tuple containing (number of documents indexed, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -242,11 +242,11 @@ async def index_bookstack_pages( # Generate unique identifier hash for this BookStack page unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.BOOKSTACK_CONNECTOR, page_id, search_space_id + DocumentType.BOOKSTACK_CONNECTOR, page_id, workspace_id ) # Generate content hash - content_hash = generate_content_hash(full_content, search_space_id) + content_hash = generate_content_hash(full_content, workspace_id) # Check if document with this unique identifier already exists existing_document = await check_document_by_unique_identifier( @@ -307,7 +307,7 @@ async def index_bookstack_pages( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=page_name, document_type=DocumentType.BOOKSTACK_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py b/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py index 91763129f..8d67cad9e 100644 --- a/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/clickup_indexer.py @@ -44,7 +44,7 @@ HEARTBEAT_INTERVAL_SECONDS = 30 async def index_clickup_tasks( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -57,7 +57,7 @@ async def index_clickup_tasks( Args: session: Database session connector_id: ID of the ClickUp connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for filtering tasks (YYYY-MM-DD format) end_date: End date for filtering tasks (YYYY-MM-DD format) @@ -67,7 +67,7 @@ async def index_clickup_tasks( Returns: Tuple of (number of indexed tasks, error message if any) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -243,11 +243,11 @@ async def index_clickup_tasks( # Generate unique identifier hash for this ClickUp task unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.CLICKUP_CONNECTOR, task_id, search_space_id + DocumentType.CLICKUP_CONNECTOR, task_id, workspace_id ) # Generate content hash - content_hash = generate_content_hash(task_content, search_space_id) + content_hash = generate_content_hash(task_content, workspace_id) # Check if document with this unique identifier already exists existing_document = await check_document_by_unique_identifier( @@ -310,7 +310,7 @@ async def index_clickup_tasks( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=task_name, document_type=DocumentType.CLICKUP_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py index 53c438197..ec26757d4 100644 --- a/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/confluence_indexer.py @@ -34,7 +34,7 @@ def _build_connector_doc( full_content: str, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Map a raw Confluence page dict to a ConnectorDocument.""" @@ -58,7 +58,7 @@ def _build_connector_doc( source_markdown=full_content, unique_id=page_id, document_type=DocumentType.CONFLUENCE_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -68,7 +68,7 @@ def _build_connector_doc( async def index_confluence_pages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -76,7 +76,7 @@ async def index_confluence_pages( on_heartbeat_callback: HeartbeatCallbackType | None = None, ) -> tuple[int, int, str | None]: """Index Confluence pages and comments.""" - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="confluence_pages_indexing", source="connector_indexing_task", @@ -197,7 +197,7 @@ async def index_confluence_pages( title=page.get("title", ""), document_type=DocumentType.CONFLUENCE_CONNECTOR, unique_id=page.get("id", ""), - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata={ @@ -259,7 +259,7 @@ async def index_confluence_pages( page, full_content, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -309,7 +309,7 @@ async def index_confluence_pages( await mark_connector_documents_failed( session, document_type=DocumentType.CONFLUENCE_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=stuck_placeholders, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py index 8c5bd8f0e..564951abe 100644 --- a/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/discord_indexer.py @@ -116,7 +116,7 @@ def _build_batch_document_string( async def index_discord_messages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -139,7 +139,7 @@ async def index_discord_messages( Args: session: Database session connector_id: ID of the Discord connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: ID of the user start_date: Start date for indexing (YYYY-MM-DD format) end_date: End date for indexing (YYYY-MM-DD format) @@ -150,7 +150,7 @@ async def index_discord_messages( Returns: Tuple containing (number of documents indexed, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -506,12 +506,12 @@ async def index_discord_messages( unique_identifier_hash = generate_unique_identifier_hash( DocumentType.DISCORD_CONNECTOR, unique_identifier, - search_space_id, + workspace_id, ) # Generate content hash content_hash = generate_content_hash( - combined_document_string, search_space_id + combined_document_string, workspace_id ) # Check if document with this unique identifier already exists @@ -579,7 +579,7 @@ async def index_discord_messages( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=f"{guild_name}#{channel_name}", document_type=DocumentType.DISCORD_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/dropbox_indexer.py b/surfsense_backend/app/tasks/connector_indexers/dropbox_indexer.py index 9bf290d85..4eb44b976 100644 --- a/surfsense_backend/app/tasks/connector_indexers/dropbox_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/dropbox_indexer.py @@ -46,7 +46,7 @@ logger = logging.getLogger(__name__) async def _should_skip_file( session: AsyncSession, file: dict, - search_space_id: int, + workspace_id: int, ) -> tuple[bool, str | None]: """Pre-filter: detect unchanged / rename-only files.""" file_id = file.get("id", "") @@ -61,14 +61,14 @@ async def _should_skip_file( return True, "missing file_id" primary_hash = compute_identifier_hash( - DocumentType.DROPBOX_FILE.value, file_id, search_space_id + DocumentType.DROPBOX_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, primary_hash) if not existing: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.DROPBOX_FILE, cast(Document.document_metadata["dropbox_file_id"], String) == file_id, ) @@ -130,7 +130,7 @@ def _build_connector_doc( dropbox_metadata: dict, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: file_id = file.get("id", "") @@ -148,7 +148,7 @@ def _build_connector_doc( source_markdown=markdown, unique_id=file_id, document_type=DocumentType.DROPBOX_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -160,7 +160,7 @@ async def _download_files_parallel( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, max_concurrency: int = 3, on_heartbeat: HeartbeatCallbackType | None = None, @@ -194,7 +194,7 @@ async def _download_files_parallel( markdown, db_metadata, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) async with hb_lock: @@ -239,7 +239,7 @@ async def _download_and_index( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, on_heartbeat: HeartbeatCallbackType | None = None, vision_llm=None, @@ -249,7 +249,7 @@ async def _download_and_index( dropbox_client, files, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat, vision_llm=vision_llm, @@ -260,7 +260,7 @@ async def _download_and_index( await mark_connector_documents_failed( session, document_type=DocumentType.DROPBOX_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=failed_files, ) @@ -277,17 +277,17 @@ async def _download_and_index( return batch_indexed, len(failed_files) + batch_failed -async def _remove_document(session: AsyncSession, file_id: str, search_space_id: int): +async def _remove_document(session: AsyncSession, file_id: str, workspace_id: int): """Remove a document that was deleted in Dropbox.""" primary_hash = compute_identifier_hash( - DocumentType.DROPBOX_FILE.value, file_id, search_space_id + DocumentType.DROPBOX_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, primary_hash) if not existing: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.DROPBOX_FILE, cast(Document.document_metadata["dropbox_file_id"], String) == file_id, ) @@ -302,7 +302,7 @@ async def _index_with_delta_sync( dropbox_client: DropboxClient, session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, cursor: str, task_logger: TaskLoggingService, @@ -354,14 +354,14 @@ async def _index_with_delta_sync( name = entry.get("name", "") file_id = entry.get("id", "") if file_id: - await _remove_document(session, file_id, search_space_id) + await _remove_document(session, file_id, workspace_id) logger.debug(f"Processed deletion: {name or path_lower}") continue if tag != "file": continue - skip, msg = await _should_skip_file(session, entry, search_space_id) + skip, msg = await _should_skip_file(session, entry, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -378,7 +378,7 @@ async def _index_with_delta_sync( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, @@ -396,7 +396,7 @@ async def _index_full_scan( dropbox_client: DropboxClient, session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, folder_path: str, folder_name: str, @@ -448,7 +448,7 @@ async def _index_full_scan( for file in all_files[:max_files]: if incremental_sync: - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -491,7 +491,7 @@ async def _index_full_scan( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, @@ -517,7 +517,7 @@ async def _index_selected_files( file_paths: list[tuple[str, str | None]], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, incremental_sync: bool = True, on_heartbeat: HeartbeatCallbackType | None = None, @@ -542,7 +542,7 @@ async def _index_selected_files( continue if incremental_sync: - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -580,7 +580,7 @@ async def _index_selected_files( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat, vision_llm=vision_llm, @@ -598,7 +598,7 @@ async def _index_selected_files( async def index_dropbox_files( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ) -> tuple[int, int, str | None, int]: @@ -615,7 +615,7 @@ async def index_dropbox_files( } } """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="dropbox_files_indexing", source="connector_indexing_task", @@ -650,7 +650,7 @@ async def index_dropbox_files( if connector_enable_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(session, search_space_id) + vision_llm = await get_vision_llm(session, workspace_id) dropbox_client = DropboxClient(session, connector_id) @@ -677,7 +677,7 @@ async def index_dropbox_files( session, file_tuples, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, incremental_sync=incremental_sync, vision_llm=vision_llm, @@ -708,7 +708,7 @@ async def index_dropbox_files( dropbox_client, session, connector_id, - search_space_id, + workspace_id, user_id, saved_cursor, task_logger, @@ -724,7 +724,7 @@ async def index_dropbox_files( dropbox_client, session, connector_id, - search_space_id, + workspace_id, user_id, folder_path, folder_name, diff --git a/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py b/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py index ba0aa3445..ed0553b28 100644 --- a/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/elasticsearch_indexer.py @@ -45,7 +45,7 @@ logger = logging.getLogger(__name__) async def index_elasticsearch_documents( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str, end_date: str, @@ -58,7 +58,7 @@ async def index_elasticsearch_documents( Args: session: Database session connector_id: Elasticsearch connector ID - search_space_id: Search space ID + workspace_id: Workspace ID user_id: User ID start_date: Start date for indexing (not used for Elasticsearch, kept for compatibility) end_date: End date for indexing (not used for Elasticsearch, kept for compatibility) @@ -68,7 +68,7 @@ async def index_elasticsearch_documents( Returns: Tuple of (number of documents processed, error message if any) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="elasticsearch_indexing", source="connector_indexing_task", @@ -231,14 +231,14 @@ async def index_elasticsearch_documents( continue # Create content hash - content_hash = generate_content_hash(content, search_space_id) + content_hash = generate_content_hash(content, workspace_id) # Build source-unique identifier and hash (prefer source id dedupe) source_identifier = f"{hit.get('_index', index_name)}:{doc_id}" unique_identifier_hash = generate_unique_identifier_hash( DocumentType.ELASTICSEARCH_CONNECTOR, source_identifier, - search_space_id, + workspace_id, ) # Two-step duplicate detection: first by source-unique id, then by content hash @@ -304,7 +304,7 @@ async def index_elasticsearch_documents( unique_identifier_hash=unique_identifier_hash, document_type=DocumentType.ELASTICSEARCH_CONNECTOR, document_metadata=metadata, - search_space_id=search_space_id, + workspace_id=workspace_id, embedding=None, chunks=[], # Empty at creation - safe for async status=DocumentStatus.pending(), # Pending until processing starts diff --git a/surfsense_backend/app/tasks/connector_indexers/github_indexer.py b/surfsense_backend/app/tasks/connector_indexers/github_indexer.py index 557c2ce71..423f5dc1d 100644 --- a/surfsense_backend/app/tasks/connector_indexers/github_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/github_indexer.py @@ -51,7 +51,7 @@ MAX_DIGEST_CHARS = 500_000 # ~125k tokens async def index_github_repos( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, # Ignored - GitHub indexes full repo snapshots end_date: str | None = None, # Ignored - GitHub indexes full repo snapshots @@ -71,7 +71,7 @@ async def index_github_repos( Args: session: Database session connector_id: ID of the GitHub connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: ID of the user start_date: Ignored - kept for API compatibility end_date: Ignored - kept for API compatibility @@ -83,7 +83,7 @@ async def index_github_repos( """ # Note: start_date and end_date are intentionally unused _ = start_date, end_date - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -220,12 +220,12 @@ async def index_github_repos( # Generate unique identifier based on repo name unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.GITHUB_CONNECTOR, repo_full_name, search_space_id + DocumentType.GITHUB_CONNECTOR, repo_full_name, workspace_id ) # Generate content hash from digest full_content = digest.full_digest - content_hash = generate_content_hash(full_content, search_space_id) + content_hash = generate_content_hash(full_content, workspace_id) # Check if document with this unique identifier already exists existing_document = await check_document_by_unique_identifier( @@ -278,7 +278,7 @@ async def index_github_repos( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=repo_full_name, document_type=DocumentType.GITHUB_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py index 51df39171..c70849a08 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py @@ -51,7 +51,7 @@ def _build_connector_doc( event_markdown: str, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Map a raw Google Calendar API event dict to a ConnectorDocument.""" @@ -82,7 +82,7 @@ def _build_connector_doc( source_markdown=event_markdown, unique_id=event_id, document_type=DocumentType.GOOGLE_CALENDAR_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -92,7 +92,7 @@ def _build_connector_doc( async def index_google_calendar_events( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -105,7 +105,7 @@ async def index_google_calendar_events( Args: session: Database session connector_id: ID of the Google Calendar connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: User ID start_date: Start date for indexing (YYYY-MM-DD format). Can be in the past or future. end_date: End date for indexing (YYYY-MM-DD format). Can be in the future to index upcoming events. @@ -116,7 +116,7 @@ async def index_google_calendar_events( Returns: Tuple containing (number of documents indexed, number of documents skipped, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="google_calendar_events_indexing", @@ -374,7 +374,7 @@ async def index_google_calendar_events( title=event.get("summary", "No Title"), document_type=DocumentType.GOOGLE_CALENDAR_CONNECTOR, unique_id=event.get("id", ""), - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata={ @@ -413,7 +413,7 @@ async def index_google_calendar_events( event, event_markdown, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -462,7 +462,7 @@ async def index_google_calendar_events( await mark_connector_documents_failed( session, document_type=DocumentType.GOOGLE_CALENDAR_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=stuck_placeholders, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py index 37de66ffd..d6efd847b 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py @@ -273,7 +273,7 @@ def _build_drive_client_for_connector( async def _should_skip_file( session: AsyncSession, file: dict, - search_space_id: int, + workspace_id: int, ) -> tuple[bool, str | None]: """Pre-filter: detect unchanged / rename-only files. @@ -295,13 +295,13 @@ async def _should_skip_file( # --- locate existing document --- primary_hash = compute_identifier_hash( - DocumentType.GOOGLE_DRIVE_FILE.value, file_id, search_space_id + DocumentType.GOOGLE_DRIVE_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, primary_hash) if not existing: legacy_hash = compute_identifier_hash( - DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR.value, file_id, search_space_id + DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, legacy_hash) if existing: @@ -313,7 +313,7 @@ async def _should_skip_file( if not existing: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type.in_( [ DocumentType.GOOGLE_DRIVE_FILE, @@ -385,7 +385,7 @@ def _build_connector_doc( drive_metadata: dict, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Build a ConnectorDocument from Drive file metadata + extracted markdown.""" @@ -404,7 +404,7 @@ def _build_connector_doc( source_markdown=markdown, unique_id=file_id, document_type=DocumentType.GOOGLE_DRIVE_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -416,7 +416,7 @@ async def _create_drive_placeholders( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> None: """Create placeholder document rows for discovered Drive files. @@ -438,7 +438,7 @@ async def _create_drive_placeholders( title=file_name, document_type=DocumentType.GOOGLE_DRIVE_FILE, unique_id=file_id, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata={ @@ -460,7 +460,7 @@ async def _download_files_parallel( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, max_concurrency: int = 3, on_heartbeat: HeartbeatCallbackType | None = None, @@ -494,7 +494,7 @@ async def _download_files_parallel( markdown, drive_metadata, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) async with hb_lock: @@ -538,7 +538,7 @@ async def _process_single_file( session: AsyncSession, file: dict, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, vision_llm=None, ) -> tuple[int, int, int]: @@ -549,7 +549,7 @@ async def _process_single_file( file_name = file.get("name", "Unknown") try: - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and "renamed" in msg.lower(): return 1, 0, 0 @@ -572,7 +572,7 @@ async def _process_single_file( await mark_connector_documents_failed( session, document_type=DocumentType.GOOGLE_DRIVE_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=[(file_id, f"Download/ETL failed: {reason}")], ) return 0, 1, 0 @@ -582,7 +582,7 @@ async def _process_single_file( markdown, drive_metadata, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -611,23 +611,23 @@ async def _process_single_file( return 0, 0, 1 -async def _remove_document(session: AsyncSession, file_id: str, search_space_id: int): +async def _remove_document(session: AsyncSession, file_id: str, workspace_id: int): """Remove a document that was deleted in Drive.""" primary_hash = compute_identifier_hash( - DocumentType.GOOGLE_DRIVE_FILE.value, file_id, search_space_id + DocumentType.GOOGLE_DRIVE_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, primary_hash) if not existing: legacy_hash = compute_identifier_hash( - DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR.value, file_id, search_space_id + DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, legacy_hash) if not existing: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type.in_( [ DocumentType.GOOGLE_DRIVE_FILE, @@ -651,7 +651,7 @@ async def _download_and_index( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, on_heartbeat: HeartbeatCallbackType | None = None, vision_llm=None, @@ -664,7 +664,7 @@ async def _download_and_index( drive_client, files, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat, vision_llm=vision_llm, @@ -676,7 +676,7 @@ async def _download_and_index( await mark_connector_documents_failed( session, document_type=DocumentType.GOOGLE_DRIVE_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=failed_files, ) @@ -699,7 +699,7 @@ async def _index_selected_files( file_ids: list[tuple[str, str | None]], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, on_heartbeat: HeartbeatCallbackType | None = None, vision_llm=None, @@ -728,7 +728,7 @@ async def _index_selected_files( errors.append(f"File '{display}': {error or 'File not found'}") continue - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -757,7 +757,7 @@ async def _index_selected_files( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -766,7 +766,7 @@ async def _index_selected_files( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat, vision_llm=vision_llm, @@ -791,7 +791,7 @@ async def _index_full_scan( session: AsyncSession, connector: object, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, folder_id: str | None, folder_name: str, @@ -865,7 +865,7 @@ async def _index_full_scan( files_processed += 1 - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -920,7 +920,7 @@ async def _index_full_scan( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -932,7 +932,7 @@ async def _index_full_scan( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, @@ -957,7 +957,7 @@ async def _index_with_delta_sync( session: AsyncSession, connector: object, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, folder_id: str | None, start_page_token: str, @@ -1018,14 +1018,14 @@ async def _index_with_delta_sync( if change_type in ["removed", "trashed"]: fid = change.get("fileId") if fid: - await _remove_document(session, fid, search_space_id) + await _remove_document(session, fid, workspace_id) continue file = change.get("file") if not file: continue - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -1062,7 +1062,7 @@ async def _index_with_delta_sync( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -1074,7 +1074,7 @@ async def _index_with_delta_sync( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, @@ -1102,7 +1102,7 @@ async def _index_with_delta_sync( async def index_google_drive_files( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, folder_id: str | None = None, folder_name: str | None = None, @@ -1116,7 +1116,7 @@ async def index_google_drive_files( Returns (indexed, skipped, error_or_none, unsupported_count). """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="google_drive_files_indexing", source="connector_indexing_task", @@ -1166,7 +1166,7 @@ async def index_google_drive_files( if connector_enable_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(session, search_space_id) + vision_llm = await get_vision_llm(session, workspace_id) if not folder_id: error_msg = "folder_id is required for Google Drive indexing" await task_logger.log_task_failure( @@ -1198,7 +1198,7 @@ async def index_google_drive_files( session, connector, connector_id, - search_space_id, + workspace_id, user_id, target_folder_id, start_page_token, @@ -1216,7 +1216,7 @@ async def index_google_drive_files( session, connector, connector_id, - search_space_id, + workspace_id, user_id, target_folder_id, target_folder_name, @@ -1241,7 +1241,7 @@ async def index_google_drive_files( session, connector, connector_id, - search_space_id, + workspace_id, user_id, target_folder_id, target_folder_name, @@ -1317,13 +1317,13 @@ async def index_google_drive_files( async def index_google_drive_single_file( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, file_id: str, file_name: str | None = None, ) -> tuple[int, str | None]: """Index a single Google Drive file by its ID.""" - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="google_drive_single_file_indexing", source="connector_indexing_task", @@ -1366,7 +1366,7 @@ async def index_google_drive_single_file( if connector_enable_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(session, search_space_id) + vision_llm = await get_vision_llm(session, workspace_id) file, error = await get_file_by_id(drive_client, file_id) if error or not file: error_msg = f"Failed to fetch file {file_id}: {error or 'File not found'}" @@ -1382,7 +1382,7 @@ async def index_google_drive_single_file( session, file, connector_id, - search_space_id, + workspace_id, user_id, vision_llm=vision_llm, ) @@ -1430,7 +1430,7 @@ async def index_google_drive_single_file( async def index_google_drive_selected_files( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, files: list[tuple[str, str | None]], on_heartbeat_callback: HeartbeatCallbackType | None = None, @@ -1442,7 +1442,7 @@ async def index_google_drive_selected_files( Returns (indexed_count, skipped_count, errors). """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="google_drive_selected_files_indexing", source="connector_indexing_task", @@ -1485,13 +1485,13 @@ async def index_google_drive_selected_files( if connector_enable_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(session, search_space_id) + vision_llm = await get_vision_llm(session, workspace_id) indexed, skipped, unsupported, errors = await _index_selected_files( drive_client, session, files, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, diff --git a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py index 25da96b61..3f5fad889 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py @@ -103,7 +103,7 @@ def _build_connector_doc( markdown_content: str, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Map a raw Gmail API message dict to a ConnectorDocument.""" @@ -142,7 +142,7 @@ def _build_connector_doc( source_markdown=markdown_content, unique_id=message_id, document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -152,7 +152,7 @@ def _build_connector_doc( async def index_google_gmail_messages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -166,7 +166,7 @@ async def index_google_gmail_messages( Args: session: Database session connector_id: ID of the Gmail connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user start_date: Start date for filtering messages (YYYY-MM-DD format) end_date: End date for filtering messages (YYYY-MM-DD format) @@ -177,7 +177,7 @@ async def index_google_gmail_messages( Returns: Tuple of (number_of_indexed_messages, number_of_skipped_messages, status_message) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="google_gmail_messages_indexing", @@ -401,7 +401,7 @@ async def index_google_gmail_messages( title=_gmail_subject(msg), document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR, unique_id=msg.get("id", ""), - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata={ @@ -443,7 +443,7 @@ async def index_google_gmail_messages( message, markdown_content, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -493,7 +493,7 @@ async def index_google_gmail_messages( await mark_connector_documents_failed( session, document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=stuck_placeholders, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py b/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py index 2bde77f79..5ce85251b 100644 --- a/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/linear_indexer.py @@ -39,7 +39,7 @@ def _build_connector_doc( issue_content: str, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Map a raw Linear issue dict to a ConnectorDocument.""" @@ -67,7 +67,7 @@ def _build_connector_doc( source_markdown=issue_content, unique_id=issue_id, document_type=DocumentType.LINEAR_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -77,7 +77,7 @@ def _build_connector_doc( async def index_linear_issues( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -90,7 +90,7 @@ async def index_linear_issues( Returns: Tuple of (indexed_count, skipped_count, warning_or_error_message) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="linear_issues_indexing", @@ -213,7 +213,7 @@ async def index_linear_issues( title=f"{issue.get('identifier', '')}: {issue.get('title', '')}", document_type=DocumentType.LINEAR_CONNECTOR, unique_id=issue.get("id", ""), - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata={ @@ -267,7 +267,7 @@ async def index_linear_issues( formatted_issue, issue_content, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -317,7 +317,7 @@ async def index_linear_issues( await mark_connector_documents_failed( session, document_type=DocumentType.LINEAR_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=stuck_placeholders, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/local_folder_indexer.py b/surfsense_backend/app/tasks/connector_indexers/local_folder_indexer.py index 2505fa7c4..18c99b4df 100644 --- a/surfsense_backend/app/tasks/connector_indexers/local_folder_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/local_folder_indexer.py @@ -173,15 +173,15 @@ async def _read_file_content( return result.markdown_content -def _content_hash(content: str, search_space_id: int) -> str: - """SHA-256 hash of content scoped to a search space. +def _content_hash(content: str, workspace_id: int) -> str: + """SHA-256 hash of content scoped to a workspace. Matches the format used by ``compute_content_hash`` in the unified pipeline so that dedup checks are consistent. """ import hashlib - return hashlib.sha256(f"{search_space_id}:{content}".encode()).hexdigest() + return hashlib.sha256(f"{workspace_id}:{content}".encode()).hexdigest() def _compute_raw_file_hash(file_path: str) -> str: @@ -203,7 +203,7 @@ def _compute_raw_file_hash(file_path: str) -> str: async def _compute_file_content_hash( file_path: str, filename: str, - search_space_id: int, + workspace_id: int, *, vision_llm=None, processing_mode: str = "basic", @@ -215,14 +215,14 @@ async def _compute_file_content_hash( content = await _read_file_content( file_path, filename, vision_llm=vision_llm, processing_mode=processing_mode ) - return content, _content_hash(content, search_space_id) + return content, _content_hash(content, workspace_id) async def _mirror_folder_structure( session: AsyncSession, folder_path: str, folder_name: str, - search_space_id: int, + workspace_id: int, user_id: str, root_folder_id: int | None = None, exclude_patterns: list[str] | None = None, @@ -262,7 +262,7 @@ async def _mirror_folder_structure( if not root_folder_id: root_folder = Folder( name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, position="a0", ) @@ -283,7 +283,7 @@ async def _mirror_folder_structure( select(Folder).where( Folder.name == dir_name, Folder.parent_id == parent_id, - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, ) ) ).scalar_one_or_none() @@ -294,7 +294,7 @@ async def _mirror_folder_structure( new_folder = Folder( name=dir_name, parent_id=parent_id, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, position="a0", ) @@ -310,7 +310,7 @@ async def _resolve_folder_for_file( session: AsyncSession, rel_path: str, root_folder_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> int: """Given a file's relative path, ensure all parent Folder rows exist and @@ -333,7 +333,7 @@ async def _resolve_folder_for_file( select(Folder).where( Folder.name == part, Folder.parent_id == current_parent_id, - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, ) ) ).scalar_one_or_none() @@ -344,7 +344,7 @@ async def _resolve_folder_for_file( new_folder = Folder( name=part, parent_id=current_parent_id, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, position="a0", ) @@ -416,7 +416,7 @@ async def _cleanup_empty_folder_chain( async def _cleanup_empty_folders( session: AsyncSession, root_folder_id: int, - search_space_id: int, + workspace_id: int, existing_dirs_on_disk: set[str], folder_mapping: dict[str, int], subtree_ids: list[int] | None = None, @@ -427,7 +427,7 @@ async def _cleanup_empty_folders( id_to_rel: dict[int, str] = {fid: rel for rel, fid in folder_mapping.items() if rel} query = select(Folder).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, Folder.id != root_folder_id, ) if subtree_ids is not None: @@ -476,7 +476,7 @@ def _build_connector_doc( relative_path: str, folder_name: str, *, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Build a ConnectorDocument from a local file's extracted content.""" @@ -493,7 +493,7 @@ def _build_connector_doc( source_markdown=content, unique_id=unique_id, document_type=DocumentType.LOCAL_FOLDER_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=None, created_by_id=user_id, metadata=metadata, @@ -502,7 +502,7 @@ def _build_connector_doc( async def index_local_folder( session: AsyncSession, - search_space_id: int, + workspace_id: int, user_id: str, folder_path: str, folder_name: str, @@ -521,7 +521,7 @@ async def index_local_folder( Returns (indexed_count, skipped_count, root_folder_id, error_or_warning_message). """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="local_folder_indexing", @@ -564,7 +564,7 @@ async def index_local_folder( if len(target_file_paths) == 1: indexed, skipped, err = await _index_single_file( session=session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_path=folder_path, folder_name=folder_name, @@ -576,7 +576,7 @@ async def index_local_folder( return indexed, skipped, root_folder_id, err indexed, failed, err = await _index_batch_files( - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_path=folder_path, folder_name=folder_name, @@ -613,7 +613,7 @@ async def index_local_folder( session=session, folder_path=folder_path, folder_name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, root_folder_id=root_folder_id, exclude_patterns=exclude_patterns, @@ -654,7 +654,7 @@ async def index_local_folder( unique_identifier_hash = compute_identifier_hash( DocumentType.LOCAL_FOLDER_FILE.value, unique_identifier, - search_space_id, + workspace_id, ) seen_unique_hashes.add(unique_identifier_hash) @@ -709,7 +709,7 @@ async def index_local_folder( content, content_hash = await _compute_file_content_hash( file_path_abs, file_info["relative_path"], - search_space_id, + workspace_id, ) except Exception as read_err: logger.warning(f"Could not read {file_path_abs}: {read_err}") @@ -745,7 +745,7 @@ async def index_local_folder( content, content_hash = await _compute_file_content_hash( file_path_abs, file_info["relative_path"], - search_space_id, + workspace_id, ) except Exception as read_err: logger.warning(f"Could not read {file_path_abs}: {read_err}") @@ -765,7 +765,7 @@ async def index_local_folder( content=content, relative_path=relative_path, folder_name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) connector_docs.append(doc) @@ -789,7 +789,7 @@ async def index_local_folder( ( await session.execute( select(Folder.id).where( - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, ) ) ) @@ -803,7 +803,7 @@ async def index_local_folder( await session.execute( select(Document).where( Document.document_type == DocumentType.LOCAL_FOLDER_FILE, - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.folder_id.in_(list(all_root_folder_ids)), ) ) @@ -886,7 +886,7 @@ async def index_local_folder( await _cleanup_empty_folders( session, root_fid, - search_space_id, + workspace_id, existing_dirs, folder_mapping, subtree_ids=subtree_ids, @@ -943,7 +943,7 @@ BATCH_CONCURRENCY = 5 async def _index_batch_files( - search_space_id: int, + workspace_id: int, user_id: str, folder_path: str, folder_name: str, @@ -968,7 +968,7 @@ async def _index_batch_files( async with semaphore: try: async with get_celery_session_maker()() as file_session: - task_logger = TaskLoggingService(file_session, search_space_id) + task_logger = TaskLoggingService(file_session, workspace_id) log_entry = await task_logger.log_task_start( task_name="local_folder_indexing", source="local_folder_batch_indexing", @@ -977,7 +977,7 @@ async def _index_batch_files( ) ix, _sk, err = await _index_single_file( session=file_session, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, folder_path=folder_path, folder_name=folder_name, @@ -1017,7 +1017,7 @@ async def _index_batch_files( async def _index_single_file( session: AsyncSession, - search_space_id: int, + workspace_id: int, user_id: str, folder_path: str, folder_name: str, @@ -1033,7 +1033,7 @@ async def _index_single_file( rel = str(full_path.relative_to(folder_path)) unique_id = f"{folder_name}:{rel}" uid_hash = compute_identifier_hash( - DocumentType.LOCAL_FOLDER_FILE.value, unique_id, search_space_id + DocumentType.LOCAL_FOLDER_FILE.value, unique_id, workspace_id ) existing = await check_document_by_unique_identifier(session, uid_hash) if existing: @@ -1052,7 +1052,7 @@ async def _index_single_file( unique_id = f"{folder_name}:{rel_path}" uid_hash = compute_identifier_hash( - DocumentType.LOCAL_FOLDER_FILE.value, unique_id, search_space_id + DocumentType.LOCAL_FOLDER_FILE.value, unique_id, workspace_id ) raw_hash = await asyncio.to_thread(_compute_raw_file_hash, str(full_path)) @@ -1081,7 +1081,7 @@ async def _index_single_file( try: content, content_hash = await _compute_file_content_hash( - str(full_path), full_path.name, search_space_id + str(full_path), full_path.name, workspace_id ) except Exception as e: return 0, 1, f"Could not read file: {e}" @@ -1108,13 +1108,13 @@ async def _index_single_file( content=content, relative_path=rel_path, folder_name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) if root_folder_id: connector_doc.folder_id = await _resolve_folder_for_file( - session, rel_path, root_folder_id, search_space_id, user_id + session, rel_path, root_folder_id, workspace_id, user_id ) pipeline = IndexingPipelineService(session) @@ -1166,7 +1166,7 @@ async def _mirror_folder_structure_from_paths( session: AsyncSession, relative_paths: list[str], folder_name: str, - search_space_id: int, + workspace_id: int, user_id: str, root_folder_id: int | None = None, ) -> tuple[dict[str, int], int]: @@ -1203,7 +1203,7 @@ async def _mirror_folder_structure_from_paths( if not root_folder_id: root_folder = Folder( name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, position="a0", ) @@ -1224,7 +1224,7 @@ async def _mirror_folder_structure_from_paths( select(Folder).where( Folder.name == dir_name, Folder.parent_id == parent_id, - Folder.search_space_id == search_space_id, + Folder.workspace_id == workspace_id, ) ) ).scalar_one_or_none() @@ -1235,7 +1235,7 @@ async def _mirror_folder_structure_from_paths( new_folder = Folder( name=dir_name, parent_id=parent_id, - search_space_id=search_space_id, + workspace_id=workspace_id, created_by_id=user_id, position="a0", ) @@ -1252,7 +1252,7 @@ UPLOAD_BATCH_CONCURRENCY = 5 async def index_uploaded_files( session: AsyncSession, - search_space_id: int, + workspace_id: int, user_id: str, folder_name: str, root_folder_id: int, @@ -1274,7 +1274,7 @@ async def index_uploaded_files( mode = ProcessingMode.coerce(processing_mode) - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="local_folder_indexing", source="uploaded_folder_indexing", @@ -1288,7 +1288,7 @@ async def index_uploaded_files( session=session, relative_paths=all_relative_paths, folder_name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, root_folder_id=root_folder_id, ) @@ -1303,7 +1303,7 @@ async def index_uploaded_files( if use_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm_instance = await get_vision_llm(session, search_space_id) + vision_llm_instance = await get_vision_llm(session, workspace_id) indexed_count = 0 failed_count = 0 @@ -1319,7 +1319,7 @@ async def index_uploaded_files( uid_hash = compute_identifier_hash( DocumentType.LOCAL_FOLDER_FILE.value, unique_id, - search_space_id, + workspace_id, ) raw_hash = await asyncio.to_thread(_compute_raw_file_hash, temp_path) @@ -1357,7 +1357,7 @@ async def index_uploaded_files( content, content_hash = await _compute_file_content_hash( temp_path, filename, - search_space_id, + workspace_id, vision_llm=vision_llm_instance, processing_mode=mode.value, ) @@ -1391,7 +1391,7 @@ async def index_uploaded_files( content=content, relative_path=relative_path, folder_name=folder_name, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -1399,7 +1399,7 @@ async def index_uploaded_files( session, relative_path, root_folder_id, - search_space_id, + workspace_id, user_id, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py b/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py index eab2c9793..79a81f319 100644 --- a/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/luma_indexer.py @@ -45,7 +45,7 @@ HEARTBEAT_INTERVAL_SECONDS = 30 async def index_luma_events( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -58,7 +58,7 @@ async def index_luma_events( Args: session: Database session connector_id: ID of the Luma connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: User ID start_date: Start date for indexing (YYYY-MM-DD format). Can be in the past or future. end_date: End date for indexing (YYYY-MM-DD format). Can be in the future to index upcoming events. @@ -69,7 +69,7 @@ async def index_luma_events( Returns: Tuple containing (number of documents indexed, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -290,11 +290,11 @@ async def index_luma_events( # Generate unique identifier hash for this Luma event unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.LUMA_CONNECTOR, event_id, search_space_id + DocumentType.LUMA_CONNECTOR, event_id, workspace_id ) # Generate content hash - content_hash = generate_content_hash(event_markdown, search_space_id) + content_hash = generate_content_hash(event_markdown, workspace_id) # Check if document with this unique identifier already exists existing_document = await check_document_by_unique_identifier( @@ -355,7 +355,7 @@ async def index_luma_events( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=event_name, document_type=DocumentType.LUMA_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py b/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py index 9ebafbcdb..21e67af73 100644 --- a/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/notion_indexer.py @@ -41,7 +41,7 @@ def _build_connector_doc( markdown_content: str, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: """Map a raw Notion page dict to a ConnectorDocument.""" @@ -61,7 +61,7 @@ def _build_connector_doc( source_markdown=markdown_content, unique_id=page_id, document_type=DocumentType.NOTION_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -71,7 +71,7 @@ def _build_connector_doc( async def index_notion_pages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -85,7 +85,7 @@ async def index_notion_pages( Returns: Tuple of (indexed_count, skipped_count, warning_or_error_message) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="notion_pages_indexing", @@ -255,7 +255,7 @@ async def index_notion_pages( title=page.get("title", f"Untitled page ({page.get('page_id', '')})"), document_type=DocumentType.NOTION_CONNECTOR, unique_id=page.get("page_id", ""), - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata={ @@ -307,7 +307,7 @@ async def index_notion_pages( page, markdown_content, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) @@ -357,7 +357,7 @@ async def index_notion_pages( await mark_connector_documents_failed( session, document_type=DocumentType.NOTION_CONNECTOR, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=stuck_placeholders, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/onedrive_indexer.py b/surfsense_backend/app/tasks/connector_indexers/onedrive_indexer.py index 1a83551fb..395b98746 100644 --- a/surfsense_backend/app/tasks/connector_indexers/onedrive_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/onedrive_indexer.py @@ -51,7 +51,7 @@ logger = logging.getLogger(__name__) async def _should_skip_file( session: AsyncSession, file: dict, - search_space_id: int, + workspace_id: int, ) -> tuple[bool, str | None]: """Pre-filter: detect unchanged / rename-only files.""" file_id = file.get("id") @@ -66,14 +66,14 @@ async def _should_skip_file( return True, "missing file_id" primary_hash = compute_identifier_hash( - DocumentType.ONEDRIVE_FILE.value, file_id, search_space_id + DocumentType.ONEDRIVE_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, primary_hash) if not existing: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.ONEDRIVE_FILE, cast(Document.document_metadata["onedrive_file_id"], String) == file_id, ) @@ -137,7 +137,7 @@ def _build_connector_doc( onedrive_metadata: dict, *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, ) -> ConnectorDocument: file_id = file.get("id", "") @@ -155,7 +155,7 @@ def _build_connector_doc( source_markdown=markdown, unique_id=file_id, document_type=DocumentType.ONEDRIVE_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, connector_id=connector_id, created_by_id=user_id, metadata=metadata, @@ -167,7 +167,7 @@ async def _download_files_parallel( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, max_concurrency: int = 3, on_heartbeat: HeartbeatCallbackType | None = None, @@ -201,7 +201,7 @@ async def _download_files_parallel( markdown, od_metadata, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) async with hb_lock: @@ -246,7 +246,7 @@ async def _download_and_index( files: list[dict], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, on_heartbeat: HeartbeatCallbackType | None = None, vision_llm=None, @@ -256,7 +256,7 @@ async def _download_and_index( onedrive_client, files, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat, vision_llm=vision_llm, @@ -267,7 +267,7 @@ async def _download_and_index( await mark_connector_documents_failed( session, document_type=DocumentType.ONEDRIVE_FILE, - search_space_id=search_space_id, + workspace_id=workspace_id, failures=failed_files, ) @@ -284,17 +284,17 @@ async def _download_and_index( return batch_indexed, len(failed_files) + batch_failed -async def _remove_document(session: AsyncSession, file_id: str, search_space_id: int): +async def _remove_document(session: AsyncSession, file_id: str, workspace_id: int): """Remove a document that was deleted in OneDrive.""" primary_hash = compute_identifier_hash( - DocumentType.ONEDRIVE_FILE.value, file_id, search_space_id + DocumentType.ONEDRIVE_FILE.value, file_id, workspace_id ) existing = await check_document_by_unique_identifier(session, primary_hash) if not existing: result = await session.execute( select(Document).where( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.ONEDRIVE_FILE, cast(Document.document_metadata["onedrive_file_id"], String) == file_id, ) @@ -312,7 +312,7 @@ async def _index_selected_files( file_ids: list[tuple[str, str | None]], *, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, on_heartbeat: HeartbeatCallbackType | None = None, vision_llm=None, @@ -335,7 +335,7 @@ async def _index_selected_files( errors.append(f"File '{display}': {error or 'File not found'}") continue - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -365,7 +365,7 @@ async def _index_selected_files( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat, vision_llm=vision_llm, @@ -389,7 +389,7 @@ async def _index_full_scan( onedrive_client: OneDriveClient, session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, folder_id: str, folder_name: str, @@ -438,7 +438,7 @@ async def _index_full_scan( raise Exception(f"Failed to list OneDrive files: {error}") for file in all_files[:max_files]: - skip, msg = await _should_skip_file(session, file, search_space_id) + skip, msg = await _should_skip_file(session, file, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -473,7 +473,7 @@ async def _index_full_scan( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, @@ -497,7 +497,7 @@ async def _index_with_delta_sync( onedrive_client: OneDriveClient, session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, folder_id: str | None, delta_link: str, @@ -553,7 +553,7 @@ async def _index_with_delta_sync( if change.get("deleted"): fid = change.get("id") if fid: - await _remove_document(session, fid, search_space_id) + await _remove_document(session, fid, workspace_id) continue if "folder" in change: @@ -562,7 +562,7 @@ async def _index_with_delta_sync( if not change.get("file"): continue - skip, msg = await _should_skip_file(session, change, search_space_id) + skip, msg = await _should_skip_file(session, change, workspace_id) if skip: if msg and msg.startswith("unsupported:"): unsupported_count += 1 @@ -597,7 +597,7 @@ async def _index_with_delta_sync( session, files_to_download, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, on_heartbeat=on_heartbeat_callback, vision_llm=vision_llm, @@ -625,7 +625,7 @@ async def _index_with_delta_sync( async def index_onedrive_files( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, items_dict: dict, ) -> tuple[int, int, str | None, int]: @@ -638,7 +638,7 @@ async def index_onedrive_files( "indexing_options": {"max_files": 500, "include_subfolders": true, "use_delta_sync": true} } """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) log_entry = await task_logger.log_task_start( task_name="onedrive_files_indexing", source="connector_indexing_task", @@ -673,7 +673,7 @@ async def index_onedrive_files( if connector_enable_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(session, search_space_id) + vision_llm = await get_vision_llm(session, workspace_id) onedrive_client = OneDriveClient(session, connector_id) @@ -695,7 +695,7 @@ async def index_onedrive_files( session, file_tuples, connector_id=connector_id, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, vision_llm=vision_llm, ) @@ -719,7 +719,7 @@ async def index_onedrive_files( onedrive_client, session, connector_id, - search_space_id, + workspace_id, user_id, folder_id, delta_link, @@ -744,7 +744,7 @@ async def index_onedrive_files( onedrive_client, session, connector_id, - search_space_id, + workspace_id, user_id, folder_id, folder_name, @@ -763,7 +763,7 @@ async def index_onedrive_files( onedrive_client, session, connector_id, - search_space_id, + workspace_id, user_id, folder_id, folder_name, diff --git a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py index ac63af38c..6b19bf7c4 100644 --- a/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/slack_indexer.py @@ -116,7 +116,7 @@ def _build_batch_document_string( async def index_slack_messages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -139,7 +139,7 @@ async def index_slack_messages( Args: session: Database session connector_id: ID of the Slack connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: ID of the user start_date: Start date for indexing (YYYY-MM-DD format) end_date: End date for indexing (YYYY-MM-DD format) @@ -150,7 +150,7 @@ async def index_slack_messages( Returns: Tuple containing (number of documents indexed, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -380,12 +380,12 @@ async def index_slack_messages( unique_identifier_hash = generate_unique_identifier_hash( DocumentType.SLACK_CONNECTOR, unique_identifier, - search_space_id, + workspace_id, ) # Generate content hash content_hash = generate_content_hash( - combined_document_string, search_space_id + combined_document_string, workspace_id ) # Check if document with this unique identifier already exists @@ -449,7 +449,7 @@ async def index_slack_messages( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=f"{team_name}#{channel_name}", document_type=DocumentType.SLACK_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py b/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py index e48aedaa5..90dc50fc5 100644 --- a/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/teams_indexer.py @@ -116,7 +116,7 @@ def _build_batch_document_string( async def index_teams_messages( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -139,7 +139,7 @@ async def index_teams_messages( Args: session: Database session connector_id: ID of the Teams connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: ID of the user start_date: Start date for indexing (YYYY-MM-DD format) end_date: End date for indexing (YYYY-MM-DD format) @@ -150,7 +150,7 @@ async def index_teams_messages( Returns: Tuple containing (number of documents indexed, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -398,12 +398,12 @@ async def index_teams_messages( unique_identifier_hash = generate_unique_identifier_hash( DocumentType.TEAMS_CONNECTOR, unique_identifier, - search_space_id, + workspace_id, ) # Generate content hash content_hash = generate_content_hash( - combined_document_string, search_space_id + combined_document_string, workspace_id ) # Check if document with this unique identifier already exists @@ -475,7 +475,7 @@ async def index_teams_messages( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=f"{team_name} - {channel_name}", document_type=DocumentType.TEAMS_CONNECTOR, document_metadata={ diff --git a/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py b/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py index d81de67c0..2390804a4 100644 --- a/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/webcrawler_indexer.py @@ -44,7 +44,7 @@ HEARTBEAT_INTERVAL_SECONDS = 30 async def index_crawled_urls( session: AsyncSession, connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, start_date: str | None = None, end_date: str | None = None, @@ -61,7 +61,7 @@ async def index_crawled_urls( Args: session: Database session connector_id: ID of the webcrawler connector - search_space_id: ID of the search space to store documents in + workspace_id: ID of the workspace to store documents in user_id: User ID start_date: Start date for filtering (YYYY-MM-DD format) - optional end_date: End date for filtering (YYYY-MM-DD format) - optional @@ -71,7 +71,7 @@ async def index_crawled_urls( Returns: Tuple containing (number of documents indexed, error message or None) """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -184,7 +184,7 @@ async def index_crawled_urls( try: # Generate unique identifier hash for this URL unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.CRAWLED_URL, url, search_space_id + DocumentType.CRAWLED_URL, url, workspace_id ) # Check if document with this unique identifier already exists @@ -220,7 +220,7 @@ async def index_crawled_urls( # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=url[:100], # Placeholder - URL as title (truncated) document_type=DocumentType.CRAWLED_URL, document_metadata={ @@ -325,7 +325,7 @@ async def index_crawled_urls( crawl_result, exclude_metadata=True ) content_hash = generate_content_hash( - structured_document_for_hash, search_space_id + structured_document_for_hash, workspace_id ) # Extract useful metadata @@ -500,15 +500,15 @@ async def index_crawled_urls( async def get_crawled_url_documents( session: AsyncSession, - search_space_id: int, + workspace_id: int, connector_id: int | None = None, ) -> list[Document]: """ - Get all crawled URL documents for a search space. + Get all crawled URL documents for a workspace. Args: session: Database session - search_space_id: ID of the search space + workspace_id: ID of the workspace connector_id: Optional connector ID to filter by Returns: @@ -517,7 +517,7 @@ async def get_crawled_url_documents( from sqlalchemy import select query = select(Document).filter( - Document.search_space_id == search_space_id, + Document.workspace_id == workspace_id, Document.document_type == DocumentType.CRAWLED_URL, ) diff --git a/surfsense_backend/app/tasks/document_processors/_helpers.py b/surfsense_backend/app/tasks/document_processors/_helpers.py index 9cd7b87c9..9db655328 100644 --- a/surfsense_backend/app/tasks/document_processors/_helpers.py +++ b/surfsense_backend/app/tasks/document_processors/_helpers.py @@ -24,7 +24,7 @@ from .base import ( def get_google_drive_unique_identifier( connector: dict | None, filename: str, - search_space_id: int, + workspace_id: int, ) -> tuple[str, str | None]: """ Get unique identifier hash, using file_id for Google Drive (stable across renames). @@ -40,15 +40,15 @@ def get_google_drive_unique_identifier( if file_id: primary_hash = generate_unique_identifier_hash( - DocumentType.GOOGLE_DRIVE_FILE, file_id, search_space_id + DocumentType.GOOGLE_DRIVE_FILE, file_id, workspace_id ) legacy_hash = generate_unique_identifier_hash( - DocumentType.GOOGLE_DRIVE_FILE, filename, search_space_id + DocumentType.GOOGLE_DRIVE_FILE, filename, workspace_id ) return primary_hash, legacy_hash primary_hash = generate_unique_identifier_hash( - DocumentType.FILE, filename, search_space_id + DocumentType.FILE, filename, workspace_id ) return primary_hash, None diff --git a/surfsense_backend/app/tasks/document_processors/_save.py b/surfsense_backend/app/tasks/document_processors/_save.py index 3b9616cbd..e30666a20 100644 --- a/surfsense_backend/app/tasks/document_processors/_save.py +++ b/surfsense_backend/app/tasks/document_processors/_save.py @@ -28,7 +28,7 @@ async def save_file_document( session: AsyncSession, file_name: str, markdown_content: str, - search_space_id: int, + workspace_id: int, user_id: str, etl_service: str, connector: dict | None = None, @@ -43,7 +43,7 @@ async def save_file_document( session: Database session file_name: Name of the processed file markdown_content: Markdown content to store - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user etl_service: Name of the ETL service (UNSTRUCTURED, LLAMACLOUD, DOCLING) connector: Optional connector info for Google Drive files @@ -53,9 +53,9 @@ async def save_file_document( """ try: primary_hash, legacy_hash = get_google_drive_unique_identifier( - connector, file_name, search_space_id + connector, file_name, workspace_id ) - content_hash = generate_content_hash(markdown_content, search_space_id) + content_hash = generate_content_hash(markdown_content, workspace_id) existing_document = await find_existing_document_with_migration( session, primary_hash, legacy_hash, content_hash @@ -99,7 +99,7 @@ async def save_file_document( doc_type = DocumentType.GOOGLE_DRIVE_FILE document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=file_name, document_type=doc_type, document_metadata=doc_metadata, diff --git a/surfsense_backend/app/tasks/document_processors/circleback_processor.py b/surfsense_backend/app/tasks/document_processors/circleback_processor.py index ee36d5bc2..413ab9d3d 100644 --- a/surfsense_backend/app/tasks/document_processors/circleback_processor.py +++ b/surfsense_backend/app/tasks/document_processors/circleback_processor.py @@ -23,7 +23,7 @@ from app.db import ( DocumentType, SearchSourceConnector, SearchSourceConnectorType, - SearchSpace, + Workspace, ) from app.utils.document_converters import ( create_document_chunks, @@ -47,7 +47,7 @@ async def add_circleback_meeting_document( meeting_name: str, markdown_content: str, metadata: dict[str, Any], - search_space_id: int, + workspace_id: int, connector_id: int | None = None, ) -> Document | None: """ @@ -64,7 +64,7 @@ async def add_circleback_meeting_document( meeting_name: Name of the meeting markdown_content: Meeting content formatted as markdown metadata: Meeting metadata dictionary - search_space_id: ID of the search space + workspace_id: ID of the workspace connector_id: ID of the Circleback connector (for deletion support) Returns: @@ -75,11 +75,11 @@ async def add_circleback_meeting_document( # Generate unique identifier hash using Circleback meeting ID unique_identifier = f"circleback_{meeting_id}" unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.CIRCLEBACK, unique_identifier, search_space_id + DocumentType.CIRCLEBACK, unique_identifier, workspace_id ) # Generate content hash - content_hash = generate_content_hash(markdown_content, search_space_id) + content_hash = generate_content_hash(markdown_content, workspace_id) # Check if document with this unique identifier already exists existing_document = await check_document_by_unique_identifier( @@ -113,13 +113,13 @@ async def add_circleback_meeting_document( # ======================================================================= # Fetch the user who set up the Circleback connector (preferred) - # or fall back to search space owner if no connector found + # or fall back to workspace owner if no connector found created_by_user_id = None - # Try to find the Circleback connector for this search space + # Try to find the Circleback connector for this workspace connector_result = await session.execute( select(SearchSourceConnector.user_id).where( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.connector_type == SearchSourceConnectorType.CIRCLEBACK_CONNECTOR, ) @@ -130,15 +130,15 @@ async def add_circleback_meeting_document( # Use the user who set up the Circleback connector created_by_user_id = connector_user else: - # Fallback: use search space owner if no connector found - search_space_result = await session.execute( - select(SearchSpace.user_id).where(SearchSpace.id == search_space_id) + # Fallback: use workspace owner if no connector found + workspace_result = await session.execute( + select(Workspace.user_id).where(Workspace.id == workspace_id) ) - created_by_user_id = search_space_result.scalar_one_or_none() + created_by_user_id = workspace_result.scalar_one_or_none() # Create new document with PENDING status (visible in UI immediately) document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=meeting_name, document_type=DocumentType.CIRCLEBACK, document_metadata={ @@ -162,7 +162,7 @@ async def add_circleback_meeting_document( # Commit immediately so document appears in UI with pending status await session.commit() logger.info( - f"Created pending Circleback meeting document {meeting_id} in search space {search_space_id}" + f"Created pending Circleback meeting document {meeting_id} in workspace {workspace_id}" ) # ======================================================================= @@ -213,11 +213,11 @@ async def add_circleback_meeting_document( if existing_document: logger.info( - f"Updated Circleback meeting document {meeting_id} in search space {search_space_id}" + f"Updated Circleback meeting document {meeting_id} in workspace {workspace_id}" ) else: logger.info( - f"Processed Circleback meeting document {meeting_id} in search space {search_space_id} - now ready" + f"Processed Circleback meeting document {meeting_id} in workspace {workspace_id} - now ready" ) return document diff --git a/surfsense_backend/app/tasks/document_processors/extension_processor.py b/surfsense_backend/app/tasks/document_processors/extension_processor.py index bdbc985fa..7453fabed 100644 --- a/surfsense_backend/app/tasks/document_processors/extension_processor.py +++ b/surfsense_backend/app/tasks/document_processors/extension_processor.py @@ -27,7 +27,7 @@ from .base import ( async def add_extension_received_document( session: AsyncSession, content: ExtensionDocumentContent, - search_space_id: int, + workspace_id: int, user_id: str, ) -> Document | None: """ @@ -36,13 +36,13 @@ async def add_extension_received_document( Args: session: Database session content: Document content from extension - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user Returns: Document object if successful, None if failed """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -90,11 +90,11 @@ async def add_extension_received_document( # Generate unique identifier hash for this extension document (using URL) unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.EXTENSION, content.metadata.VisitedWebPageURL, search_space_id + DocumentType.EXTENSION, content.metadata.VisitedWebPageURL, workspace_id ) # Generate content hash - content_hash = generate_content_hash(combined_document_string, search_space_id) + content_hash = generate_content_hash(combined_document_string, workspace_id) # Check if document with this unique identifier already exists existing_document = await check_document_by_unique_identifier( @@ -146,7 +146,7 @@ async def add_extension_received_document( else: # Create new document document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=content.metadata.VisitedWebPageTitle, document_type=DocumentType.EXTENSION, document_metadata=content.metadata.model_dump(), diff --git a/surfsense_backend/app/tasks/document_processors/file_processors.py b/surfsense_backend/app/tasks/document_processors/file_processors.py index 174ac966d..63862b62a 100644 --- a/surfsense_backend/app/tasks/document_processors/file_processors.py +++ b/surfsense_backend/app/tasks/document_processors/file_processors.py @@ -42,7 +42,7 @@ class _ProcessingContext: session: AsyncSession file_path: str filename: str - search_space_id: int + workspace_id: int user_id: str task_logger: TaskLoggingService log_entry: Log @@ -135,7 +135,7 @@ async def _process_non_document_upload(ctx: _ProcessingContext) -> Document | No if ctx.use_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(ctx.session, ctx.search_space_id) + vision_llm = await get_vision_llm(ctx.session, ctx.workspace_id) etl_result = await extract_with_cache( EtlRequest(file_path=ctx.file_path, filename=ctx.filename), @@ -151,7 +151,7 @@ async def _process_non_document_upload(ctx: _ProcessingContext) -> Document | No ctx.session, ctx.filename, etl_result.markdown_content, - ctx.search_space_id, + ctx.workspace_id, ctx.user_id, ctx.connector, ) @@ -237,7 +237,7 @@ async def _process_document_upload(ctx: _ProcessingContext) -> Document | None: if ctx.use_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(ctx.session, ctx.search_space_id) + vision_llm = await get_vision_llm(ctx.session, ctx.workspace_id) etl_result = await extract_with_cache( EtlRequest( @@ -258,7 +258,7 @@ async def _process_document_upload(ctx: _ProcessingContext) -> Document | None: ctx.session, ctx.filename, etl_result.markdown_content, - ctx.search_space_id, + ctx.workspace_id, ctx.user_id, etl_result.etl_service, ctx.connector, @@ -302,7 +302,7 @@ async def _process_document_upload(ctx: _ProcessingContext) -> Document | None: async def process_file_in_background( file_path: str, filename: str, - search_space_id: int, + workspace_id: int, user_id: str, session: AsyncSession, task_logger: TaskLoggingService, @@ -316,7 +316,7 @@ async def process_file_in_background( session=session, file_path=file_path, filename=filename, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, task_logger=task_logger, log_entry=log_entry, @@ -368,7 +368,7 @@ async def process_file_in_background( async def _extract_file_content( file_path: str, filename: str, - search_space_id: int, + workspace_id: int, session: AsyncSession, user_id: str, task_logger: TaskLoggingService, @@ -432,7 +432,7 @@ async def _extract_file_content( if use_vision_llm: from app.services.llm_service import get_vision_llm - vision_llm = await get_vision_llm(session, search_space_id) + vision_llm = await get_vision_llm(session, workspace_id) from app.etl_pipeline.cache import extract_with_cache @@ -459,7 +459,7 @@ async def process_file_in_background_with_document( document: Document, file_path: str, filename: str, - search_space_id: int, + workspace_id: int, user_id: str, session: AsyncSession, task_logger: TaskLoggingService, @@ -491,7 +491,7 @@ async def process_file_in_background_with_document( markdown_content, etl_service, billable_pages = await _extract_file_content( file_path, filename, - search_space_id, + workspace_id, session, user_id, task_logger, @@ -504,7 +504,7 @@ async def process_file_in_background_with_document( if not markdown_content: raise RuntimeError(f"Failed to extract content from file: {filename}") - content_hash = generate_content_hash(markdown_content, search_space_id) + content_hash = generate_content_hash(markdown_content, workspace_id) existing_by_content = await check_duplicate_document(session, content_hash) if existing_by_content and existing_by_content.id != doc_id: logging.info( @@ -525,7 +525,7 @@ async def process_file_in_background_with_document( markdown_content=markdown_content, filename=filename, etl_service=etl_service, - search_space_id=search_space_id, + workspace_id=workspace_id, user_id=user_id, ) diff --git a/surfsense_backend/app/tasks/document_processors/markdown_processor.py b/surfsense_backend/app/tasks/document_processors/markdown_processor.py index 19a4df87d..9e31327d9 100644 --- a/surfsense_backend/app/tasks/document_processors/markdown_processor.py +++ b/surfsense_backend/app/tasks/document_processors/markdown_processor.py @@ -120,7 +120,7 @@ async def add_received_markdown_file_document( session: AsyncSession, file_name: str, file_in_markdown: str, - search_space_id: int, + workspace_id: int, user_id: str, connector: dict | None = None, ) -> Document | None: @@ -131,14 +131,14 @@ async def add_received_markdown_file_document( session: Database session file_name: Name of the markdown file file_in_markdown: Content of the markdown file - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: ID of the user connector: Optional connector info for Google Drive files Returns: Document object if successful, None if failed """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -155,11 +155,11 @@ async def add_received_markdown_file_document( try: # Generate unique identifier hash (uses file_id for Google Drive, filename for others) primary_hash, legacy_hash = get_google_drive_unique_identifier( - connector, file_name, search_space_id + connector, file_name, workspace_id ) # Generate content hash - content_hash = generate_content_hash(file_in_markdown, search_space_id) + content_hash = generate_content_hash(file_in_markdown, workspace_id) # Check if document exists (with migration support for Google Drive and content_hash fallback) existing_document = await find_existing_document_with_migration( @@ -214,7 +214,7 @@ async def add_received_markdown_file_document( doc_type = DocumentType.GOOGLE_DRIVE_FILE document = Document( - search_space_id=search_space_id, + workspace_id=workspace_id, title=file_name, document_type=doc_type, document_metadata={ diff --git a/surfsense_backend/app/tasks/document_processors/youtube_processor.py b/surfsense_backend/app/tasks/document_processors/youtube_processor.py index dde5e4222..255ba4f7e 100644 --- a/surfsense_backend/app/tasks/document_processors/youtube_processor.py +++ b/surfsense_backend/app/tasks/document_processors/youtube_processor.py @@ -63,7 +63,7 @@ def get_youtube_video_id(url: str) -> str | None: async def add_youtube_video_document( session: AsyncSession, url: str, - search_space_id: int, + workspace_id: int, user_id: str, notification=None, ) -> Document: @@ -77,7 +77,7 @@ async def add_youtube_video_document( Args: session: Database session for storing the document url: YouTube video URL (supports standard, shortened, and embed formats) - search_space_id: ID of the search space to add the document to + workspace_id: ID of the workspace to add the document to user_id: ID of the user notification: Optional notification object — if provided, the document_id is stored in its metadata right after document creation so the stale @@ -91,7 +91,7 @@ async def add_youtube_video_document( SQLAlchemyError: If there's a database error RuntimeError: If the video processing fails """ - task_logger = TaskLoggingService(session, search_space_id) + task_logger = TaskLoggingService(session, workspace_id) # Log task start log_entry = await task_logger.log_task_start( @@ -125,7 +125,7 @@ async def add_youtube_video_document( # Generate unique identifier hash for this YouTube video unique_identifier_hash = generate_unique_identifier_hash( - DocumentType.YOUTUBE_VIDEO, video_id, search_space_id + DocumentType.YOUTUBE_VIDEO, video_id, workspace_id ) # Check if document with this unique identifier already exists @@ -181,7 +181,7 @@ async def add_youtube_video_document( embedding=None, chunks=[], # Empty at creation status=DocumentStatus.pending(), # PENDING status - visible in UI - search_space_id=search_space_id, + workspace_id=workspace_id, updated_at=get_current_timestamp(), created_by_id=user_id, ) @@ -346,7 +346,7 @@ async def add_youtube_video_document( combined_document_string = "\n".join(document_parts) # Generate content hash - content_hash = generate_content_hash(combined_document_string, search_space_id) + content_hash = generate_content_hash(combined_document_string, workspace_id) # For existing documents, check if content has changed if not is_new_document and existing_document.content_hash == content_hash: diff --git a/surfsense_backend/app/users.py b/surfsense_backend/app/users.py index bf9ec74d1..c3e58cea9 100644 --- a/surfsense_backend/app/users.py +++ b/surfsense_backend/app/users.py @@ -22,9 +22,9 @@ from app.auth.session_cookies import access_expires_at, write_session from app.config import config from app.db import ( Prompt, - SearchSpace, - SearchSpaceMembership, - SearchSpaceRole, + Workspace, + WorkspaceMembership, + WorkspaceRole, User, async_session_maker, get_async_session, @@ -146,34 +146,34 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): async def on_after_register(self, user: User, request: Request | None = None): """ - Called after a user registers. Creates a default search space for the user + Called after a user registers. Creates a default workspace for the user so they can start chatting immediately without manual setup. """ - logger.info(f"User {user.id} has registered. Creating default search space...") + logger.info(f"User {user.id} has registered. Creating default workspace...") try: async with async_session_maker() as session: - # Create default search space - default_search_space = SearchSpace( - name="My Search Space", - description="Your personal search space", + # Create default workspace + default_workspace = Workspace( + name="My Workspace", + description="Your personal workspace", user_id=user.id, ) - session.add(default_search_space) - await session.flush() # Get the search space ID + session.add(default_workspace) + await session.flush() # Get the workspace ID # Create default roles default_roles = get_default_roles_config() owner_role_id = None for role_config in default_roles: - db_role = SearchSpaceRole( + db_role = WorkspaceRole( name=role_config["name"], description=role_config["description"], permissions=role_config["permissions"], is_default=role_config["is_default"], is_system_role=role_config["is_system_role"], - search_space_id=default_search_space.id, + workspace_id=default_workspace.id, ) session.add(db_role) await session.flush() @@ -182,9 +182,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): owner_role_id = db_role.id # Create owner membership - owner_membership = SearchSpaceMembership( + owner_membership = WorkspaceMembership( user_id=user.id, - search_space_id=default_search_space.id, + workspace_id=default_workspace.id, role_id=owner_role_id, is_owner=True, ) @@ -204,11 +204,11 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): await session.commit() logger.info( - f"Created default search space (ID: {default_search_space.id}) for user {user.id}" + f"Created default workspace (ID: {default_workspace.id}) for user {user.id}" ) except Exception as e: logger.error( - f"Failed to create default search space for user {user.id}: {e}" + f"Failed to create default workspace for user {user.id}: {e}" ) async def on_after_forgot_password( @@ -384,7 +384,7 @@ async def allow_any_principal( ) -> AuthContext: """Allow either session or PAT principals for bootstrap probes only. - Routes using this dependency intentionally have no search-space gate. + Routes using this dependency intentionally have no workspace gate. Adding a new call site is a security decision and must be covered by the fail-closed PAT allowlist test. """ diff --git a/surfsense_backend/app/utils/connector_naming.py b/surfsense_backend/app/utils/connector_naming.py index 99c8243a5..502e89775 100644 --- a/surfsense_backend/app/utils/connector_naming.py +++ b/surfsense_backend/app/utils/connector_naming.py @@ -118,14 +118,14 @@ def generate_connector_name_with_identifier( async def count_connectors_of_type( session: AsyncSession, connector_type: SearchSourceConnectorType, - search_space_id: int, + workspace_id: int, user_id: UUID, ) -> int: - """Count existing connectors of a type for a user in a search space.""" + """Count existing connectors of a type for a user in a workspace.""" result = await session.execute( select(func.count(SearchSourceConnector.id)).where( SearchSourceConnector.connector_type == connector_type, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, ) ) @@ -135,7 +135,7 @@ async def count_connectors_of_type( async def check_duplicate_connector( session: AsyncSession, connector_type: SearchSourceConnectorType, - search_space_id: int, + workspace_id: int, user_id: UUID, identifier: str | None, ) -> bool: @@ -145,7 +145,7 @@ async def check_duplicate_connector( Args: session: Database session connector_type: The type of connector - search_space_id: The search space ID + workspace_id: The workspace ID user_id: The user ID identifier: User identifier (email, workspace name, etc.) @@ -159,7 +159,7 @@ async def check_duplicate_connector( result = await session.execute( select(func.count(SearchSourceConnector.id)).where( SearchSourceConnector.connector_type == connector_type, - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.name == expected_name, ) @@ -170,11 +170,11 @@ async def check_duplicate_connector( async def ensure_unique_connector_name( session: AsyncSession, name: str, - search_space_id: int, + workspace_id: int, user_id: UUID, ) -> str: """ - Ensure a connector name is unique within a user's search space. + Ensure a connector name is unique within a user's workspace. If the name already exists, appends a counter suffix: (2), (3), etc. Uses the same suffix format as generate_unique_connector_name. @@ -182,7 +182,7 @@ async def ensure_unique_connector_name( Args: session: Database session name: Desired connector name - search_space_id: The search space ID + workspace_id: The workspace ID user_id: The user ID Returns: @@ -190,7 +190,7 @@ async def ensure_unique_connector_name( """ result = await session.execute( select(SearchSourceConnector.name).where( - SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.workspace_id == workspace_id, SearchSourceConnector.user_id == user_id, ) ) @@ -208,7 +208,7 @@ async def ensure_unique_connector_name( async def generate_unique_connector_name( session: AsyncSession, connector_type: SearchSourceConnectorType, - search_space_id: int, + workspace_id: int, user_id: UUID, identifier: str | None = None, ) -> str: @@ -221,7 +221,7 @@ async def generate_unique_connector_name( Args: session: Database session connector_type: The type of connector - search_space_id: The search space ID + workspace_id: The workspace ID user_id: The user ID identifier: Optional user identifier (email, workspace name, etc.) @@ -235,12 +235,12 @@ async def generate_unique_connector_name( return await ensure_unique_connector_name( session, name, - search_space_id, + workspace_id, user_id, ) count = await count_connectors_of_type( - session, connector_type, search_space_id, user_id + session, connector_type, workspace_id, user_id ) if count == 0: diff --git a/surfsense_backend/app/utils/document_converters.py b/surfsense_backend/app/utils/document_converters.py index bd8740358..3c3e2a562 100644 --- a/surfsense_backend/app/utils/document_converters.py +++ b/surfsense_backend/app/utils/document_converters.py @@ -257,28 +257,28 @@ async def convert_document_to_markdown(elements): return "".join(markdown_parts) -def generate_content_hash(content: str, search_space_id: int) -> str: - """Generate SHA-256 hash for the given content combined with search space ID.""" - combined_data = f"{search_space_id}:{content}" +def generate_content_hash(content: str, workspace_id: int) -> str: + """Generate SHA-256 hash for the given content combined with workspace ID.""" + combined_data = f"{workspace_id}:{content}" return hashlib.sha256(combined_data.encode("utf-8")).hexdigest() def generate_unique_identifier_hash( document_type: DocumentType, unique_identifier: str | int | float, - search_space_id: int, + workspace_id: int, ) -> str: """ Generate SHA-256 hash for a unique document identifier from connector sources. This function creates a consistent hash based on the document type, its unique - identifier from the source system, and the search space ID. This helps prevent + identifier from the source system, and the workspace ID. This helps prevent duplicate documents when syncing from various connectors like Slack, Notion, Jira, etc. Args: document_type: The type of document (e.g., SLACK_CONNECTOR, NOTION_CONNECTOR) unique_identifier: The unique ID from the source system (e.g., message ID, page ID) - search_space_id: The search space this document belongs to + workspace_id: The workspace this document belongs to Returns: str: SHA-256 hash string representing the unique document identifier @@ -294,7 +294,7 @@ def generate_unique_identifier_hash( # Convert unique_identifier to string to handle different types identifier_str = str(unique_identifier) - # Combine document type value, unique identifier, and search space ID - combined_data = f"{document_type.value}:{identifier_str}:{search_space_id}" + # Combine document type value, unique identifier, and workspace ID + combined_data = f"{document_type.value}:{identifier_str}:{workspace_id}" return hashlib.sha256(combined_data.encode("utf-8")).hexdigest() diff --git a/surfsense_backend/app/utils/oauth_security.py b/surfsense_backend/app/utils/oauth_security.py index c39b1e9b1..691e96687 100644 --- a/surfsense_backend/app/utils/oauth_security.py +++ b/surfsense_backend/app/utils/oauth_security.py @@ -63,7 +63,7 @@ class OAuthStateManager: Generate cryptographically signed state parameter. Args: - space_id: The search space ID + space_id: The workspace ID user_id: The user ID **extra_fields: Additional fields to include in state (e.g., code_verifier for PKCE) diff --git a/surfsense_backend/app/utils/periodic_scheduler.py b/surfsense_backend/app/utils/periodic_scheduler.py index 35e8ad781..5cce245f8 100644 --- a/surfsense_backend/app/utils/periodic_scheduler.py +++ b/surfsense_backend/app/utils/periodic_scheduler.py @@ -29,7 +29,7 @@ CONNECTOR_TASK_MAP = { def create_periodic_schedule( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, connector_type: SearchSourceConnectorType, frequency_minutes: int, @@ -44,7 +44,7 @@ def create_periodic_schedule( Args: connector_id: ID of the connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: User ID connector_type: Type of connector frequency_minutes: Frequency in minutes (used for logging) @@ -94,7 +94,7 @@ def create_periodic_schedule( # Trigger the first run immediately task = task_map.get(connector_type) if task: - task.delay(connector_id, search_space_id, user_id, None, None) + task.delay(connector_id, workspace_id, user_id, None, None) logger.info( f"✓ First indexing run triggered for connector {connector_id}. " f"Periodic indexing will continue automatically every {frequency_minutes} minutes." @@ -133,7 +133,7 @@ def delete_periodic_schedule(connector_id: int) -> bool: def update_periodic_schedule( connector_id: int, - search_space_id: int, + workspace_id: int, user_id: str, connector_type: SearchSourceConnectorType, frequency_minutes: int, @@ -146,7 +146,7 @@ def update_periodic_schedule( Args: connector_id: ID of the connector - search_space_id: ID of the search space + workspace_id: ID of the workspace user_id: User ID connector_type: Type of connector frequency_minutes: New frequency in minutes @@ -160,5 +160,5 @@ def update_periodic_schedule( ) # Optionally trigger an immediate run with the new schedule # Uncomment the line below if you want immediate execution on schedule update - # return create_periodic_schedule(connector_id, search_space_id, user_id, connector_type, frequency_minutes) + # return create_periodic_schedule(connector_id, workspace_id, user_id, connector_type, frequency_minutes) return True diff --git a/surfsense_backend/app/utils/rbac.py b/surfsense_backend/app/utils/rbac.py index c82c94344..3d425ab4b 100644 --- a/surfsense_backend/app/utils/rbac.py +++ b/surfsense_backend/app/utils/rbac.py @@ -1,6 +1,6 @@ """ RBAC (Role-Based Access Control) utility functions. -Provides helpers for checking user permissions in search spaces. +Provides helpers for checking user permissions in workspaces. """ import secrets @@ -14,9 +14,9 @@ from sqlalchemy.orm import selectinload from app.auth.context import AuthContext from app.db import ( Permission, - SearchSpace, - SearchSpaceMembership, - SearchSpaceRole, + Workspace, + WorkspaceMembership, + WorkspaceRole, has_permission, ) @@ -24,25 +24,25 @@ from app.db import ( async def get_user_membership( session: AsyncSession, user_id: UUID, - search_space_id: int, -) -> SearchSpaceMembership | None: + workspace_id: int, +) -> WorkspaceMembership | None: """ - Get the user's membership in a search space. + Get the user's membership in a workspace. Args: session: Database session user_id: User UUID - search_space_id: Search space ID + workspace_id: Workspace ID Returns: - SearchSpaceMembership if found, None otherwise + WorkspaceMembership if found, None otherwise """ result = await session.execute( - select(SearchSpaceMembership) - .options(selectinload(SearchSpaceMembership.role)) + select(WorkspaceMembership) + .options(selectinload(WorkspaceMembership.role)) .filter( - SearchSpaceMembership.user_id == user_id, - SearchSpaceMembership.search_space_id == search_space_id, + WorkspaceMembership.user_id == user_id, + WorkspaceMembership.workspace_id == workspace_id, ) ) return result.scalars().first() @@ -51,20 +51,20 @@ async def get_user_membership( async def get_user_permissions( session: AsyncSession, user_id: UUID, - search_space_id: int, + workspace_id: int, ) -> list[str]: """ - Get the user's permissions in a search space. + Get the user's permissions in a workspace. Args: session: Database session user_id: User UUID - search_space_id: Search space ID + workspace_id: Workspace ID Returns: List of permission strings """ - membership = await get_user_membership(session, user_id, search_space_id) + membership = await get_user_membership(session, user_id, workspace_id) if not membership: return [] @@ -84,19 +84,19 @@ async def get_allowed_read_space_ids( session: AsyncSession, auth: AuthContext, ) -> list[int]: - """Return search spaces the principal may read through sync transports. + """Return workspaces the principal may read through sync transports. - This mirrors the basic REST search-space access rule: membership is required, + This mirrors the basic REST workspace access rule: membership is required, and PAT principals are additionally constrained by the per-space API gate. """ stmt = ( - select(SearchSpaceMembership.search_space_id) - .join(SearchSpace, SearchSpace.id == SearchSpaceMembership.search_space_id) - .filter(SearchSpaceMembership.user_id == auth.user.id) - .order_by(SearchSpaceMembership.search_space_id) + select(WorkspaceMembership.workspace_id) + .join(Workspace, Workspace.id == WorkspaceMembership.workspace_id) + .filter(WorkspaceMembership.user_id == auth.user.id) + .order_by(WorkspaceMembership.workspace_id) ) if auth.is_gated: - stmt = stmt.filter(SearchSpace.api_access_enabled == True) # noqa: E712 + stmt = stmt.filter(Workspace.api_access_enabled == True) # noqa: E712 result = await session.execute(stmt) return list(result.scalars().all()) @@ -105,57 +105,57 @@ async def get_allowed_read_space_ids( async def _enforce_api_access_gate( session: AsyncSession, auth: AuthContext, - search_space_id: int, - search_space: SearchSpace | None = None, -) -> SearchSpace: - if search_space is None: + workspace_id: int, + workspace: Workspace | None = None, +) -> Workspace: + if workspace is None: result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - search_space = result.scalars().first() + workspace = result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") - if auth.is_gated and not search_space.api_access_enabled: + if auth.is_gated and not workspace.api_access_enabled: raise HTTPException( status_code=403, - detail="API access is not enabled for this search space.", + detail="API access is not enabled for this workspace.", ) - return search_space + return workspace async def check_permission( session: AsyncSession, auth: AuthContext, - search_space_id: int, + workspace_id: int, required_permission: str, error_message: str = "You don't have permission to perform this action", -) -> SearchSpaceMembership: +) -> WorkspaceMembership: """ - Check if a user has a specific permission in a search space. + Check if a user has a specific permission in a workspace. Raises HTTPException if permission is denied. Args: session: Database session user: User object - search_space_id: Search space ID + workspace_id: Workspace ID required_permission: Permission string to check error_message: Custom error message for permission denied Returns: - SearchSpaceMembership if permission granted + WorkspaceMembership if permission granted Raises: HTTPException: If user doesn't have access or permission """ - membership = await get_user_membership(session, auth.user.id, search_space_id) + membership = await get_user_membership(session, auth.user.id, workspace_id) if not membership: raise HTTPException( status_code=403, - detail="You don't have access to this search space", + detail="You don't have access to this workspace", ) # Get user's permissions @@ -169,110 +169,110 @@ async def check_permission( if not has_permission(permissions, required_permission): raise HTTPException(status_code=403, detail=error_message) - await _enforce_api_access_gate(session, auth, search_space_id) + await _enforce_api_access_gate(session, auth, workspace_id) return membership -async def check_search_space_access( +async def check_workspace_access( session: AsyncSession, auth: AuthContext, - search_space_id: int, -) -> SearchSpaceMembership: + workspace_id: int, +) -> WorkspaceMembership: """ - Check if a user has any access to a search space. + Check if a user has any access to a workspace. This is used for basic access control (user is a member). Args: session: Database session user: User object - search_space_id: Search space ID + workspace_id: Workspace ID Returns: - SearchSpaceMembership if user has access + WorkspaceMembership if user has access Raises: HTTPException: If user doesn't have access """ - membership = await get_user_membership(session, auth.user.id, search_space_id) + membership = await get_user_membership(session, auth.user.id, workspace_id) if not membership: raise HTTPException( status_code=403, - detail="You don't have access to this search space", + detail="You don't have access to this workspace", ) - await _enforce_api_access_gate(session, auth, search_space_id) + await _enforce_api_access_gate(session, auth, workspace_id) return membership -async def is_search_space_owner( +async def is_workspace_owner( session: AsyncSession, user_id: UUID, - search_space_id: int, + workspace_id: int, ) -> bool: """ - Check if a user is the owner of a search space. + Check if a user is the owner of a workspace. Args: session: Database session user_id: User UUID - search_space_id: Search space ID + workspace_id: Workspace ID Returns: True if user is the owner, False otherwise """ - membership = await get_user_membership(session, user_id, search_space_id) + membership = await get_user_membership(session, user_id, workspace_id) return membership is not None and membership.is_owner -async def get_search_space_with_access_check( +async def get_workspace_with_access_check( session: AsyncSession, auth: AuthContext, - search_space_id: int, + workspace_id: int, required_permission: str | None = None, -) -> tuple[SearchSpace, SearchSpaceMembership]: +) -> tuple[Workspace, WorkspaceMembership]: """ - Get a search space with access and optional permission check. + Get a workspace with access and optional permission check. Args: session: Database session user: User object - search_space_id: Search space ID + workspace_id: Workspace ID required_permission: Optional permission to check Returns: - Tuple of (SearchSpace, SearchSpaceMembership) + Tuple of (Workspace, WorkspaceMembership) Raises: - HTTPException: If search space not found or user lacks access/permission + HTTPException: If workspace not found or user lacks access/permission """ - # Get the search space + # Get the workspace result = await session.execute( - select(SearchSpace).filter(SearchSpace.id == search_space_id) + select(Workspace).filter(Workspace.id == workspace_id) ) - search_space = result.scalars().first() + workspace = result.scalars().first() - if not search_space: - raise HTTPException(status_code=404, detail="Search space not found") + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") # Check access if required_permission: membership = await check_permission( - session, auth, search_space_id, required_permission + session, auth, workspace_id, required_permission ) else: - membership = await check_search_space_access(session, auth, search_space_id) + membership = await check_workspace_access(session, auth, workspace_id) - await _enforce_api_access_gate(session, auth, search_space_id, search_space) + await _enforce_api_access_gate(session, auth, workspace_id, workspace) - return search_space, membership + return workspace, membership def generate_invite_code() -> str: """ - Generate a unique invite code for search space invites. + Generate a unique invite code for workspace invites. Returns: A 32-character URL-safe invite code @@ -282,22 +282,22 @@ def generate_invite_code() -> str: async def get_default_role( session: AsyncSession, - search_space_id: int, -) -> SearchSpaceRole | None: + workspace_id: int, +) -> WorkspaceRole | None: """ - Get the default role for a search space (used when accepting invites without a specific role). + Get the default role for a workspace (used when accepting invites without a specific role). Args: session: Database session - search_space_id: Search space ID + workspace_id: Workspace ID Returns: - Default SearchSpaceRole or None + Default WorkspaceRole or None """ result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.is_default == True, # noqa: E712 + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.is_default == True, # noqa: E712 ) ) return result.scalars().first() @@ -305,22 +305,22 @@ async def get_default_role( async def get_owner_role( session: AsyncSession, - search_space_id: int, -) -> SearchSpaceRole | None: + workspace_id: int, +) -> WorkspaceRole | None: """ - Get the Owner role for a search space. + Get the Owner role for a workspace. Args: session: Database session - search_space_id: Search space ID + workspace_id: Workspace ID Returns: - Owner SearchSpaceRole or None + Owner WorkspaceRole or None """ result = await session.execute( - select(SearchSpaceRole).filter( - SearchSpaceRole.search_space_id == search_space_id, - SearchSpaceRole.name == "Owner", + select(WorkspaceRole).filter( + WorkspaceRole.workspace_id == workspace_id, + WorkspaceRole.name == "Owner", ) ) return result.scalars().first() diff --git a/surfsense_backend/app/utils/validators.py b/surfsense_backend/app/utils/validators.py index 6a87679ec..215e78ab7 100644 --- a/surfsense_backend/app/utils/validators.py +++ b/surfsense_backend/app/utils/validators.py @@ -13,59 +13,59 @@ import validators from fastapi import HTTPException -def validate_search_space_id(search_space_id: Any) -> int: +def validate_workspace_id(workspace_id: Any) -> int: """ - Validate and convert search_space_id to integer. + Validate and convert workspace_id to integer. Args: - search_space_id: The search space ID to validate + workspace_id: The workspace ID to validate Returns: - int: Validated search space ID + int: Validated workspace ID Raises: HTTPException: If validation fails """ - if search_space_id is None: - raise HTTPException(status_code=400, detail="search_space_id is required") + if workspace_id is None: + raise HTTPException(status_code=400, detail="workspace_id is required") - if isinstance(search_space_id, bool): + if isinstance(workspace_id, bool): raise HTTPException( - status_code=400, detail="search_space_id must be an integer, not a boolean" + status_code=400, detail="workspace_id must be an integer, not a boolean" ) - if isinstance(search_space_id, int): - if search_space_id <= 0: + if isinstance(workspace_id, int): + if workspace_id <= 0: raise HTTPException( - status_code=400, detail="search_space_id must be a positive integer" + status_code=400, detail="workspace_id must be a positive integer" ) - return search_space_id + return workspace_id - if isinstance(search_space_id, str): + if isinstance(workspace_id, str): # Check if it's a valid integer string - if not search_space_id.strip(): + if not workspace_id.strip(): raise HTTPException( - status_code=400, detail="search_space_id cannot be empty" + status_code=400, detail="workspace_id cannot be empty" ) # Check for valid integer format (no leading zeros, no decimal points) - if not re.match(r"^[1-9]\d*$", search_space_id.strip()): + if not re.match(r"^[1-9]\d*$", workspace_id.strip()): raise HTTPException( status_code=400, - detail="search_space_id must be a valid positive integer", + detail="workspace_id must be a valid positive integer", ) - value = int(search_space_id.strip()) + value = int(workspace_id.strip()) # Regex already guarantees value > 0, but check retained for clarity if value <= 0: raise HTTPException( - status_code=400, detail="search_space_id must be a positive integer" + status_code=400, detail="workspace_id must be a positive integer" ) return value raise HTTPException( status_code=400, - detail="search_space_id must be an integer or string representation of an integer", + detail="workspace_id must be an integer or string representation of an integer", )