mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-06 22:12:12 +02:00
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:
parent
0c53d884eb
commit
7fb0707933
259 changed files with 1996 additions and 1996 deletions
|
|
@ -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 []
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 ""))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue