refactor(backend): rename search_space -> workspace across app bulk (Phase 2 Wave D)

Scoped codemod over surfsense_backend/app (excluding routes/, Wave E): renames
search_space_id -> workspace_id, search_space -> workspace, SearchSpace -> Workspace
across services, utils, tasks, agents, gateway, event_bus, notifications, podcasts,
automations, observability params, and prompt .md files. Also flips the camelCase
payload key searchSpaceId -> workspaceId (no backend reader; hard cutover).

Preserved carve-outs (verbatim): Celery task names "delete_search_space_background"
and "ai_sort_search_space" (wire names), and the OTel/metric key "search_space.id"
(dashboards depend on it). Enum values 'SEARCH_SPACE' and SearchSourceConnector
untouched.
This commit is contained in:
CREDO23 2026-06-26 18:30:47 +02:00
parent 0c53d884eb
commit 7fb0707933
259 changed files with 1996 additions and 1996 deletions

View file

@ -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 []
)

View file

@ -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,

View file

@ -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

View file

@ -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.

View file

@ -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:

View file

@ -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

View file

@ -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 (

View file

@ -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,

View file

@ -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(),

View file

@ -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,

View file

@ -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 ``<workspace_tree>`` 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())

View file

@ -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,
)

View file

@ -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()

View file

@ -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,

View file

@ -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,
)

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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).

View file

@ -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,

View file

@ -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",
]

View file

@ -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 `<team_memory>` with usage vs limit.
- Call when a team member asks to remember or forget something, or when
the conversation surfaces durable team decisions, conventions,

View file

@ -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:

View file

@ -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

View file

@ -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"),
),
}

View file

@ -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 <team_memory>. 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,

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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:

View file

@ -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,

View file

@ -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",

View file

@ -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,
)

View file

@ -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

View file

@ -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)

View file

@ -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"),
),

View file

@ -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)

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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"),
)

View file

@ -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,

View file

@ -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,

View file

@ -6,7 +6,7 @@ Persist durable preferences/facts/instructions with `update_memory` while avoidi
</goal>
<visibility_scope>
Memory is search-space-scoped; do not assume cross-workspace visibility.
Memory is workspace-scoped; do not assume cross-workspace visibility.
</visibility_scope>
<available_tools>

View file

@ -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"),
)

View file

@ -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 <team_memory>. 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,

View file

@ -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")),

View file

@ -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":

View file

@ -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),
)

View file

@ -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 [

View file

@ -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),
)

View file

@ -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":

View file

@ -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":

View file

@ -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,

View file

@ -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"),
}

View file

@ -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 = (

View file

@ -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,

View file

@ -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 [

View file

@ -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."}

View file

@ -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."}

View file

@ -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."}

View file

@ -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":

View file

@ -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 [

View file

@ -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,

View file

@ -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"),
)

View file

@ -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 [

View file

@ -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),
)

View file

@ -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),
)

View file

@ -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":

View file

@ -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),
)

View file

@ -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),
)

View file

@ -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":

View file

@ -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 [

View file

@ -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),
)

View file

@ -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,

View file

@ -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."}

View file

@ -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 [

View file

@ -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."}

View file

@ -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."}

View file

@ -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":

View file

@ -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(
{

View file

@ -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 [

View file

@ -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"),
)

View file

@ -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":

View file

@ -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 [

View file

@ -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,

View file

@ -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,

View file

@ -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 [

View file

@ -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."}

View file

@ -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."}

View file

@ -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."}

View file

@ -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)

View file

@ -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.
"""

View file

@ -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.
"""

View file

@ -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 ""))

View file

@ -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 ``" (<doc_id>).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:

View file

@ -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, [])

Some files were not shown because too many files have changed in this diff Show more