Merge pull request #1562 from MODSetter/feat/rename-searchspace-to-workspace

feat: rename searchspace to workspace
This commit is contained in:
Rohan Verma 2026-06-29 17:58:07 -07:00 committed by GitHub
commit 96b18af607
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
454 changed files with 5378 additions and 4751 deletions

View file

@ -2,6 +2,10 @@
Part of [00-umbrella-plan.md](00-umbrella-plan.md), Phase 1. Backend only (`surfsense_backend`).
> **Status: SHIPPED** · as of 2026-06-27 · branch `feat/rename-searchspace-to-workspace` · PR [#1546](https://github.com/MODSetter/SurfSense/pull/1546)
> Last commit of this phase: `49d3001fb` (DB rename through migration 170). Phase 1 + Phase 2 shipped as one atomic PR.
> The sections below are the **original design/rationale**; the as-built state + how to re-verify live in the [Implementation record](#implementation-record-as-built) at the bottom. Ground truth for files/commits is the PR, not this doc.
## Goal
Physically rename the `SearchSpace` schema to `WorkSpace` in PostgreSQL via a single Alembic migration, and update the ORM's physical mapping + the Zero publication so the app keeps booting and running — WITHOUT yet touching the ~250 backend files that reference the `search_space_id` Python attribute (that symbolic rename is Phase 2).
@ -180,3 +184,30 @@ The Alembic migration, the `db.py` edits (shim + constraint `name=` + `_INDEX_DE
- Phase 2: rename Python attribute `search_space_id` -> `workspace_id`, class `SearchSpace` -> `Workspace`, Pydantic schemas, API routes/paths (`/searchspaces` + `/search-spaces` -> `/workspaces`), Redis keys, storage path segments; drop the explicit `Column("workspace_id", ...)` shim.
- Frontend Zero schema, route segment, i18n — deferred frontend umbrella.
---
## Implementation record (as-built)
### Deviations from the plan
- **Migration 168 idempotency fixed up-front**: the plan flagged from-scratch alembic as pre-existing-broken; we made 168 idempotent so the rename starts from a clean head, but did **not** take on the full baseline-squash. From-scratch `alembic upgrade head` therefore stays pre-existing-broken (rev 23 conflict); only the existing-DB `169 -> 170` path is in scope/verified.
- **Cosmetic sequence/PK renames applied** (the plan's confirmed decision): `searchspaces_id_seq -> workspaces_id_seq`, `searchspaces_pkey -> workspaces_pkey`, plus the RBAC tables' `*_id_seq` / `*_pkey`, for a clean final schema.
- Everything else implemented exactly as designed: shim via `Column("workspace_id", ...)`; publication mutated **only** through the blessed `apply_publication` path (no raw DROP/CREATE PUBLICATION, per migration 116); `__table_args__` inner column-reference strings deliberately **left** as `search_space_id` (flipped in Phase 2).
### Carve-outs as-shipped (Phase 1)
- `'SEARCH_SPACE'` CHECK literal in `ck_connections_scope_owner` kept; only the `search_space_id` column reference flipped to `workspace_id`. No enum/data migration.
### Verify current state (re-runnable)
Each line is a command + the last captured result. Re-run to confirm the *current* truth instead of trusting the snapshot. Schema gates assume a DB at rev `170` (locally `surfsense_oldshape`) with `AUTH_TYPE=LOCAL` (so the env-gated `oauth_account` table isn't in the ORM metadata).
- **Schema drift (ORM ↔ physical @170)**
`AUTH_TYPE=LOCAL DATABASE_URL=postgresql+asyncpg://…@localhost:5432/surfsense_oldshape uv run alembic check`
→ last (2026-06-27): `No new upgrade operations detected.` (With `AUTH_TYPE=GOOGLE` the only delta is the env-gated `oauth_account` table — unrelated to the rename.)
- **Publication columns**
`rg -n 'workspace_id|search_space_id' app/zero_publication.py`
→ last (2026-06-27): all four lists = `workspace_id`, zero `search_space_id`.
Validated during the build run (2026-06-26), **not** reproducible on a `create_all`-built snapshot (it has no `zero_publication` object): `python -m app.zero_publication --verify` → verified (interlock-footgun guard, finding 2); `alembic upgrade 170 → downgrade -1 → upgrade 170` round-trip (SQL reversibility; downgrade restores the `search_space_id` publication shape via hardcoded lists, per finding 7).

View file

@ -2,6 +2,10 @@
Part of [00-umbrella-plan.md](00-umbrella-plan.md), Phase 2. Backend only (`surfsense_backend`).
> **Status: SHIPPED** · as of 2026-06-27 · branch `feat/rename-searchspace-to-workspace` · PR [#1546](https://github.com/MODSetter/SurfSense/pull/1546)
> Last commit of this phase: `902b3374e` (backend code/API rename, Waves AF + carve-outs). Phase 1 + Phase 2 shipped as one atomic PR.
> The sections below are the **original design/rationale**; the as-built state + how to re-verify live in the [Implementation record](#implementation-record-as-built) at the bottom. Ground truth for files/commits is the PR, not this doc.
## Goal
Remove the Phase 1 shim and complete the SYMBOLIC rename `SearchSpace -> Workspace` / `search_space_id -> workspace_id` across the ~150 backend files, then consolidate the three live URL spellings (`/searchspaces`, `/search-spaces`, `/search-space`) onto a single canonical `/workspaces`. After this phase the physical DB (Phase 1) and the Python/API surface speak the same name.
@ -131,3 +135,54 @@ These do not move with a symbol rename; each is decided here.
- Satellite apps (desktop, Obsidian, browser extension, evals) + docs — deferred.
- Connector `category` discriminator and `SearchSourceConnector` handling — Phase 4 ([04a-connector-category.md](04a-connector-category.md)).
- Enum VALUE migration and observability-key rename — deliberately deferred announced changes.
---
## Implementation record (as-built)
Whole-PR footprint (vs `ci_mvp`): **452 files, +5317/-4781** — 325 under `app/`, 124 under `tests/`, and **exactly 2** `alembic/versions/` files (`168` idempotency + `170` rename), confirming the immutable-replay-log carve-out (#9) held.
The plan's Waves AF were executed in order, as described above; what follows records only what diverged from that design and the evidence that it landed cleanly.
### Deviations from the plan
- **Bulk rename executed via scoped Python codemods** (multiple passes), strictly limited to `app/` + `tests/` (never `alembic/versions/`), instead of IDE/`ast-grep`. Same scope rule and same carve-out exclusions as designed.
- **camelCase `searchSpaceId` -> `workspaceId`**: a camelCase payload key (in `app/agents/chat/multi_agent_chat/main_agent/middleware/kb_persistence/middleware.py`) was renamed too — decided internal/transient, so it follows the hard cutover. The plan enumerated snake/Pascal/Title/hyphen variants; this camelCase variant was caught in a follow-up codemod pass (residual `searchSpaceId` in `app/` now = 0).
- All other carve-outs honored exactly as decided (below).
### Carve-outs as-shipped (verified by residual grep)
KEPT (intentional):
- Enum VALUES `'SEARCH_SPACE'` (`ConnectionScope`, `ChatVisibility`) — value migration deferred (carve-out 1).
- Celery wire task names `"delete_search_space_background"`, `"ai_sort_search_space"` (carve-out 2). Python functions renamed (`ai_sort_workspace_task`, `delete_workspace_task`); producer and worker agree because dispatch is via `.delay()` on the task object (no hardcoded `send_task("ai_sort_search_space")`).
- OTel/metric key `search_space.id` (carve-out 5) — 3 sites (`observability/otel.py` x2, `observability/metrics.py`); dashboards/alerts depend on it.
- API error code `"SEARCH_SPACE_FORBIDDEN"` (external contract string).
- `SearchSourceConnector` class/table (carve-out 8, unrelated taxonomy).
- Historical `alembic/versions/*` (carve-out 9, immutable replay log).
RENAMED (accepted transient deploy cost):
- Redis keys `surfsense:spawn_paused:{workspace_id}`, `ai_sort:workspace:{workspace_id}:lock` (carve-out 3) + `.env.example` runbook.
- Event payload key (carve-out 4) and LangGraph state-channel key (carve-out 10) -> `workspace_id`; default seed name -> `"My Workspace"` (carve-out 11).
### Verify current state (re-runnable)
Each line is a command + the last captured result. Re-run to confirm the *current* truth instead of trusting the snapshot. Run from `surfsense_backend/`.
- **Full backend suite**`uv run pytest tests/unit tests/integration`
→ last (2026-06-27): `3016 passed, 1 skipped` in ~101s. The skip is a Windows-only event-loop test (`tests/unit/tasks/test_celery_async_runner.py:302`).
- **Schema drift @170**`AUTH_TYPE=LOCAL DATABASE_URL=…surfsense_oldshape uv run alembic check`
→ last (2026-06-27): `No new upgrade operations detected.`
- **Route hard-cutover** — load `app.app:app` and inspect `{r.path for r in app.routes}`
→ last (2026-06-27): legacy `/searchspaces` `/search-spaces` `/search-space` = 0 each; canonical `/workspaces` = 26 (of 278 routes).
- **Residual symbols**`rg -ni 'search.?space' app`
→ last (2026-06-27): 52 hits, all carve-outs (enum `SEARCH_SPACE` values dominate; 3× OTel `search_space.id`; 2× Celery wire names; 1× `SEARCH_SPACE_FORBIDDEN`). No stray attribute/relationship/route residue.
- **`alembic/versions/` immutability** — `git diff --stat ci_mvp..HEAD -- surfsense_backend/alembic/versions`
→ last (2026-06-27): only `168` + `170` (guard against carve-out 9).
### Known caveats at deploy (team-acknowledged)
- **Zero-cache replica reset** required on deploy (`ZERO_AUTO_RESET`) — consequence of the schema rename.
- **From-scratch `alembic upgrade head` stays pre-existing-broken** (rev 23 conflict); the existing-DB `169 -> 170` path is verified. Separate baseline-squash task.
- **Planned-downtime deploy**: drain Celery/event queues and restart workers before cutover (renamed event payload / LangGraph state-channel / Redis keys). In-flight state across the boundary is discarded by design — acceptable given the downtime window.
- **Live Celery worker not exercised end-to-end** (the broker is mocked in tests). The task code paths are covered by unit/integration tests; a real worker + Redis smoke is the one verification not yet run.
- **Clients are intentionally broken** by the hard cutover until the frontend/satellite umbrellas land — backend correctness is verified via the suite + OpenAPI/route introspection + direct API calls, not the old UI.

View file

@ -472,7 +472,7 @@ LANGSMITH_PROJECT=surfsense
# -----------------------------------------------------------------------------
# Connector discovery TTL cache (Phase 1.4 perf optimization)
# -----------------------------------------------------------------------------
# Caches the per-search-space "available connectors" + "available document
# Caches the per-workspace "available connectors" + "available document
# types" lookups that ``create_surfsense_deep_agent`` hits on every turn.
# ORM event listeners auto-invalidate on connector / document inserts,
# updates and deletes — the TTL only bounds staleness for bulk-import
@ -512,8 +512,8 @@ LANGSMITH_PROJECT=surfsense
# Per-workspace spawn-paused kill switch — set via Redis at runtime, not
# this env var. The env var below only disables the check itself (useful
# for local dev without Redis). To pause a workspace in production:
# redis-cli SET surfsense:spawn_paused:<search_space_id> 1 EX 600
# redis-cli DEL surfsense:spawn_paused:<search_space_id>
# redis-cli SET surfsense:spawn_paused:<workspace_id> 1 EX 600
# redis-cli DEL surfsense:spawn_paused:<workspace_id>
# The check is fail-open: a Redis blip never blocks ``task(...)``.
# SURFSENSE_TASK_SPAWN_PAUSED_DISABLED=false

View file

@ -6,8 +6,6 @@ Revises: 167
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "168"
@ -63,9 +61,16 @@ def downgrade() -> None:
)
op.execute(
"""
UPDATE refresh_tokens
SET is_revoked = TRUE
WHERE revoked_at IS NOT NULL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'refresh_tokens'
AND column_name = 'revoked_at'
) THEN
UPDATE refresh_tokens SET is_revoked = TRUE WHERE revoked_at IS NOT NULL;
END IF;
END $$;
"""
)
op.execute("ALTER TABLE refresh_tokens ALTER COLUMN is_revoked DROP DEFAULT")

View file

@ -0,0 +1,508 @@
"""rename searchspace schema to workspace
Renames the SearchSpace tables/columns/constraints/indexes/sequences to the
workspace_* names the ORM already declares, and reconciles the Zero publication
to match.
Revision ID: 170
Revises: 169
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from app.zero_publication import apply_publication
revision: str = "170"
down_revision: str | None = "169"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
PUBLICATION_NAME = "zero_publication"
# Tables whose published column list references the renamed column. Their
# column-list dependency must be removed before RENAME COLUMN is permitted,
# then re-added by apply_publication with the new column name.
PUBLICATION_COLUMN_LIST_TABLES = [
"documents",
"automations",
"new_chat_threads",
"podcasts",
]
# (old_table, new_table)
TABLE_RENAMES: list[tuple[str, str]] = [
("searchspaces", "workspaces"),
("search_space_roles", "workspace_roles"),
("search_space_memberships", "workspace_memberships"),
("search_space_invites", "workspace_invites"),
]
# (table, old_column, new_column) -- table names are POST-table-rename.
COLUMN_RENAMES: list[tuple[str, str, str]] = [
("agent_action_log", "search_space_id", "workspace_id"),
("agent_permission_rules", "search_space_id", "workspace_id"),
("automations", "search_space_id", "workspace_id"),
("connections", "search_space_id", "workspace_id"),
("document_files", "search_space_id", "workspace_id"),
("document_revisions", "search_space_id", "workspace_id"),
("documents", "search_space_id", "workspace_id"),
("external_chat_bindings", "search_space_id", "workspace_id"),
("folder_revisions", "search_space_id", "workspace_id"),
("folders", "search_space_id", "workspace_id"),
("image_generations", "search_space_id", "workspace_id"),
("logs", "search_space_id", "workspace_id"),
("new_chat_threads", "search_space_id", "workspace_id"),
("notifications", "search_space_id", "workspace_id"),
("podcasts", "search_space_id", "workspace_id"),
("prompts", "search_space_id", "workspace_id"),
("reports", "search_space_id", "workspace_id"),
("search_source_connectors", "search_space_id", "workspace_id"),
("token_usage", "search_space_id", "workspace_id"),
("video_presentations", "search_space_id", "workspace_id"),
("external_chat_accounts", "owner_search_space_id", "owner_workspace_id"),
("workspace_roles", "search_space_id", "workspace_id"),
("workspace_memberships", "search_space_id", "workspace_id"),
("workspace_invites", "search_space_id", "workspace_id"),
]
# (table, old_constraint, new_constraint) -- table names are POST-table-rename.
CONSTRAINT_RENAMES: list[tuple[str, str, str]] = [
(
"agent_action_log",
"agent_action_log_search_space_id_fkey",
"agent_action_log_workspace_id_fkey",
),
(
"agent_permission_rules",
"agent_permission_rules_search_space_id_fkey",
"agent_permission_rules_workspace_id_fkey",
),
(
"automations",
"automations_search_space_id_fkey",
"automations_workspace_id_fkey",
),
(
"connections",
"connections_search_space_id_fkey",
"connections_workspace_id_fkey",
),
(
"document_files",
"document_files_search_space_id_fkey",
"document_files_workspace_id_fkey",
),
(
"document_revisions",
"document_revisions_search_space_id_fkey",
"document_revisions_workspace_id_fkey",
),
("documents", "documents_search_space_id_fkey", "documents_workspace_id_fkey"),
(
"external_chat_accounts",
"external_chat_accounts_owner_search_space_id_fkey",
"external_chat_accounts_owner_workspace_id_fkey",
),
(
"external_chat_bindings",
"external_chat_bindings_search_space_id_fkey",
"external_chat_bindings_workspace_id_fkey",
),
(
"folder_revisions",
"folder_revisions_search_space_id_fkey",
"folder_revisions_workspace_id_fkey",
),
("folders", "folders_search_space_id_fkey", "folders_workspace_id_fkey"),
(
"image_generations",
"image_generations_search_space_id_fkey",
"image_generations_workspace_id_fkey",
),
("logs", "logs_search_space_id_fkey", "logs_workspace_id_fkey"),
(
"new_chat_threads",
"new_chat_threads_search_space_id_fkey",
"new_chat_threads_workspace_id_fkey",
),
(
"notifications",
"notifications_search_space_id_fkey",
"notifications_workspace_id_fkey",
),
("podcasts", "podcasts_search_space_id_fkey", "podcasts_workspace_id_fkey"),
("prompts", "prompts_search_space_id_fkey", "prompts_workspace_id_fkey"),
("reports", "reports_search_space_id_fkey", "reports_workspace_id_fkey"),
(
"search_source_connectors",
"search_source_connectors_search_space_id_fkey",
"search_source_connectors_workspace_id_fkey",
),
(
"search_source_connectors",
"uq_searchspace_user_connector_type_name",
"uq_workspace_user_connector_type_name",
),
(
"token_usage",
"token_usage_search_space_id_fkey",
"token_usage_workspace_id_fkey",
),
(
"video_presentations",
"video_presentations_search_space_id_fkey",
"video_presentations_workspace_id_fkey",
),
(
"workspace_invites",
"search_space_invites_created_by_id_fkey",
"workspace_invites_created_by_id_fkey",
),
(
"workspace_invites",
"search_space_invites_pkey",
"workspace_invites_pkey",
),
(
"workspace_invites",
"search_space_invites_role_id_fkey",
"workspace_invites_role_id_fkey",
),
(
"workspace_invites",
"search_space_invites_search_space_id_fkey",
"workspace_invites_workspace_id_fkey",
),
(
"workspace_memberships",
"search_space_memberships_invited_by_invite_id_fkey",
"workspace_memberships_invited_by_invite_id_fkey",
),
(
"workspace_memberships",
"search_space_memberships_pkey",
"workspace_memberships_pkey",
),
(
"workspace_memberships",
"search_space_memberships_role_id_fkey",
"workspace_memberships_role_id_fkey",
),
(
"workspace_memberships",
"search_space_memberships_search_space_id_fkey",
"workspace_memberships_workspace_id_fkey",
),
(
"workspace_memberships",
"search_space_memberships_user_id_fkey",
"workspace_memberships_user_id_fkey",
),
(
"workspace_memberships",
"uq_user_searchspace_membership",
"uq_user_workspace_membership",
),
(
"workspace_roles",
"search_space_roles_pkey",
"workspace_roles_pkey",
),
(
"workspace_roles",
"search_space_roles_search_space_id_fkey",
"workspace_roles_workspace_id_fkey",
),
(
"workspace_roles",
"uq_searchspace_role_name",
"uq_workspace_role_name",
),
("workspaces", "searchspaces_pkey", "workspaces_pkey"),
("workspaces", "searchspaces_user_id_fkey", "workspaces_user_id_fkey"),
]
# (old_index, new_index) -- plain indexes only; PK/unique-backed indexes follow
# their RENAME CONSTRAINT above. ALTER INDEX IF EXISTS covers both create_all
# indexes and the runtime ``setup_indexes()`` ones (idx_documents_*).
INDEX_RENAMES: list[tuple[str, str]] = [
("ix_agent_action_log_search_space_id", "ix_agent_action_log_workspace_id"),
(
"ix_agent_permission_rules_search_space_id",
"ix_agent_permission_rules_workspace_id",
),
("ix_automations_search_space_id", "ix_automations_workspace_id"),
("ix_document_files_search_space_id", "ix_document_files_workspace_id"),
("ix_document_revisions_search_space_id", "ix_document_revisions_workspace_id"),
(
"ix_external_chat_bindings_search_space_state",
"ix_external_chat_bindings_workspace_state",
),
("ix_folder_revisions_search_space_id", "ix_folder_revisions_workspace_id"),
("ix_folders_search_space_id", "ix_folders_workspace_id"),
("ix_notifications_search_space_id", "ix_notifications_workspace_id"),
("ix_notifications_user_space_created", "ix_notifications_user_workspace_created"),
("ix_prompts_search_space_id", "ix_prompts_workspace_id"),
("ix_token_usage_search_space_id", "ix_token_usage_workspace_id"),
("ix_search_space_invites_created_at", "ix_workspace_invites_created_at"),
("ix_search_space_invites_id", "ix_workspace_invites_id"),
("ix_search_space_invites_invite_code", "ix_workspace_invites_invite_code"),
("ix_search_space_memberships_created_at", "ix_workspace_memberships_created_at"),
("ix_search_space_memberships_id", "ix_workspace_memberships_id"),
("ix_search_space_roles_created_at", "ix_workspace_roles_created_at"),
("ix_search_space_roles_id", "ix_workspace_roles_id"),
("ix_search_space_roles_name", "ix_workspace_roles_name"),
("ix_searchspaces_created_at", "ix_workspaces_created_at"),
("ix_searchspaces_id", "ix_workspaces_id"),
("ix_searchspaces_name", "ix_workspaces_name"),
("idx_documents_search_space_id", "idx_documents_workspace_id"),
("idx_documents_search_space_updated", "idx_documents_workspace_updated"),
]
# (old_sequence, new_sequence)
SEQUENCE_RENAMES: list[tuple[str, str]] = [
("searchspaces_id_seq", "workspaces_id_seq"),
("search_space_roles_id_seq", "workspace_roles_id_seq"),
("search_space_memberships_id_seq", "workspace_memberships_id_seq"),
("search_space_invites_id_seq", "workspace_invites_id_seq"),
]
# ---- Hardcoded OLD-shape publication (for downgrade only; finding 7). ----
# Mirrors app.zero_publication.ZERO_PUBLICATION at revision 169 but with the
# pre-rename ``search_space_id`` column. NEVER import the live module here: it
# now reflects ``workspace_id`` and would silently drop the column-list tables.
_DOWNGRADE_DOCUMENT_COLS = [
"id",
"title",
"document_type",
"search_space_id",
"folder_id",
"created_by_id",
"status",
"created_at",
"updated_at",
]
_DOWNGRADE_USER_COLS = ["id", "credit_micros_balance"]
_DOWNGRADE_AUTOMATION_RUN_COLS = [
"id",
"automation_id",
"trigger_id",
"status",
"step_results",
"started_at",
"finished_at",
"created_at",
]
_DOWNGRADE_AUTOMATION_COLS = ["id", "search_space_id"]
_DOWNGRADE_NEW_CHAT_THREAD_COLS = ["id", "search_space_id"]
_DOWNGRADE_PODCAST_COLS = [
"id",
"title",
"status",
"spec",
"spec_version",
"duration_seconds",
"error",
"search_space_id",
"thread_id",
"created_at",
]
# Ordered to match ZERO_PUBLICATION; None == all columns.
_DOWNGRADE_PUBLICATION: list[tuple[str, list[str] | None]] = [
("notifications", None),
("documents", _DOWNGRADE_DOCUMENT_COLS),
("folders", None),
("search_source_connectors", None),
("new_chat_threads", _DOWNGRADE_NEW_CHAT_THREAD_COLS),
("new_chat_messages", None),
("chat_comments", None),
("chat_session_state", None),
("user", _DOWNGRADE_USER_COLS),
("automations", _DOWNGRADE_AUTOMATION_COLS),
("automation_runs", _DOWNGRADE_AUTOMATION_RUN_COLS),
("podcasts", _DOWNGRADE_PODCAST_COLS),
]
def _quote(identifier: str) -> str:
return '"' + identifier.replace('"', '""') + '"'
def _publication_exists(conn) -> bool:
return (
conn.execute(
sa.text("SELECT 1 FROM pg_publication WHERE pubname = :n"),
{"n": PUBLICATION_NAME},
).fetchone()
is not None
)
def _is_publication_member(conn, table: str) -> bool:
return (
conn.execute(
sa.text(
"SELECT 1 FROM pg_publication_tables "
"WHERE pubname = :n AND schemaname = current_schema() "
"AND tablename = :t"
),
{"n": PUBLICATION_NAME, "t": table},
).fetchone()
is not None
)
def _table_columns(conn, table: str) -> set[str]:
rows = conn.execute(
sa.text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_schema = current_schema() AND table_name = :t"
),
{"t": table},
).fetchall()
return {row[0] for row in rows}
def _neutralize_column_list_tables(conn) -> None:
"""Remove the column-list dependency so RENAME COLUMN is permitted."""
if not _publication_exists(conn):
return
for table in PUBLICATION_COLUMN_LIST_TABLES:
if _is_publication_member(conn, table):
conn.execute(
sa.text(f"ALTER PUBLICATION {_quote(PUBLICATION_NAME)} DROP TABLE {table}")
)
def _rename_table(conn, old: str, new: str) -> None:
conn.execute(sa.text(f"ALTER TABLE IF EXISTS {old} RENAME TO {new}"))
def _rename_column(conn, table: str, old: str, new: str) -> None:
conn.execute(
sa.text(
f"""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = '{table}' AND column_name = '{old}'
) THEN
ALTER TABLE {table} RENAME COLUMN {old} TO {new};
END IF;
END$$;
"""
)
)
def _rename_constraint(conn, table: str, old: str, new: str) -> None:
conn.execute(
sa.text(
f"""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = '{old}'
AND conrelid = to_regclass('{table}')
) THEN
ALTER TABLE {table} RENAME CONSTRAINT {old} TO {new};
END IF;
END$$;
"""
)
)
def _rename_index(conn, old: str, new: str) -> None:
conn.execute(sa.text(f"ALTER INDEX IF EXISTS {old} RENAME TO {new}"))
def _rename_sequence(conn, old: str, new: str) -> None:
conn.execute(sa.text(f"ALTER SEQUENCE IF EXISTS {old} RENAME TO {new}"))
def _restore_downgrade_publication(conn) -> None:
"""Re-emit the OLD (search_space_id) publication shape via plain SET TABLE.
Does NOT call apply_publication (which reads the live, now-workspace_id
module) -- that would silently drop the column-list tables (finding 7).
"""
if not _publication_exists(conn):
return
# Drop the column-list tables first so the SET TABLE has no stale
# workspace_id dependency to trip over.
for table in PUBLICATION_COLUMN_LIST_TABLES:
if _is_publication_member(conn, table):
conn.execute(
sa.text(f"ALTER PUBLICATION {_quote(PUBLICATION_NAME)} DROP TABLE {table}")
)
entries: list[str] = []
for table, cols in _DOWNGRADE_PUBLICATION:
actual = _table_columns(conn, table)
if not actual:
continue
if cols is None:
entries.append(_quote(table))
continue
expected = list(cols)
if table in {"documents", "user", "podcasts"} and "_0_version" in actual:
expected.append("_0_version")
if any(col not in actual for col in expected):
continue
col_sql = ", ".join(_quote(col) for col in expected)
entries.append(f"{_quote(table)} ({col_sql})")
table_list = ", ".join(entries)
conn.execute(
sa.text(f"ALTER PUBLICATION {_quote(PUBLICATION_NAME)} SET TABLE {table_list}")
)
def upgrade() -> None:
conn = op.get_bind()
_neutralize_column_list_tables(conn)
for old, new in TABLE_RENAMES:
_rename_table(conn, old, new)
for table, old, new in COLUMN_RENAMES:
_rename_column(conn, table, old, new)
for table, old, new in CONSTRAINT_RENAMES:
_rename_constraint(conn, table, old, new)
for old, new in INDEX_RENAMES:
_rename_index(conn, old, new)
for old, new in SEQUENCE_RENAMES:
_rename_sequence(conn, old, new)
# Reconcile to the new workspace_id shape via the blessed apply_publication
# path (ALTER ... SET TABLE) -- never raw DROP/CREATE PUBLICATION (bug #1355,
# migration 116). No-op if the publication does not exist.
apply_publication(conn)
def downgrade() -> None:
conn = op.get_bind()
_neutralize_column_list_tables(conn)
for old, new in SEQUENCE_RENAMES:
_rename_sequence(conn, new, old)
for old, new in INDEX_RENAMES:
_rename_index(conn, new, old)
for table, old, new in CONSTRAINT_RENAMES:
# table here is the POST-rename name; constraints/tables are still
# renamed at this point (table rename is reversed last).
_rename_constraint(conn, table, new, old)
for table, old, new in COLUMN_RENAMES:
_rename_column(conn, table, new, old)
for old, new in TABLE_RENAMES:
_rename_table(conn, new, old)
_restore_downgrade_publication(conn)

View file

@ -124,7 +124,7 @@ async def create_anonymous_chat_agent(
tools (used when the user toggles web search off).
"""
tools = (
[create_web_search_tool(search_space_id=None, available_connectors=None)]
[create_web_search_tool(workspace_id=None, available_connectors=None)]
if enable_web_search
else []
)

View file

@ -31,7 +31,7 @@ def build_compiled_agent_graph_sync(
final_system_prompt: str,
backend_resolver: Any,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
user_id: str | None,
thread_id: int | None,
visibility: ChatVisibility,
@ -53,7 +53,7 @@ def build_compiled_agent_graph_sync(
tools=tools,
backend_resolver=backend_resolver,
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
thread_id=thread_id,
visibility=visibility,

View file

@ -14,7 +14,7 @@ def build_action_log_mw(
*,
flags: AgentFeatureFlags,
thread_id: int | None,
search_space_id: int,
workspace_id: int,
user_id: str | None,
) -> ActionLogMiddleware | None:
if not enabled(flags, "enable_action_log") or thread_id is None:
@ -25,7 +25,7 @@ def build_action_log_mw(
# tool via ``ToolDefinition.reverse`` and can be wired here when used.
return ActionLogMiddleware(
thread_id=thread_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
except Exception: # pragma: no cover - defensive

View file

@ -84,7 +84,7 @@ class ActionLogMiddleware(AgentMiddleware):
Args:
thread_id: The current chat thread's primary-key id. Required to
persist a row; if ``None`` the middleware silently no-ops.
search_space_id: Search-space id for cascade-on-delete safety.
workspace_id: Workspace id for cascade-on-delete safety.
user_id: UUID string of the user driving this turn (nullable in
anonymous mode).
tool_definitions: Optional mapping of tool name -> :class:`ToolDefinition`
@ -98,13 +98,13 @@ class ActionLogMiddleware(AgentMiddleware):
self,
*,
thread_id: int | None,
search_space_id: int,
workspace_id: int,
user_id: str | None,
tool_definitions: dict[str, ToolDefinition] | None = None,
) -> None:
super().__init__()
self._thread_id = thread_id
self._search_space_id = search_space_id
self._workspace_id = workspace_id
self._user_id = user_id
self._tool_definitions = dict(tool_definitions or {})
@ -187,7 +187,7 @@ class ActionLogMiddleware(AgentMiddleware):
row = AgentActionLog(
thread_id=thread_id,
user_id=self._user_id,
search_space_id=self._search_space_id,
workspace_id=self._workspace_id,
# ``turn_id`` is the deprecated alias of ``tool_call_id``
# kept for one release for safe rollback. New consumers
# should read ``tool_call_id`` directly.

View file

@ -40,7 +40,7 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware):
subagents: list[SubAgent | CompiledSubAgent],
system_prompt: str | None = TASK_SYSTEM_PROMPT,
task_description: str | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
) -> None:
self._surf_checkpointer = checkpointer
super(SubAgentMiddleware, self).__init__()
@ -50,11 +50,11 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware):
)
self._backend = backend
self._subagents = subagents
# Search-space id is captured at build time (the orchestrator runs in
# exactly one search space for its lifetime). The spawn-paused kill
# Workspace id is captured at build time (the orchestrator runs in
# exactly one workspace for its lifetime). The spawn-paused kill
# switch keys on it so an operator can quarantine one workspace
# without affecting the rest of the deployment.
self._search_space_id = search_space_id
self._workspace_id = workspace_id
# Lazy subagent compilation. Compiling a subagent graph via
# ``create_agent`` is expensive (~250-400ms each) and there can be up
@ -75,7 +75,7 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware):
task_tool = build_task_tool_with_parent_config(
descriptors,
task_description,
search_space_id=search_space_id,
workspace_id=workspace_id,
resolve_subagent=self._resolve_subagent,
)
if system_prompt and descriptors:

View file

@ -1,12 +1,12 @@
"""Per-search-space spawn-paused kill switch for the ``task`` boundary.
"""Per-workspace spawn-paused kill switch for the ``task`` boundary.
When operators see a runaway loop, a vendor outage, or a billing event
that requires immediate cessation of subagent traffic for a specific
workspace, they flip a Redis flag and the ``task`` tool short-circuits
without touching downstream services. The flag is **per-search-space**
without touching downstream services. The flag is **per-workspace**
so one tenant's incident never silences the rest of the deployment.
Flag key: ``surfsense:spawn_paused:{search_space_id}``
Flag key: ``surfsense:spawn_paused:{workspace_id}``
Flag value: any string-truthy value (we read presence, not contents).
TTL: set by whoever toggles the flag this module never expires
keys on its own, since "the flag is on" is itself the signal
@ -43,18 +43,18 @@ _DISABLED = os.environ.get(
}
def _flag_key(search_space_id: int) -> str:
return f"surfsense:spawn_paused:{search_space_id}"
def _flag_key(workspace_id: int) -> str:
return f"surfsense:spawn_paused:{workspace_id}"
async def is_spawn_paused(search_space_id: int | None) -> bool:
async def is_spawn_paused(workspace_id: int | None) -> bool:
"""Return ``True`` iff the workspace's spawn-paused flag is set in Redis.
A ``None`` search-space (e.g. dev paths that did not plumb the id
A ``None`` workspace (e.g. dev paths that did not plumb the id
through yet) bypasses the check. So does a Redis outage see module
docstring for the fail-open rationale.
"""
if _DISABLED or search_space_id is None:
if _DISABLED or workspace_id is None:
return False
try:
# Local import keeps the cold-path import cheap and lets routes
@ -63,7 +63,7 @@ async def is_spawn_paused(search_space_id: int | None) -> bool:
client = aioredis.from_url(config.REDIS_APP_URL, decode_responses=True)
try:
raw = await client.get(_flag_key(search_space_id))
raw = await client.get(_flag_key(workspace_id))
finally:
# ``aclose()`` is the async-safe variant on redis-py >=5; fall back
# to ``close()`` for older clients pinned in tests.
@ -74,8 +74,8 @@ async def is_spawn_paused(search_space_id: int | None) -> bool:
return bool(raw)
except Exception:
logger.warning(
"spawn_paused check failed for search_space_id=%s; failing open.",
search_space_id,
"spawn_paused check failed for workspace_id=%s; failing open.",
workspace_id,
exc_info=True,
)
return False

View file

@ -142,7 +142,7 @@ def build_task_tool_with_parent_config(
subagents: list[dict[str, Any]],
task_description: str | None = None,
*,
search_space_id: int | None = None,
workspace_id: int | None = None,
resolve_subagent: Callable[[str], Runnable] | None = None,
) -> BaseTool:
"""Upstream ``_build_task_tool`` + parent ``runtime.config`` propagation + resume bridging.
@ -829,10 +829,10 @@ def build_task_tool_with_parent_config(
atask_start = time.perf_counter()
# Ops kill switch: short-circuit every task() call for this workspace
# so the orchestrator stops hammering downstream APIs.
if await is_spawn_paused(search_space_id):
if await is_spawn_paused(workspace_id):
logger.warning(
"[hitl_route] atask SPAWN_PAUSED: search_space_id=%s tool_call_id=%s",
search_space_id,
"[hitl_route] atask SPAWN_PAUSED: workspace_id=%s tool_call_id=%s",
workspace_id,
runtime.tool_call_id,
)
return (

View file

@ -12,14 +12,14 @@ from .middleware import (
def build_kb_persistence_mw(
*,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
user_id: str | None,
thread_id: int | None,
) -> KnowledgeBasePersistenceMiddleware | None:
if filesystem_mode != FilesystemMode.CLOUD:
return None
return KnowledgeBasePersistenceMiddleware(
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=user_id,
filesystem_mode=filesystem_mode,
thread_id=thread_id,

View file

@ -83,11 +83,11 @@ def _basename(path: str) -> str:
async def _ensure_folder_hierarchy(
session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
created_by_id: str | None,
folder_parts: list[str],
) -> int | None:
"""Ensure a chain of folder names exists under the search space.
"""Ensure a chain of folder names exists under the workspace.
Returns the leaf folder id, or ``None`` if ``folder_parts`` is empty
(i.e. a document directly under ``/documents/``).
@ -98,7 +98,7 @@ async def _ensure_folder_hierarchy(
for raw in folder_parts:
name = safe_folder_segment(str(raw))
query = select(Folder).where(
Folder.search_space_id == search_space_id,
Folder.workspace_id == workspace_id,
Folder.name == name,
)
if parent_id is None:
@ -112,7 +112,7 @@ async def _ensure_folder_hierarchy(
select(Folder.position).order_by(Folder.position.desc()).limit(1)
)
sibling_query = sibling_query.where(
Folder.search_space_id == search_space_id
Folder.workspace_id == workspace_id
)
if parent_id is None:
sibling_query = sibling_query.where(Folder.parent_id.is_(None))
@ -124,7 +124,7 @@ async def _ensure_folder_hierarchy(
name=name,
position=generate_key_between(last_position, None),
parent_id=parent_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_at=datetime.now(UTC),
)
@ -137,7 +137,7 @@ async def _ensure_folder_hierarchy(
async def _resolve_folder_id(
session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
folder_parts: list[str],
) -> int | None:
"""Look up an existing folder chain without creating anything.
@ -151,7 +151,7 @@ async def _resolve_folder_id(
for raw in folder_parts:
name = safe_folder_segment(str(raw))
query = select(Folder).where(
Folder.search_space_id == search_space_id,
Folder.workspace_id == workspace_id,
Folder.name == name,
)
query = (
@ -185,7 +185,7 @@ async def _create_document(
*,
virtual_path: str,
content: str,
search_space_id: int,
workspace_id: int,
created_by_id: str | None,
) -> Document:
"""Create a NOTE Document + Chunks for ``virtual_path``."""
@ -194,21 +194,21 @@ async def _create_document(
raise ValueError(f"invalid /documents path '{virtual_path}'")
folder_id = await _ensure_folder_hierarchy(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
folder_parts=folder_parts,
)
unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.NOTE,
virtual_path,
search_space_id,
workspace_id,
)
# Pre-check the path-derived unique_identifier_hash so a duplicate path
# surfaces as a clean ValueError instead of an INSERT IntegrityError that
# poisons the session. Content is intentionally not unique (cp a b).
path_collision = await session.execute(
select(Document.id).where(
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
Document.unique_identifier_hash == unique_identifier_hash,
)
)
@ -217,7 +217,7 @@ async def _create_document(
f"a document already exists at path '{virtual_path}' "
"(unique_identifier_hash collision)"
)
content_hash = generate_content_hash(content, search_space_id)
content_hash = generate_content_hash(content, workspace_id)
doc = Document(
title=title,
document_type=DocumentType.NOTE,
@ -226,7 +226,7 @@ async def _create_document(
content_hash=content_hash,
unique_identifier_hash=unique_identifier_hash,
source_markdown=content,
search_space_id=search_space_id,
workspace_id=workspace_id,
folder_id=folder_id,
created_by_id=created_by_id,
updated_at=datetime.now(UTC),
@ -261,13 +261,13 @@ async def _update_document(
doc_id: int,
content: str,
virtual_path: str,
search_space_id: int,
workspace_id: int,
) -> Document | None:
"""Update an existing Document's content + chunks."""
result = await session.execute(
select(Document).where(
Document.id == doc_id,
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
)
)
document = result.scalar_one_or_none()
@ -276,7 +276,7 @@ async def _update_document(
document.content = content
document.source_markdown = content
document.content_hash = generate_content_hash(content, search_space_id)
document.content_hash = generate_content_hash(content, workspace_id)
document.updated_at = datetime.now(UTC)
metadata = dict(document.document_metadata or {})
metadata["virtual_path"] = virtual_path
@ -284,7 +284,7 @@ async def _update_document(
document.unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.NOTE,
virtual_path,
search_space_id,
workspace_id,
)
summary_embedding = (await asyncio.to_thread(embed_texts, [content]))[0]
@ -318,7 +318,7 @@ async def _update_document(
async def _apply_move(
session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
created_by_id: str | None,
move: dict[str, Any],
doc_id_by_path: dict[str, int],
@ -341,14 +341,14 @@ async def _apply_move(
result = await session.execute(
select(Document).where(
Document.id == doc_id,
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
)
)
document = result.scalar_one_or_none()
if document is None:
document = await virtual_path_to_doc(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
virtual_path=source,
)
if document is None:
@ -364,7 +364,7 @@ async def _apply_move(
return None
folder_id = await _ensure_folder_hierarchy(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
folder_parts=folder_parts,
)
@ -377,7 +377,7 @@ async def _apply_move(
document.unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.NOTE,
dest,
search_space_id,
workspace_id,
)
document.updated_at = datetime.now(UTC)
@ -501,7 +501,7 @@ async def _snapshot_document_pre_write(
*,
doc: Document,
action_id: int | None,
search_space_id: int,
workspace_id: int,
turn_id: str | None = None,
deferred_dispatches: list[int] | None = None,
) -> int | None:
@ -517,7 +517,7 @@ async def _snapshot_document_pre_write(
payload = _doc_revision_payload(doc, chunks_before=chunks)
rev = DocumentRevision(
document_id=doc.id,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_turn_id=turn_id,
agent_action_id=action_id,
**payload,
@ -544,7 +544,7 @@ async def _snapshot_document_pre_create(
session: AsyncSession,
*,
action_id: int | None,
search_space_id: int,
workspace_id: int,
turn_id: str | None = None,
deferred_dispatches: list[int] | None = None,
) -> int | None:
@ -558,7 +558,7 @@ async def _snapshot_document_pre_create(
async with session.begin_nested():
rev = DocumentRevision(
document_id=None,
search_space_id=search_space_id,
workspace_id=workspace_id,
content_before=None,
title_before=None,
folder_id_before=None,
@ -586,7 +586,7 @@ async def _snapshot_document_pre_move(
*,
doc: Document,
action_id: int | None,
search_space_id: int,
workspace_id: int,
turn_id: str | None = None,
deferred_dispatches: list[int] | None = None,
) -> int | None:
@ -596,7 +596,7 @@ async def _snapshot_document_pre_move(
payload = _doc_revision_payload(doc, chunks_before=None)
rev = DocumentRevision(
document_id=doc.id,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_turn_id=turn_id,
agent_action_id=action_id,
**payload,
@ -624,7 +624,7 @@ async def _snapshot_folder_pre_mkdir(
*,
folder: Folder,
action_id: int | None,
search_space_id: int,
workspace_id: int,
turn_id: str | None = None,
deferred_dispatches: list[int] | None = None,
) -> int | None:
@ -637,7 +637,7 @@ async def _snapshot_folder_pre_mkdir(
async with session.begin_nested():
rev = FolderRevision(
folder_id=folder.id,
search_space_id=search_space_id,
workspace_id=workspace_id,
name_before=None,
parent_id_before=None,
position_before=None,
@ -670,7 +670,7 @@ async def _snapshot_folder_pre_mkdir(
async def commit_staged_filesystem_state(
state: dict[str, Any] | AgentState,
*,
search_space_id: int,
workspace_id: int,
created_by_id: str | None,
filesystem_mode: FilesystemMode = FilesystemMode.CLOUD,
thread_id: int | None = None,
@ -814,7 +814,7 @@ async def commit_staged_filesystem_state(
continue
folder_id = await _ensure_folder_hierarchy(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
folder_parts=folder_parts_full,
)
@ -833,7 +833,7 @@ async def commit_staged_filesystem_state(
session,
folder=folder_row,
action_id=action_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
turn_id=tcid,
deferred_dispatches=deferred_dispatches,
)
@ -851,14 +851,14 @@ async def commit_staged_filesystem_state(
res_pre = await session.execute(
select(Document).where(
Document.id == doc_id_pre,
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
)
)
document_pre = res_pre.scalar_one_or_none()
if document_pre is None:
document_pre = await virtual_path_to_doc(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
virtual_path=source,
)
if document_pre is not None:
@ -866,14 +866,14 @@ async def commit_staged_filesystem_state(
session,
doc=document_pre,
action_id=action_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
turn_id=tcid,
deferred_dispatches=deferred_dispatches,
)
applied = await _apply_move(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
move=move,
doc_id_by_path=doc_id_by_path,
@ -937,7 +937,7 @@ async def commit_staged_filesystem_state(
# INSERT (which would hit the path-derived unique hash).
existing = await virtual_path_to_doc(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
virtual_path=path,
)
if existing is not None:
@ -948,7 +948,7 @@ async def commit_staged_filesystem_state(
result_doc = await session.execute(
select(Document).where(
Document.id == doc_id,
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
)
)
existing_doc = result_doc.scalar_one_or_none()
@ -957,7 +957,7 @@ async def commit_staged_filesystem_state(
session,
doc=existing_doc,
action_id=action_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
turn_id=tcid,
deferred_dispatches=deferred_dispatches,
)
@ -966,7 +966,7 @@ async def commit_staged_filesystem_state(
doc_id=doc_id,
content=content,
virtual_path=path,
search_space_id=search_space_id,
workspace_id=workspace_id,
)
if updated is not None:
committed_updates.append(
@ -974,7 +974,7 @@ async def commit_staged_filesystem_state(
"id": updated.id,
"title": updated.title,
"documentType": DocumentType.NOTE.value,
"searchSpaceId": search_space_id,
"workspaceId": workspace_id,
"folderId": updated.folder_id,
"createdById": str(created_by_id)
if created_by_id
@ -991,7 +991,7 @@ async def commit_staged_filesystem_state(
placeholder_revision_id = await _snapshot_document_pre_create(
session,
action_id=action_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
turn_id=tcid,
deferred_dispatches=deferred_dispatches,
)
@ -1001,7 +1001,7 @@ async def commit_staged_filesystem_state(
session,
virtual_path=path,
content=content,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
)
except ValueError as exc:
@ -1045,7 +1045,7 @@ async def commit_staged_filesystem_state(
"id": new_doc.id,
"title": new_doc.title,
"documentType": DocumentType.NOTE.value,
"searchSpaceId": search_space_id,
"workspaceId": workspace_id,
"folderId": new_doc.folder_id,
"createdById": str(created_by_id)
if created_by_id
@ -1069,14 +1069,14 @@ async def commit_staged_filesystem_state(
result = await session.execute(
select(Document).where(
Document.id == doc_id_for_delete,
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
)
)
document_to_delete = result.scalar_one_or_none()
if document_to_delete is None:
document_to_delete = await virtual_path_to_doc(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
virtual_path=final,
)
if document_to_delete is None:
@ -1100,7 +1100,7 @@ async def commit_staged_filesystem_state(
)
rev = DocumentRevision(
document_id=doc_pk,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_turn_id=tcid,
agent_action_id=action_id,
**payload,
@ -1130,7 +1130,7 @@ async def commit_staged_filesystem_state(
"id": doc_pk,
"title": doc_title,
"documentType": DocumentType.NOTE.value,
"searchSpaceId": search_space_id,
"workspaceId": workspace_id,
"folderId": doc_folder_id,
"createdById": str(created_by_id) if created_by_id else None,
"virtualPath": final,
@ -1151,7 +1151,7 @@ async def commit_staged_filesystem_state(
continue
folder_id = await _resolve_folder_id(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
folder_parts=folder_parts,
)
if folder_id is None:
@ -1163,7 +1163,7 @@ async def commit_staged_filesystem_state(
docs_in_folder = await session.execute(
select(Document.id)
.where(Document.folder_id == folder_id)
.where(Document.search_space_id == search_space_id)
.where(Document.workspace_id == workspace_id)
.limit(1)
)
if docs_in_folder.scalar_one_or_none() is not None:
@ -1175,7 +1175,7 @@ async def commit_staged_filesystem_state(
child_folders = await session.execute(
select(Folder.id)
.where(Folder.parent_id == folder_id)
.where(Folder.search_space_id == search_space_id)
.where(Folder.workspace_id == workspace_id)
.limit(1)
)
if child_folders.scalar_one_or_none() is not None:
@ -1203,7 +1203,7 @@ async def commit_staged_filesystem_state(
if snapshot_enabled and action_id is not None:
rev = FolderRevision(
folder_id=folder_pk,
search_space_id=search_space_id,
workspace_id=workspace_id,
name_before=folder_name,
parent_id_before=folder_parent_id,
position_before=folder_position,
@ -1232,7 +1232,7 @@ async def commit_staged_filesystem_state(
{
"id": folder_pk,
"name": folder_name,
"searchSpaceId": search_space_id,
"workspaceId": workspace_id,
"parentId": folder_parent_id,
"virtualPath": final,
}
@ -1242,7 +1242,7 @@ async def commit_staged_filesystem_state(
await session.commit()
except Exception: # pragma: no cover - rollback safety net
logger.exception(
"kb_persistence: commit failed (search_space=%s)", search_space_id
"kb_persistence: commit failed (workspace=%s)", workspace_id
)
# Outer commit raised: everything above rolled back, so drop the
# deferred dispatches.
@ -1402,9 +1402,9 @@ async def commit_staged_filesystem_state(
_ = turn_id_for_revision # diagnostic-only; silence unused lint
logger.info(
"kb_persistence: commit (search_space=%s) creates=%d updates=%d "
"kb_persistence: commit (workspace=%s) creates=%d updates=%d "
"moves=%d staged_dirs=%d deletes=%d folder_deletes=%d discarded=%d",
search_space_id,
workspace_id,
len(committed_creates),
len(committed_updates),
len(applied_moves),
@ -1430,12 +1430,12 @@ class KnowledgeBasePersistenceMiddleware(AgentMiddleware): # type: ignore[type-
def __init__(
self,
*,
search_space_id: int,
workspace_id: int,
created_by_id: str | None,
filesystem_mode: FilesystemMode,
thread_id: int | None = None,
) -> None:
self.search_space_id = search_space_id
self.workspace_id = workspace_id
self.created_by_id = created_by_id
self.filesystem_mode = filesystem_mode
self.thread_id = thread_id
@ -1450,7 +1450,7 @@ class KnowledgeBasePersistenceMiddleware(AgentMiddleware): # type: ignore[type-
return None
return await commit_staged_filesystem_state(
state,
search_space_id=self.search_space_id,
workspace_id=self.workspace_id,
created_by_id=self.created_by_id,
filesystem_mode=self.filesystem_mode,
thread_id=self._resolve_thread_id(),

View file

@ -12,13 +12,13 @@ from .middleware import KnowledgeTreeMiddleware
def build_knowledge_tree_mw(
*,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
llm: BaseChatModel,
) -> KnowledgeTreeMiddleware | None:
if filesystem_mode != FilesystemMode.CLOUD:
return None
return KnowledgeTreeMiddleware(
search_space_id=search_space_id,
workspace_id=workspace_id,
filesystem_mode=filesystem_mode,
llm=llm,
inject_system_message=False,

View file

@ -1,7 +1,7 @@
"""Workspace-tree middleware for the SurfSense agent.
Renders the full ``Folder``+``Document`` tree under ``/documents/`` once per
turn (cloud only), caches it by ``(search_space_id, tree_version)``, and
turn (cloud only), caches it by ``(workspace_id, tree_version)``, and
injects the result as a ``<workspace_tree>`` system message immediately
before the latest human turn.
@ -106,14 +106,14 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg]
def __init__(
self,
*,
search_space_id: int,
workspace_id: int,
filesystem_mode: FilesystemMode,
llm: BaseChatModel | None = None,
max_entries: int = MAX_TREE_ENTRIES,
max_tokens: int = MAX_TREE_TOKENS,
inject_system_message: bool = True, # For backwards compatibility
) -> None:
self.search_space_id = search_space_id
self.workspace_id = workspace_id
self.filesystem_mode = filesystem_mode
self.llm = llm
self.max_entries = max_entries
@ -141,7 +141,7 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg]
cache_outcome = "anon"
else:
version = int(state.get("tree_version") or 0)
cache_key = (self.search_space_id, version, False)
cache_key = (self.workspace_id, version, False)
cache_outcome = "hit" if cache_key in self._cache else "miss"
tree_msg = await self._render_kb_tree(state)
@ -158,7 +158,7 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg]
cache_outcome,
len(tree_msg),
time.perf_counter() - start,
self.search_space_id,
self.workspace_id,
)
return update
@ -190,17 +190,17 @@ class KnowledgeTreeMiddleware(AgentMiddleware): # type: ignore[type-arg]
async def _render_kb_tree(self, state: AgentState) -> str:
version = int(state.get("tree_version") or 0)
cache_key = (self.search_space_id, version, False)
cache_key = (self.workspace_id, version, False)
cached = self._cache.get(cache_key)
if cached is not None:
return cached
try:
async with shielded_async_session() as session:
index = await build_path_index(session, self.search_space_id)
index = await build_path_index(session, self.workspace_id)
doc_rows = await session.execute(
select(Document.id, Document.title, Document.folder_id).where(
Document.search_space_id == self.search_space_id
Document.workspace_id == self.workspace_id
)
)
docs = list(doc_rows.all())

View file

@ -10,11 +10,11 @@ from .middleware import MemoryInjectionMiddleware
def build_memory_mw(
*,
user_id: str | None,
search_space_id: int,
workspace_id: int,
visibility: ChatVisibility,
) -> MemoryInjectionMiddleware:
return MemoryInjectionMiddleware(
user_id=user_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_visibility=visibility,
)

View file

@ -18,7 +18,7 @@ from langgraph.runtime import Runtime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import ChatVisibility, SearchSpace, User, shielded_async_session
from app.db import ChatVisibility, Workspace, User, shielded_async_session
from app.services.memory import MEMORY_HARD_LIMIT, MEMORY_SOFT_LIMIT
from app.utils.perf import get_perf_logger
@ -35,11 +35,11 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg]
self,
*,
user_id: str | UUID | None,
search_space_id: int,
workspace_id: int,
thread_visibility: ChatVisibility | None = None,
) -> None:
self.user_id = UUID(user_id) if isinstance(user_id, str) else user_id
self.search_space_id = search_space_id
self.workspace_id = workspace_id
self.visibility = thread_visibility or ChatVisibility.PRIVATE
async def abefore_agent( # type: ignore[override]
@ -149,8 +149,8 @@ class MemoryInjectionMiddleware(AgentMiddleware): # type: ignore[type-arg]
async def _load_team_memory(self, session: AsyncSession) -> str | None:
try:
result = await session.execute(
select(SearchSpace.shared_memory_md).where(
SearchSpace.id == self.search_space_id
select(Workspace.shared_memory_md).where(
Workspace.id == self.workspace_id
)
)
row = result.scalar_one_or_none()

View file

@ -21,7 +21,7 @@ from ..plugins.loader import (
def build_plugin_middlewares(
*,
flags: AgentFeatureFlags,
search_space_id: int,
workspace_id: int,
user_id: str | None,
visibility: ChatVisibility,
llm: BaseChatModel,
@ -34,7 +34,7 @@ def build_plugin_middlewares(
return []
return load_plugin_middlewares(
PluginContext.build(
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
thread_visibility=visibility,
llm=llm,

View file

@ -17,13 +17,13 @@ def build_skills_mw(
*,
flags: AgentFeatureFlags,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
) -> SkillsMiddleware | None:
if not enabled(flags, "enable_skills"):
return None
try:
skills_factory = build_skills_backend_factory(
search_space_id=search_space_id
workspace_id=workspace_id
if filesystem_mode == FilesystemMode.CLOUD
else None,
)

View file

@ -99,7 +99,7 @@ def build_main_agent_deepagent_middleware(
tools: Sequence[BaseTool],
backend_resolver: Any,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
user_id: str | None,
thread_id: int | None,
visibility: ChatVisibility,
@ -120,7 +120,7 @@ def build_main_agent_deepagent_middleware(
memory_mw = build_memory_mw(
user_id=user_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
visibility=visibility,
)
@ -232,19 +232,19 @@ def build_main_agent_deepagent_middleware(
),
build_knowledge_tree_mw(
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
llm=llm,
),
build_kb_persistence_mw(
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
thread_id=thread_id,
),
build_skills_mw(
flags=flags,
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
),
SurfSenseCheckpointedSubAgentMiddleware(
checkpointer=checkpointer,
@ -252,7 +252,7 @@ def build_main_agent_deepagent_middleware(
subagents=subagents,
system_prompt=None,
task_description=TASK_TOOL_DESCRIPTION,
search_space_id=search_space_id,
workspace_id=workspace_id,
),
resilience.model_call_limit,
resilience.tool_call_limit,
@ -272,14 +272,14 @@ def build_main_agent_deepagent_middleware(
build_action_log_mw(
flags=flags,
thread_id=thread_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
),
build_patch_tool_calls_mw(),
build_dedup_hitl_mw(tools),
*build_plugin_middlewares(
flags=flags,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
visibility=visibility,
llm=llm,

View file

@ -57,7 +57,7 @@ class PluginContext(dict):
Backed by ``dict`` so plugins can inspect the keys they care about
without coupling to a concrete dataclass shape. Required keys:
* ``search_space_id`` (int)
* ``workspace_id`` (int)
* ``user_id`` (str | None)
* ``thread_visibility`` (:class:`app.db.ChatVisibility`)
* ``llm`` (:class:`langchain_core.language_models.BaseChatModel`)
@ -72,13 +72,13 @@ class PluginContext(dict):
def build(
cls,
*,
search_space_id: int,
workspace_id: int,
user_id: str | None,
thread_visibility: ChatVisibility,
llm: BaseChatModel,
) -> PluginContext:
return cls(
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
thread_visibility=thread_visibility,
llm=llm,

View file

@ -7,7 +7,7 @@ result. This particular plugin is read-only and only transforms the
mutation).
The plugin is built as a factory function so the entry-point loader can
inject :class:`PluginContext` (containing the agent's LLM, search-space
inject :class:`PluginContext` (containing the agent's LLM, workspace
ID, etc.). The factory signature
``Callable[[PluginContext], AgentMiddleware]`` is the only contract --
SurfSense doesn't define a custom plugin protocol on top of LangChain's

View file

@ -42,7 +42,7 @@ async def build_agent_with_cache(
final_system_prompt: str,
backend_resolver: Any,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
user_id: str | None,
thread_id: int | None,
visibility: ChatVisibility,
@ -69,7 +69,7 @@ async def build_agent_with_cache(
final_system_prompt=final_system_prompt,
backend_resolver=backend_resolver,
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
thread_id=thread_id,
visibility=visibility,
@ -104,7 +104,7 @@ async def build_agent_with_cache(
config_id,
None if cross_thread else thread_id,
user_id,
search_space_id,
workspace_id,
visibility,
filesystem_mode,
anon_session_id,
@ -120,7 +120,7 @@ async def build_agent_with_cache(
sorted(disabled_tools) if disabled_tools else None,
# Bound into the generate_image subagent tool at construction time, so it
# must key the compiled-agent cache to avoid leaking one automation's
# image model into another with the same config_id/search_space.
# image model into another with the same config_id/workspace.
image_gen_model_id_override,
)
return await get_cache().get_or_build(cache_key, builder=_build)

View file

@ -26,7 +26,7 @@ Why a per-thread key (not a global pool)
----------------------------------------
Most middleware in the SurfSense stack captures per-thread state in
``__init__`` closures (``thread_id``, ``user_id``, ``search_space_id``,
``__init__`` closures (``thread_id``, ``user_id``, ``workspace_id``,
``filesystem_mode``, ``mentioned_document_ids``). Cross-thread reuse
would silently leak state across users and threads. Keying the cache on
``(llm_config_id, thread_id, ...)`` gives us safe reuse for repeated
@ -34,7 +34,7 @@ turns on the same thread without changing any middleware's behavior.
Phase 2 will move those captured fields onto :class:`SurfSenseContextSchema`
(read via ``runtime.context``) so the cache can collapse to a single
``(llm_config_id, search_space_id, ...)`` key shared across threads. Until
``(llm_config_id, workspace_id, ...)`` key shared across threads. Until
then, per-thread keying is the only safe option.
Cache shape
@ -111,7 +111,7 @@ def tools_signature(
* A tool is added or removed from the bound list (built-in toggles,
MCP tools loaded for the user changes, gating rules flip, etc.).
* The available connectors / document types for the search space
* The available connectors / document types for the workspace
change (new connector added, last connector removed, new document
type indexed). Connector gating derives disabled tools from
``available_connectors``, so the tool surface is technically already

View file

@ -1,7 +1,7 @@
"""Map configured connectors to the searchable document/connector types.
This is agent-agnostic infrastructure shared by every agent factory (single-
and multi-agent). It translates the connectors a search space has enabled into
and multi-agent). It translates the connectors a workspace has enabled into
the set of searchable type strings that pre-search middleware and ``web_search``
understand, and always layers in the document types that exist independently of
any connector (uploads, notes, extension captures, YouTube).

View file

@ -58,7 +58,7 @@ _perf_log = get_perf_logger()
async def create_multi_agent_chat_deep_agent(
llm: BaseChatModel,
search_space_id: int,
workspace_id: int,
db_session: AsyncSession,
connector_service: ConnectorService,
checkpointer: Checkpointer,
@ -78,9 +78,9 @@ async def create_multi_agent_chat_deep_agent(
):
"""Deep agent with SurfSense tools/middleware; registry route subagents behind ``task`` when enabled.
``image_gen_model_id`` overrides the search space's image model for
``image_gen_model_id`` overrides the workspace's image model for
this invocation (used by automations to run on their captured model). When
``None``, the ``generate_image`` tool resolves the live search-space pref.
``None``, the ``generate_image`` tool resolves the live workspace pref.
"""
_t_agent_total = time.perf_counter()
@ -89,7 +89,7 @@ async def create_multi_agent_chat_deep_agent(
filesystem_selection = filesystem_selection or FilesystemSelection()
backend_resolver = build_backend_resolver(
filesystem_selection,
search_space_id=search_space_id
workspace_id=workspace_id
if filesystem_selection.mode == FilesystemMode.CLOUD
else None,
)
@ -100,12 +100,12 @@ async def create_multi_agent_chat_deep_agent(
_t0 = time.perf_counter()
try:
connector_types = await connector_service.get_available_connectors(
search_space_id
workspace_id
)
available_connectors = map_connectors_to_searchable_types(connector_types)
available_document_types = await connector_service.get_available_document_types(
search_space_id
workspace_id
)
except Exception as e:
@ -136,7 +136,7 @@ async def create_multi_agent_chat_deep_agent(
)
dependencies: dict[str, Any] = {
"search_space_id": search_space_id,
"workspace_id": workspace_id,
"db_session": db_session,
"connector_service": connector_service,
"firecrawl_api_key": firecrawl_api_key,
@ -156,7 +156,7 @@ async def create_multi_agent_chat_deep_agent(
_t0 = time.perf_counter()
try:
mcp_tools_by_agent = await load_mcp_tools_by_connector(
db_session, search_space_id
db_session, workspace_id
)
except Exception as e:
# Degrade to builtins-only rather than aborting the turn: a transient
@ -193,7 +193,7 @@ async def create_multi_agent_chat_deep_agent(
user_allowlist_by_subagent = await fetch_user_allowlist_rulesets(
db_session,
user_id=user_uuid,
search_space_id=search_space_id,
workspace_id=workspace_id,
)
except Exception as e:
logging.warning(
@ -291,7 +291,7 @@ async def create_multi_agent_chat_deep_agent(
final_system_prompt=final_system_prompt,
backend_resolver=backend_resolver,
filesystem_mode=filesystem_selection.mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
thread_id=thread_id,
visibility=visibility,

View file

@ -17,12 +17,12 @@ Two backends are provided:
* :class:`BuiltinSkillsBackend` disk-backed read of bundled skills from
``app/agents/shared/skills/builtin/``.
* :class:`SearchSpaceSkillsBackend` a thin read-only wrapper over
* :class:`WorkspaceSkillsBackend` a thin read-only wrapper over
:class:`KBPostgresBackend` that filters notes under the privileged folder
``/documents/_skills/``.
Both backends are intentionally read-only: skill authoring happens out of band
(via filesystem or a search-space-admin route), so we never expose
(via filesystem or a workspace-admin route), so we never expose
``write`` / ``edit`` / ``upload_files``. The base class' ``NotImplementedError``
gives a clean failure mode if anything tries.
"""
@ -181,12 +181,12 @@ class BuiltinSkillsBackend(BackendProtocol):
return responses
class SearchSpaceSkillsBackend(BackendProtocol):
"""Read-only view of search-space-authored skills.
class WorkspaceSkillsBackend(BackendProtocol):
"""Read-only view of workspace-authored skills.
Wraps a :class:`KBPostgresBackend` and only ever reads under the privileged
folder ``/documents/_skills/`` (configurable). The folder is intended to be
writable only by search-space admins; this backend never writes.
writable only by workspace admins; this backend never writes.
The skills middleware expects a layout like::
@ -236,14 +236,14 @@ class SearchSpaceSkillsBackend(BackendProtocol):
# path falls back to ``asyncio.to_thread(...)`` in the base class. We
# keep this stub to satisfy abstract resolution; the middleware calls
# ``als_info``.
raise NotImplementedError("SearchSpaceSkillsBackend is async-only")
raise NotImplementedError("WorkspaceSkillsBackend is async-only")
async def als_info(self, path: str) -> list[FileInfo]:
kb_path = self._to_kb(path)
try:
infos = await self._kb.als_info(kb_path)
except Exception as exc: # pragma: no cover - defensive
logger.warning("SearchSpaceSkillsBackend.als_info failed: %s", exc)
logger.warning("WorkspaceSkillsBackend.als_info failed: %s", exc)
return []
remapped: list[FileInfo] = []
for info in infos:
@ -254,7 +254,7 @@ class SearchSpaceSkillsBackend(BackendProtocol):
return remapped
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
raise NotImplementedError("SearchSpaceSkillsBackend is async-only")
raise NotImplementedError("WorkspaceSkillsBackend is async-only")
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
kb_paths = [self._to_kb(p) for p in paths]
@ -274,16 +274,16 @@ SKILLS_SPACE_PREFIX = "/skills/space/"
def build_skills_backend_factory(
*,
builtin_root: Path | str | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
) -> Callable[[ToolRuntime], BackendProtocol]:
"""Return a runtime-aware factory for the skills :class:`CompositeBackend`.
When ``search_space_id`` is provided the composite includes a
:class:`SearchSpaceSkillsBackend` route at ``/skills/space/`` over a fresh
When ``workspace_id`` is provided the composite includes a
:class:`WorkspaceSkillsBackend` route at ``/skills/space/`` over a fresh
per-runtime :class:`KBPostgresBackend`, mirroring how
:func:`build_backend_resolver` constructs the main filesystem backend.
When ``search_space_id`` is ``None`` (e.g., desktop-local mode or unit
When ``workspace_id`` is ``None`` (e.g., desktop-local mode or unit
tests) only the bundled :class:`BuiltinSkillsBackend` is exposed.
Returning a factory rather than a fixed instance is intentional: the
@ -293,7 +293,7 @@ def build_skills_backend_factory(
"""
builtin = BuiltinSkillsBackend(builtin_root)
if search_space_id is None:
if workspace_id is None:
def _factory_builtin_only(runtime: ToolRuntime) -> BackendProtocol:
# Default StateBackend is intentionally inert: any path outside the
@ -314,8 +314,8 @@ def build_skills_backend_factory(
KBPostgresBackend,
)
kb = KBPostgresBackend(search_space_id, runtime)
space = SearchSpaceSkillsBackend(kb)
kb = KBPostgresBackend(workspace_id, runtime)
space = WorkspaceSkillsBackend(kb)
return CompositeBackend(
default=StateBackend(runtime),
routes={
@ -336,7 +336,7 @@ __all__ = [
"SKILLS_BUILTIN_PREFIX",
"SKILLS_SPACE_PREFIX",
"BuiltinSkillsBackend",
"SearchSpaceSkillsBackend",
"WorkspaceSkillsBackend",
"build_skills_backend_factory",
"default_skills_sources",
]

View file

@ -1,5 +1,5 @@
- `update_memory` — Curate the team's **shared** long-term memory document
for this search space.
for this workspace.
- The current memory (if any) appears in `<team_memory>` with usage vs limit.
- Call when a team member asks to remember or forget something, or when
the conversation surfaces durable team decisions, conventions,

View file

@ -45,14 +45,14 @@ _JSON_FENCE = re.compile(r"```(?:json)?\s*(.*?)\s*```", re.DOTALL)
def create_create_automation_tool(
*,
search_space_id: int,
workspace_id: int,
user_id: str | UUID,
llm: Any,
auth_context: AuthContext | None = None,
):
"""Factory for the ``create_automation`` tool.
``search_space_id`` is injected from the chat session (the model never
``workspace_id`` is injected from the chat session (the model never
has to guess it). ``llm`` is the drafting sub-model we reuse the main
agent's LLM and tag the call so it's identifiable in traces. A fresh
``AsyncSession`` is opened per call to avoid stale sessions on
@ -101,12 +101,12 @@ def create_create_automation_tool(
"""
# Models are chosen per-automation on the approval card (premium/BYOK
# selectors) and validated when persisted by ``AutomationService.create``
# — so there's no fail-fast search-space eligibility gate here. The
# search space's current chat/role model selection no longer constrains
# — so there's no fail-fast workspace eligibility gate here. The
# workspace's current chat/role model selection no longer constrains
# whether an automation can be drafted or saved.
# --- 1. Draft via sub-LLM ---
prompt = build_draft_prompt(search_space_id=search_space_id, intent=intent)
prompt = build_draft_prompt(workspace_id=workspace_id, intent=intent)
try:
response = await llm.ainvoke(
[HumanMessage(content=prompt)],
@ -125,8 +125,8 @@ def create_create_automation_tool(
"raw": raw_text,
}
# search_space_id is injected here so the sub-LLM never has to guess.
draft["search_space_id"] = search_space_id
# workspace_id is injected here so the sub-LLM never has to guess.
draft["workspace_id"] = workspace_id
try:
validated_draft = AutomationCreate.model_validate(draft)
except ValidationError as exc:
@ -139,14 +139,14 @@ def create_create_automation_tool(
# --- 2. HITL approval card ---
try:
card_params = validated_draft.model_dump(mode="json", by_alias=True)
# search_space_id is session-scoped, not user-editable.
card_params.pop("search_space_id", None)
# workspace_id is session-scoped, not user-editable.
card_params.pop("workspace_id", None)
result = request_approval(
action_type="automation_create",
tool_name="create_automation",
params=card_params,
context={"search_space_id": search_space_id},
context={"workspace_id": workspace_id},
tool_call_id=runtime.tool_call_id,
)
@ -157,7 +157,7 @@ def create_create_automation_tool(
}
# --- 3. Persist (re-validate in case the user edited) ---
final_payload = {**result.params, "search_space_id": search_space_id}
final_payload = {**result.params, "workspace_id": workspace_id}
try:
final_validated = AutomationCreate.model_validate(final_payload)
except ValidationError as exc:

View file

@ -34,7 +34,7 @@ into a SINGLE JSON object matching the AutomationCreate schema. Output
ONLY that JSON object no prose, no markdown fence, no commentary.
Current UTC time (for cron context): {now}
Target search_space_id: {search_space_id}
Target workspace_id: {workspace_id}
"""
@ -165,12 +165,12 @@ User intent:
"""
def build_draft_prompt(*, search_space_id: int, intent: str) -> str:
def build_draft_prompt(*, workspace_id: int, intent: str) -> str:
"""Render the drafting sub-LLM system prompt for the given intent."""
return (
_HEADER.format(
now=datetime.now(UTC).isoformat(timespec="seconds"),
search_space_id=search_space_id,
workspace_id=workspace_id,
)
+ _SCHEMA
+ _FEW_SHOTS

View file

@ -37,7 +37,7 @@ def _build_scrape_webpage_tool(deps: dict[str, Any]) -> BaseTool:
def _build_web_search_tool(deps: dict[str, Any]) -> BaseTool:
return create_web_search_tool(
search_space_id=deps.get("search_space_id"),
workspace_id=deps.get("workspace_id"),
available_connectors=deps.get("available_connectors"),
)
@ -49,7 +49,7 @@ def _build_create_automation_tool(deps: dict[str, Any]) -> BaseTool:
from .automation import create_create_automation_tool
return create_create_automation_tool(
search_space_id=deps["search_space_id"],
workspace_id=deps["workspace_id"],
user_id=deps["user_id"],
auth_context=deps.get("auth_context"),
llm=deps["llm"],
@ -59,7 +59,7 @@ def _build_create_automation_tool(deps: dict[str, Any]) -> BaseTool:
def _build_update_memory_tool(deps: dict[str, Any]) -> BaseTool:
if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE:
return create_update_team_memory_tool(
search_space_id=deps["search_space_id"],
workspace_id=deps["workspace_id"],
db_session=deps["db_session"],
llm=deps.get("llm"),
)
@ -80,11 +80,11 @@ _MAIN_AGENT_TOOL_FACTORIES: dict[
"web_search": (_build_web_search_tool, ()),
"create_automation": (
_build_create_automation_tool,
("search_space_id", "user_id", "llm"),
("workspace_id", "user_id", "llm"),
),
"update_memory": (
_build_update_memory_tool,
("user_id", "search_space_id", "db_session", "thread_visibility", "llm"),
("user_id", "workspace_id", "db_session", "thread_visibility", "llm"),
),
}

View file

@ -53,7 +53,7 @@ def create_update_memory_tool(
def create_update_team_memory_tool(
search_space_id: int,
workspace_id: int,
db_session: AsyncSession,
llm: Any | None = None,
):
@ -62,7 +62,7 @@ def create_update_team_memory_tool(
@tool
async def update_memory(updated_memory: str) -> dict[str, Any]:
"""Update the team's shared memory document for this search space.
"""Update the team's shared memory document for this workspace.
The current team memory is shown in <team_memory>. Pass the FULL updated
markdown document, not a diff.
@ -71,7 +71,7 @@ def create_update_team_memory_tool(
async with async_session_maker() as db_session:
result = await save_memory(
scope=MemoryScope.TEAM,
target_id=search_space_id,
target_id=workspace_id,
content=updated_memory,
session=db_session,
llm=llm,

View file

@ -120,8 +120,8 @@ class KBPostgresBackend(BackendProtocol):
_IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp"})
def __init__(self, search_space_id: int, runtime: ToolRuntime) -> None:
self.search_space_id = search_space_id
def __init__(self, workspace_id: int, runtime: ToolRuntime) -> None:
self.workspace_id = workspace_id
self.runtime = runtime
@property
@ -405,7 +405,7 @@ class KBPostgresBackend(BackendProtocol):
if not normalized_path.startswith(DOCUMENTS_ROOT):
return [], set()
index = await build_path_index(session, self.search_space_id)
index = await build_path_index(session, self.workspace_id)
target_folder_id: int | None = None
if normalized_path != DOCUMENTS_ROOT:
target_path = normalized_path
@ -418,7 +418,7 @@ class KBPostgresBackend(BackendProtocol):
result = await session.execute(
select(Document.id, Document.title, Document.folder_id, Document.updated_at)
.where(Document.search_space_id == self.search_space_id)
.where(Document.workspace_id == self.workspace_id)
.where(
Document.folder_id == target_folder_id
if target_folder_id is not None
@ -519,7 +519,7 @@ class KBPostgresBackend(BackendProtocol):
async with shielded_async_session() as session:
document_row = await virtual_path_to_doc(
session,
search_space_id=self.search_space_id,
workspace_id=self.workspace_id,
virtual_path=path,
)
if document_row is None:
@ -667,10 +667,10 @@ class KBPostgresBackend(BackendProtocol):
if normalized.startswith(DOCUMENTS_ROOT) or normalized == "/":
try:
async with shielded_async_session() as session:
index = await build_path_index(session, self.search_space_id)
index = await build_path_index(session, self.workspace_id)
rows = await session.execute(
select(Document.id, Document.title, Document.folder_id).where(
Document.search_space_id == self.search_space_id
Document.workspace_id == self.workspace_id
)
)
for row in rows.all():
@ -747,11 +747,11 @@ class KBPostgresBackend(BackendProtocol):
if normalized.startswith(DOCUMENTS_ROOT) or normalized == "/":
try:
async with shielded_async_session() as session:
index = await build_path_index(session, self.search_space_id)
index = await build_path_index(session, self.workspace_id)
sub = (
select(Chunk.document_id, Chunk.id, Chunk.content)
.join(Document, Document.id == Chunk.document_id)
.where(Document.search_space_id == self.search_space_id)
.where(Document.workspace_id == self.workspace_id)
.where(Chunk.content.ilike(f"%{pattern}%"))
.order_by(Chunk.document_id, Chunk.position, Chunk.id)
)
@ -849,14 +849,14 @@ class KBPostgresBackend(BackendProtocol):
try:
async with shielded_async_session() as session:
index = await build_path_index(session, self.search_space_id)
index = await build_path_index(session, self.workspace_id)
doc_rows_raw = await session.execute(
select(
Document.id,
Document.title,
Document.folder_id,
Document.updated_at,
).where(Document.search_space_id == self.search_space_id)
).where(Document.workspace_id == self.workspace_id)
)
doc_rows = list(doc_rows_raw.all())
except Exception as exc: # pragma: no cover

View file

@ -31,14 +31,14 @@ def _cached_multi_root_backend(
def build_backend_resolver(
selection: FilesystemSelection,
*,
search_space_id: int | None = None,
workspace_id: int | None = None,
) -> Callable[[ToolRuntime], BackendProtocol]:
"""Create deepagents backend resolver for the selected filesystem mode.
In cloud mode the resolver returns a fresh :class:`KBPostgresBackend`
bound to the current ``runtime`` so the backend can read staging state
(``staged_dirs``, ``pending_moves``, ``files`` cache, ``kb_anon_doc``)
for each tool call. When no ``search_space_id``
for each tool call. When no ``workspace_id``
is provided, the resolver falls back to :class:`StateBackend` (used by
sub-agents and tests that don't need DB-backed reads).
@ -55,10 +55,10 @@ def build_backend_resolver(
return _resolve_local
if search_space_id is not None:
if workspace_id is not None:
def _resolve_kb(runtime: ToolRuntime) -> BackendProtocol:
return KBPostgresBackend(search_space_id, runtime)
return KBPostgresBackend(workspace_id, runtime)
return _resolve_kb

View file

@ -13,7 +13,7 @@ def build_filesystem_mw(
*,
backend_resolver: Any,
filesystem_mode: FilesystemMode,
search_space_id: int,
workspace_id: int,
user_id: str | None,
thread_id: int | None,
read_only: bool = False,
@ -21,7 +21,7 @@ def build_filesystem_mw(
return SurfSenseFilesystemMiddleware(
backend=backend_resolver,
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
workspace_id=workspace_id,
created_by_id=user_id,
thread_id=thread_id,
read_only=read_only,

View file

@ -49,14 +49,14 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
*,
backend: Any = None,
filesystem_mode: FilesystemMode = FilesystemMode.CLOUD,
search_space_id: int | None = None,
workspace_id: int | None = None,
created_by_id: str | None = None,
thread_id: int | str | None = None,
tool_token_limit_before_evict: int | None = 20000,
read_only: bool = False,
) -> None:
self._filesystem_mode = filesystem_mode
self._search_space_id = search_space_id
self._workspace_id = workspace_id
self._created_by_id = created_by_id
self._thread_id = thread_id
self._read_only = read_only

View file

@ -31,7 +31,7 @@ _SURFACE = "chunks"
async def search_chunks(
db_session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
query: str,
scope: SearchScope,
top_k: int,
@ -44,14 +44,14 @@ async def search_chunks(
"""
started = time.perf_counter()
with otel.kb_search_span(
search_space_id=search_space_id,
workspace_id=workspace_id,
query_chars=len(query),
extra={"search.surface": _SURFACE, "search.mode": "hybrid"},
) as span:
try:
documents = await _search(
db_session,
search_space_id=search_space_id,
workspace_id=workspace_id,
query=query,
scope=scope,
top_k=top_k,
@ -60,14 +60,14 @@ async def search_chunks(
finally:
elapsed_ms = (time.perf_counter() - started) * 1000
metrics.record_kb_search_duration(
elapsed_ms, search_space_id=search_space_id, surface=_SURFACE
elapsed_ms, workspace_id=workspace_id, surface=_SURFACE
)
span.set_attribute("result.count", len(documents))
get_perf_logger().info(
"[chunk_search] hybrid in %.3fs docs=%d space=%d",
elapsed_ms / 1000,
len(documents),
search_space_id,
workspace_id,
)
return documents
@ -75,7 +75,7 @@ async def search_chunks(
async def _search(
db_session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
query: str,
scope: SearchScope,
top_k: int,
@ -91,7 +91,7 @@ async def _search(
config.embedding_model_instance.embed, query
)
conditions = _base_conditions(search_space_id, scope, document_types)
conditions = _base_conditions(workspace_id, scope, document_types)
rows = await _fused_chunks(
db_session,
query=query,
@ -116,13 +116,13 @@ def _resolve_document_types(
def _base_conditions(
search_space_id: int,
workspace_id: int,
scope: SearchScope,
document_types: list[DocumentType] | None,
) -> list:
"""Filters shared by both search legs."""
conditions = [
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
func.coalesce(Document.status["state"].astext, "ready") != "deleting",
]
if document_types:

View file

@ -29,7 +29,7 @@ _DEFAULT_TOP_K = 10
async def search_knowledge_base_context(
db_session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
query: str,
registry: CitationRegistry,
scope: SearchScope | None = None,
@ -42,7 +42,7 @@ async def search_knowledge_base_context(
"""
hits = await search_chunks(
db_session,
search_space_id=search_space_id,
workspace_id=workspace_id,
query=query,
scope=scope or SearchScope(),
top_k=top_k,

View file

@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
# artifact in the user's own workspace with no external visibility (drafts
# aren't sent; new files aren't shared). They still call ``request_approval``,
# which returns ``decision_type="auto_approved"`` without firing an interrupt.
# Per-search-space ``agent_permission_rules`` can re-enable prompting.
# Per-workspace ``agent_permission_rules`` can re-enable prompting.
DEFAULT_AUTO_APPROVED_TOOLS: frozenset[str] = frozenset(
{
"create_gmail_draft",

View file

@ -99,11 +99,11 @@ async def write_cached_tools(
def refresh_mcp_tools_cache_for_connector(
connector_id: int,
search_space_id: int,
workspace_id: int,
) -> None:
"""Maintain the MCP tool cache after a single-connector lifecycle event.
Synchronously evicts the in-process LRU for the connector's search space
Synchronously evicts the in-process LRU for the connector's workspace
(LRU keys are per-space, so eviction cannot be scoped finer), then schedules
a background live discovery for this connector alone so its persisted
``cached_tools`` row is refreshed before the next user query.
@ -116,11 +116,11 @@ def refresh_mcp_tools_cache_for_connector(
invalidate_mcp_tools_cache,
)
invalidate_mcp_tools_cache(search_space_id)
invalidate_mcp_tools_cache(workspace_id)
except Exception:
logger.debug(
"MCP in-process cache eviction skipped for space %d",
search_space_id,
workspace_id,
exc_info=True,
)

View file

@ -56,7 +56,7 @@ _MCP_CACHE_MAX_SIZE = 50
_MCP_DISCOVERY_TIMEOUT_SECONDS = 30
_TOOL_CALL_MAX_RETRIES = 3
_TOOL_CALL_RETRY_DELAY = 1.5 # seconds, doubles per attempt
# Keyed by ``(search_space_id, bypass_internal_hitl)`` so single-agent and
# Keyed by ``(workspace_id, bypass_internal_hitl)`` so single-agent and
# multi-agent paths cannot share tool closures with different HITL wiring.
_MCPCacheKey = tuple[int, bool]
_mcp_tools_cache: dict[_MCPCacheKey, tuple[float, list[StructuredTool]]] = {}
@ -814,7 +814,7 @@ async def _refresh_connector_token(
await session.commit()
await session.refresh(connector)
invalidate_mcp_tools_cache(connector.search_space_id)
invalidate_mcp_tools_cache(connector.workspace_id)
return new_access
@ -990,7 +990,7 @@ async def _mark_connector_auth_expired(connector_id: int) -> None:
"Marked MCP connector %s as auth_expired after unrecoverable 401",
connector_id,
)
invalidate_mcp_tools_cache(connector.search_space_id)
invalidate_mcp_tools_cache(connector.workspace_id)
except Exception:
logger.warning(
@ -1000,10 +1000,10 @@ async def _mark_connector_auth_expired(connector_id: int) -> None:
)
def invalidate_mcp_tools_cache(search_space_id: int | None = None) -> None:
def invalidate_mcp_tools_cache(workspace_id: int | None = None) -> None:
"""Invalidate cached MCP tools (both ``bypass_internal_hitl`` variants together)."""
if search_space_id is not None:
for key in [k for k in _mcp_tools_cache if k[0] == search_space_id]:
if workspace_id is not None:
for key in [k for k in _mcp_tools_cache if k[0] == workspace_id]:
_mcp_tools_cache.pop(key, None)
else:
_mcp_tools_cache.clear()
@ -1099,27 +1099,27 @@ async def discover_single_mcp_connector(connector_id: int) -> None:
async def load_mcp_tools(
session: AsyncSession,
search_space_id: int,
workspace_id: int,
*,
bypass_internal_hitl: bool = False,
) -> list[StructuredTool]:
"""Load all MCP tools from the user's active MCP server connectors.
Results are cached per ``(search_space_id, bypass_internal_hitl)`` for up
Results are cached per ``(workspace_id, bypass_internal_hitl)`` for up
to 5 minutes; bypass is keyed because each variant builds a different tool
closure (with vs. without the in-wrapper ``request_approval`` gate).
"""
_evict_expired_mcp_cache()
now = time.monotonic()
cache_key: _MCPCacheKey = (search_space_id, bypass_internal_hitl)
cache_key: _MCPCacheKey = (workspace_id, bypass_internal_hitl)
cached = _mcp_tools_cache.get(cache_key)
if cached is not None:
cached_at, cached_tools = cached
if now - cached_at < _MCP_CACHE_TTL_SECONDS:
logger.info(
"Using cached MCP tools for search space %s (%d tools, age=%.0fs, bypass_hitl=%s)",
search_space_id,
"Using cached MCP tools for workspace %s (%d tools, age=%.0fs, bypass_hitl=%s)",
workspace_id,
len(cached_tools),
now - cached_at,
bypass_internal_hitl,
@ -1132,7 +1132,7 @@ async def load_mcp_tools(
# Cast JSON -> JSONB so we can use has_key to filter by the presence of "server_config".
result = await session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
cast(SearchSourceConnector.config, JSONB).has_key("server_config"),
),
)
@ -1322,9 +1322,9 @@ async def load_mcp_tools(
del _mcp_tools_cache[oldest_key]
logger.info(
"Loaded %d MCP tools for search space %d (bypass_hitl=%s)",
"Loaded %d MCP tools for workspace %d (bypass_hitl=%s)",
len(tools),
search_space_id,
workspace_id,
bypass_internal_hitl,
)
return tools

View file

@ -1,4 +1,4 @@
"""Image generation via litellm; resolves model config from the search space and returns UI-ready payloads."""
"""Image generation via litellm; resolves model config from the workspace and returns UI-ready payloads."""
import hashlib
import logging
@ -18,7 +18,7 @@ from app.config import config
from app.db import (
ImageGeneration,
Model,
SearchSpace,
Workspace,
shielded_async_session,
)
from app.services.auto_model_pin_service import (
@ -48,14 +48,14 @@ def _get_global_connection(connection_id: int) -> dict | None:
def create_generate_image_tool(
search_space_id: int,
workspace_id: int,
db_session: AsyncSession,
image_gen_model_id_override: int | None = None,
):
"""Create ``generate_image`` with bound search space; DB work uses a per-call session.
"""Create ``generate_image`` with bound workspace; DB work uses a per-call session.
``image_gen_model_id_override``: when set (automations running on a
captured model), use this model id instead of reading the search space's
captured model), use this model id instead of reading the workspace's
live ``image_gen_model_id``.
"""
del db_session # tool uses a fresh per-call session instead
@ -102,22 +102,22 @@ def create_generate_image_tool(
# autoflushes from a concurrent writer poison this tool too.
async with shielded_async_session() as session:
result = await session.execute(
select(SearchSpace).filter(SearchSpace.id == search_space_id)
select(Workspace).filter(Workspace.id == workspace_id)
)
search_space = result.scalars().first()
if not search_space:
workspace = result.scalars().first()
if not workspace:
return _failed(
{"error": "Search space not found"},
error="Search space not found",
{"error": "Workspace not found"},
error="Workspace not found",
)
if image_gen_model_id_override is not None:
# Automation run: use the captured image model, insulated from
# later search-space changes. No search-space read needed.
# later workspace changes. No workspace read needed.
config_id = image_gen_model_id_override or IMAGE_GEN_AUTO_MODE_ID
else:
config_id = (
search_space.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID
workspace.image_gen_model_id or IMAGE_GEN_AUTO_MODE_ID
)
# size/quality/style are intentionally omitted: valid values
@ -129,8 +129,8 @@ def create_generate_image_tool(
if is_image_gen_auto_mode(config_id):
candidates = await auto_model_candidates(
session,
search_space_id=search_space_id,
user_id=search_space.user_id,
workspace_id=workspace_id,
user_id=workspace.user_id,
capability="image_gen",
)
if not candidates:
@ -140,7 +140,7 @@ def create_generate_image_tool(
)
return _failed({"error": err}, error=err)
config_id = int(
choose_auto_model_candidate(candidates, search_space_id)["id"]
choose_auto_model_candidate(candidates, workspace_id)["id"]
)
provider_base_url: str | None = None
@ -186,14 +186,14 @@ def create_generate_image_tool(
return _failed({"error": err}, error=err)
conn = db_model.connection
if (
conn.search_space_id is not None
and conn.search_space_id != search_space_id
conn.workspace_id is not None
and conn.workspace_id != workspace_id
):
err = f"Image generation model {config_id} not found"
return _failed({"error": err}, error=err)
if (
conn.user_id is not None
and conn.user_id != search_space.user_id
and conn.user_id != workspace.user_id
):
err = f"Image generation model {config_id} not found"
return _failed({"error": err}, error=err)
@ -225,7 +225,7 @@ def create_generate_image_tool(
n=n,
image_gen_model_id=config_id,
response_data=response_dict,
search_space_id=search_space_id,
workspace_id=workspace_id,
access_token=access_token,
)
session.add(db_image_gen)

View file

@ -28,28 +28,28 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
return [
create_generate_podcast_tool(
search_space_id=d["search_space_id"],
workspace_id=d["workspace_id"],
db_session=d["db_session"],
thread_id=d["thread_id"],
),
create_generate_video_presentation_tool(
search_space_id=d["search_space_id"],
workspace_id=d["workspace_id"],
db_session=d["db_session"],
thread_id=d["thread_id"],
),
create_generate_report_tool(
search_space_id=d["search_space_id"],
workspace_id=d["workspace_id"],
thread_id=d["thread_id"],
connector_service=d.get("connector_service"),
available_connectors=d.get("available_connectors"),
available_document_types=d.get("available_document_types"),
),
create_generate_resume_tool(
search_space_id=d["search_space_id"],
workspace_id=d["workspace_id"],
thread_id=d["thread_id"],
),
create_generate_image_tool(
search_space_id=d["search_space_id"],
workspace_id=d["workspace_id"],
db_session=d["db_session"],
image_gen_model_id_override=d.get("image_gen_model_id_override"),
),

View file

@ -28,11 +28,11 @@ logger = logging.getLogger(__name__)
def create_generate_podcast_tool(
search_space_id: int,
workspace_id: int,
db_session: AsyncSession,
thread_id: int | None = None,
):
"""Create ``generate_podcast`` with bound search space and thread; DB writes use a tool-local session."""
"""Create ``generate_podcast`` with bound workspace and thread; DB writes use a tool-local session."""
del db_session # writes use a fresh tool-local session, see below
@tool
@ -77,13 +77,13 @@ def create_generate_podcast_tool(
service = PodcastService(session)
podcast = await service.create(
title=podcast_title,
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_id=resolve_root_thread_id(runtime, thread_id),
)
podcast.source_content = source_content
spec = await propose_brief(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
focus=user_prompt,
)
await service.attach_brief(podcast, spec)

View file

@ -31,7 +31,7 @@ def _report_search_types(
"""Build the document-type scope for the shared KB search.
``None`` means "search every indexed type"; a tuple narrows the scope to the
connectors/document types the search space actually has.
connectors/document types the workspace actually has.
"""
types: set[str] = set()
if available_document_types:
@ -561,7 +561,7 @@ async def _revise_with_sections(
def create_generate_report_tool(
search_space_id: int,
workspace_id: int,
thread_id: int | None = None,
connector_service: ConnectorService | None = None,
available_connectors: list[str] | None = None,
@ -728,7 +728,7 @@ def create_generate_report_tool(
"error_message": error_msg,
},
report_style=report_style,
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_id=resolve_root_thread_id(runtime, thread_id),
report_group_id=report_group_id,
)
@ -769,7 +769,7 @@ def create_generate_report_tool(
"creating standalone report"
)
llm = await get_agent_llm(read_session, search_space_id)
llm = await get_agent_llm(read_session, workspace_id)
if not llm:
error_msg = (
@ -846,7 +846,7 @@ def create_generate_report_tool(
async with shielded_async_session() as kb_session:
return await search_chunks(
kb_session,
search_space_id=search_space_id,
workspace_id=workspace_id,
query=q,
scope=scope,
top_k=10,
@ -1044,7 +1044,7 @@ def create_generate_report_tool(
content=report_content,
report_metadata=metadata,
report_style=report_style,
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_id=resolve_root_thread_id(runtime, thread_id),
report_group_id=report_group_id,
)

View file

@ -421,7 +421,7 @@ def _validate_max_pages(max_pages: int) -> int:
def create_generate_resume_tool(
search_space_id: int,
workspace_id: int,
thread_id: int | None = None,
):
"""
@ -531,7 +531,7 @@ def create_generate_resume_tool(
"error_message": error_msg,
},
report_style="resume",
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_id=resolve_root_thread_id(runtime, thread_id),
report_group_id=report_group_id,
)
@ -581,7 +581,7 @@ def create_generate_resume_tool(
f"(group {report_group_id})"
)
llm = await get_agent_llm(read_session, search_space_id)
llm = await get_agent_llm(read_session, workspace_id)
if not llm:
error_msg = (
@ -819,7 +819,7 @@ def create_generate_resume_tool(
content_type="typst",
report_metadata=metadata,
report_style="resume",
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_id=resolve_root_thread_id(runtime, thread_id),
report_group_id=report_group_id,
)

View file

@ -31,11 +31,11 @@ logger = logging.getLogger(__name__)
def create_generate_video_presentation_tool(
search_space_id: int,
workspace_id: int,
db_session: AsyncSession,
thread_id: int | None = None,
):
"""Create ``generate_video_presentation`` with bound search space and thread; writes use a tool-local session."""
"""Create ``generate_video_presentation`` with bound workspace and thread; writes use a tool-local session."""
del db_session # writes use a fresh tool-local session, see below
@tool
@ -60,7 +60,7 @@ def create_generate_video_presentation_tool(
video_pres = VideoPresentation(
title=video_title,
status=VideoPresentationStatus.PENDING,
search_space_id=search_space_id,
workspace_id=workspace_id,
thread_id=resolve_root_thread_id(runtime, thread_id),
)
session.add(video_pres)
@ -75,7 +75,7 @@ def create_generate_video_presentation_tool(
task = generate_video_presentation_task.delay(
video_presentation_id=video_pres_id,
source_content=source_content,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_prompt=user_prompt,
)

View file

@ -36,7 +36,7 @@ _KB_READONLY_RULESET = Ruleset(origin=READONLY_NAME, rules=[])
def _build_search_knowledge_base_tool(dependencies: dict[str, Any]) -> BaseTool:
"""Construct the hybrid-RAG ``search_knowledge_base`` tool from shared deps."""
return create_search_knowledge_base_tool(
search_space_id=dependencies["search_space_id"],
workspace_id=dependencies["workspace_id"],
available_connectors=dependencies.get("available_connectors"),
available_document_types=dependencies.get("available_document_types"),
)

View file

@ -115,7 +115,7 @@ def build_kb_middleware(
fs_mw = build_filesystem_mw(
backend_resolver=dependencies["backend_resolver"],
filesystem_mode=filesystem_mode,
search_space_id=dependencies["search_space_id"],
workspace_id=dependencies["workspace_id"],
user_id=dependencies.get("user_id"),
thread_id=dependencies.get("thread_id"),
read_only=read_only,

View file

@ -89,7 +89,7 @@ def _resolve_mention_pins(
async def _build_search_scope(
session: AsyncSession,
*,
search_space_id: int,
workspace_id: int,
document_types: tuple[str, ...] | None,
runtime: ToolRuntime[None, SurfSenseFilesystemState],
) -> SearchScope:
@ -97,7 +97,7 @@ async def _build_search_scope(
mentioned_document_ids, mentioned_folder_ids = _resolve_mention_pins(runtime)
document_ids = await referenced_document_ids(
session,
search_space_id=search_space_id,
workspace_id=workspace_id,
document_ids=mentioned_document_ids,
folder_ids=mentioned_folder_ids,
)
@ -109,13 +109,13 @@ async def _build_search_scope(
def create_search_knowledge_base_tool(
*,
search_space_id: int,
workspace_id: int,
available_connectors: list[str] | None = None,
available_document_types: list[str] | None = None,
) -> BaseTool:
"""Factory for the on-demand ``search_knowledge_base`` tool."""
_space_id = search_space_id
_space_id = workspace_id
_document_types = _search_types(available_connectors, available_document_types)
async def _impl(
@ -140,13 +140,13 @@ def create_search_knowledge_base_tool(
async with shielded_async_session() as session:
scope = await _build_search_scope(
session,
search_space_id=_space_id,
workspace_id=_space_id,
document_types=_document_types,
runtime=runtime,
)
hits = await search_chunks(
session,
search_space_id=_space_id,
workspace_id=_space_id,
query=cleaned_query,
scope=scope,
top_k=clamped_top_k,

View file

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

View file

@ -23,7 +23,7 @@ def load_tools(
if d.get("thread_visibility") == ChatVisibility.SEARCH_SPACE:
return [
create_update_team_memory_tool(
search_space_id=d["search_space_id"],
workspace_id=d["workspace_id"],
db_session=d["db_session"],
llm=d.get("llm"),
)

View file

@ -51,13 +51,13 @@ def create_update_memory_tool(
def create_update_team_memory_tool(
search_space_id: int,
workspace_id: int,
db_session: AsyncSession,
llm: Any | None = None,
):
@tool
async def update_memory(updated_memory: str) -> dict[str, Any]:
"""Update the team's shared memory document for this search space.
"""Update the team's shared memory document for this workspace.
The current team memory is shown in <team_memory>. Pass the FULL updated
markdown document, not a diff.
@ -65,7 +65,7 @@ def create_update_team_memory_tool(
try:
result = await save_memory(
scope=MemoryScope.TEAM,
target_id=search_space_id,
target_id=workspace_id,
content=updated_memory,
session=db_session,
llm=llm,

View file

@ -22,7 +22,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
return [
create_web_search_tool(
search_space_id=d.get("search_space_id"),
workspace_id=d.get("workspace_id"),
available_connectors=d.get("available_connectors"),
),
create_scrape_webpage_tool(firecrawl_api_key=d.get("firecrawl_api_key")),

View file

@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
def create_create_calendar_event_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -62,7 +62,7 @@ def create_create_calendar_event_tool(
f"create_calendar_event called: summary='{summary}', start='{start_datetime}', end='{end_datetime}'"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Google Calendar tool not properly configured. Please contact support.",
@ -71,7 +71,7 @@ def create_create_calendar_event_tool(
try:
metadata_service = GoogleCalendarToolMetadataService(db_session)
context = await metadata_service.get_creation_context(
search_space_id, user_id
workspace_id, user_id
)
if "error" in context:
@ -138,7 +138,7 @@ def create_create_calendar_event_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_calendar_types),
)
@ -153,7 +153,7 @@ def create_create_calendar_event_tool(
else:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_calendar_types),
)
@ -318,7 +318,7 @@ def create_create_calendar_event_tool(
html_link=created.get("htmlLink"),
description=final_description,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
def create_delete_calendar_event_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -56,7 +56,7 @@ def create_delete_calendar_event_tool(
f"delete_calendar_event called: event_ref='{event_title_or_id}', delete_from_kb={delete_from_kb}"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Google Calendar tool not properly configured. Please contact support.",
@ -65,7 +65,7 @@ def create_delete_calendar_event_tool(
try:
metadata_service = GoogleCalendarToolMetadataService(db_session)
context = await metadata_service.get_deletion_context(
search_space_id, user_id, event_title_or_id
workspace_id, user_id, event_title_or_id
)
if "error" in context:
@ -143,7 +143,7 @@ def create_delete_calendar_event_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_calendar_types),
)

View file

@ -28,7 +28,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -28,7 +28,7 @@ def _to_calendar_boundary(value: str, *, is_end: bool) -> str:
def create_search_calendar_events_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -48,7 +48,7 @@ def create_search_calendar_events_tool(
Dictionary with status and a list of events including
event_id, summary, start, end, location, attendees.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Calendar tool not properly configured.",
@ -59,7 +59,7 @@ def create_search_calendar_events_tool(
try:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_CALENDAR_TYPES),
)

View file

@ -32,7 +32,7 @@ def _build_time_body(value: str, context: dict[str, Any] | Any) -> dict[str, str
def create_update_calendar_event_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -76,7 +76,7 @@ def create_update_calendar_event_tool(
"""
logger.info(f"update_calendar_event called: event_ref='{event_title_or_id}'")
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Google Calendar tool not properly configured. Please contact support.",
@ -85,7 +85,7 @@ def create_update_calendar_event_tool(
try:
metadata_service = GoogleCalendarToolMetadataService(db_session)
context = await metadata_service.get_update_context(
search_space_id, user_id, event_title_or_id
workspace_id, user_id, event_title_or_id
)
if "error" in context:
@ -176,7 +176,7 @@ def create_update_calendar_event_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_calendar_types),
)
@ -363,7 +363,7 @@ def create_update_calendar_event_tool(
document_id=document_id,
event_id=final_event_id,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_create_confluence_page_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
connector_id: int | None = None,
):
@ -44,7 +44,7 @@ def create_create_confluence_page_tool(
"""
logger.info(f"create_confluence_page called: title='{title}'")
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Confluence tool not properly configured.",
@ -53,7 +53,7 @@ def create_create_confluence_page_tool(
try:
metadata_service = ConfluenceToolMetadataService(db_session)
context = await metadata_service.get_creation_context(
search_space_id, user_id
workspace_id, user_id
)
if "error" in context:
@ -103,7 +103,7 @@ def create_create_confluence_page_tool(
if actual_connector_id is None:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
@ -120,7 +120,7 @@ def create_create_confluence_page_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == actual_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
@ -181,7 +181,7 @@ def create_create_confluence_page_tool(
space_id=final_space_id,
body_content=final_content,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_delete_confluence_page_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
connector_id: int | None = None,
):
@ -45,7 +45,7 @@ def create_delete_confluence_page_tool(
f"delete_confluence_page called: page_title_or_id='{page_title_or_id}'"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Confluence tool not properly configured.",
@ -54,7 +54,7 @@ def create_delete_confluence_page_tool(
try:
metadata_service = ConfluenceToolMetadataService(db_session)
context = await metadata_service.get_deletion_context(
search_space_id, user_id, page_title_or_id
workspace_id, user_id, page_title_or_id
)
if "error" in context:
@ -112,7 +112,7 @@ def create_delete_confluence_page_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,

View file

@ -26,7 +26,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
"connector_id": d.get("connector_id"),
}

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_update_confluence_page_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
connector_id: int | None = None,
):
@ -47,7 +47,7 @@ def create_update_confluence_page_tool(
f"update_confluence_page called: page_title_or_id='{page_title_or_id}'"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Confluence tool not properly configured.",
@ -56,7 +56,7 @@ def create_update_confluence_page_tool(
try:
metadata_service = ConfluenceToolMetadataService(db_session)
context = await metadata_service.get_update_context(
search_space_id, user_id, page_title_or_id
workspace_id, user_id, page_title_or_id
)
if "error" in context:
@ -124,7 +124,7 @@ def create_update_confluence_page_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
@ -183,7 +183,7 @@ def create_update_confluence_page_tool(
document_id=final_document_id,
page_id=final_page_id,
user_id=user_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
)
if kb_result["status"] == "success":
kb_message_suffix = (

View file

@ -12,12 +12,12 @@ DISCORD_API = "https://discord.com/api/v10"
async def get_discord_connector(
db_session: AsyncSession,
search_space_id: int,
workspace_id: int,
user_id: str,
) -> SearchSourceConnector | None:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.DISCORD_CONNECTOR,

View file

@ -26,7 +26,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def create_list_discord_channels_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -22,7 +22,7 @@ def create_list_discord_channels_tool(
Returns:
Dictionary with status and a list of channels (id, name).
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Discord tool not properly configured.",
@ -30,7 +30,7 @@ def create_list_discord_channels_tool(
try:
connector = await get_discord_connector(
db_session, search_space_id, user_id
db_session, workspace_id, user_id
)
if not connector:
return {"status": "error", "message": "No Discord connector found."}

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def create_read_discord_messages_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -30,7 +30,7 @@ def create_read_discord_messages_tool(
Dictionary with status and a list of messages including
id, author, content, timestamp.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Discord tool not properly configured.",
@ -40,7 +40,7 @@ def create_read_discord_messages_tool(
try:
connector = await get_discord_connector(
db_session, search_space_id, user_id
db_session, workspace_id, user_id
)
if not connector:
return {"status": "error", "message": "No Discord connector found."}

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_send_discord_message_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -36,7 +36,7 @@ def create_send_discord_message_tool(
IMPORTANT:
- If status is "rejected", the user explicitly declined. Do NOT retry.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Discord tool not properly configured.",
@ -50,7 +50,7 @@ def create_send_discord_message_tool(
try:
connector = await get_discord_connector(
db_session, search_space_id, user_id
db_session, workspace_id, user_id
)
if not connector:
return {"status": "error", "message": "No Discord connector found."}

View file

@ -58,7 +58,7 @@ def _markdown_to_docx(markdown_text: str) -> bytes:
def create_create_dropbox_file_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -84,7 +84,7 @@ def create_create_dropbox_file_tool(
f"create_dropbox_file called: name='{name}', file_type='{file_type}'"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Dropbox tool not properly configured.",
@ -93,7 +93,7 @@ def create_create_dropbox_file_tool(
try:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.DROPBOX_CONNECTOR,
@ -195,7 +195,7 @@ def create_create_dropbox_file_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.DROPBOX_CONNECTOR,
@ -244,7 +244,7 @@ def create_create_dropbox_file_tool(
web_url=web_url,
content=final_content,
connector_id=connector.id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -25,7 +25,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
def create_delete_dropbox_file_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -57,7 +57,7 @@ def create_delete_dropbox_file_tool(
f"delete_dropbox_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Dropbox tool not properly configured.",
@ -72,7 +72,7 @@ def create_delete_dropbox_file_tool(
)
.filter(
and_(
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
Document.document_type == DocumentType.DROPBOX_FILE,
func.lower(Document.title) == func.lower(file_name),
SearchSourceConnector.user_id == user_id,
@ -92,7 +92,7 @@ def create_delete_dropbox_file_tool(
)
.filter(
and_(
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
Document.document_type == DocumentType.DROPBOX_FILE,
func.lower(
cast(
@ -140,7 +140,7 @@ def create_delete_dropbox_file_tool(
select(SearchSourceConnector).filter(
and_(
SearchSourceConnector.id == document.connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.DROPBOX_CONNECTOR,
@ -202,7 +202,7 @@ def create_delete_dropbox_file_tool(
select(SearchSourceConnector).filter(
and_(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.DROPBOX_CONNECTOR,

View file

@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
def create_create_gmail_draft_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -59,7 +59,7 @@ def create_create_gmail_draft_tool(
"""
logger.info(f"create_gmail_draft called: to='{to}', subject='{subject}'")
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Gmail tool not properly configured. Please contact support.",
@ -68,7 +68,7 @@ def create_create_gmail_draft_tool(
try:
metadata_service = GmailToolMetadataService(db_session)
context = await metadata_service.get_creation_context(
search_space_id, user_id
workspace_id, user_id
)
if "error" in context:
@ -127,7 +127,7 @@ def create_create_gmail_draft_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_gmail_types),
)
@ -142,7 +142,7 @@ def create_create_gmail_draft_tool(
else:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_gmail_types),
)
@ -307,7 +307,7 @@ def create_create_gmail_draft_tool(
date_str=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
body_text=final_body,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
draft_id=created.get("id"),
)

View file

@ -29,7 +29,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -17,7 +17,7 @@ _GMAIL_TYPES = [
def create_read_gmail_email_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -32,13 +32,13 @@ def create_read_gmail_email_tool(
Returns:
Dictionary with status and the full email content formatted as markdown.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Gmail tool not properly configured."}
try:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_GMAIL_TYPES),
)

View file

@ -17,7 +17,7 @@ _GMAIL_TYPES = [
def create_search_gmail_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -38,7 +38,7 @@ def create_search_gmail_tool(
Dictionary with status and a list of email summaries including
message_id, subject, from, date, snippet.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Gmail tool not properly configured."}
max_results = min(max_results, 20)
@ -46,7 +46,7 @@ def create_search_gmail_tool(
try:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_GMAIL_TYPES),
)

View file

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
def create_send_gmail_email_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -86,7 +86,7 @@ def create_send_gmail_email_tool(
tool_call_id=runtime.tool_call_id,
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
msg = "Gmail tool not properly configured. Please contact support."
return _emit(
{"status": "error", "message": msg},
@ -97,7 +97,7 @@ def create_send_gmail_email_tool(
try:
metadata_service = GmailToolMetadataService(db_session)
context = await metadata_service.get_creation_context(
search_space_id, user_id
workspace_id, user_id
)
if "error" in context:
@ -168,7 +168,7 @@ def create_send_gmail_email_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_gmail_types),
)
@ -187,7 +187,7 @@ def create_send_gmail_email_tool(
else:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_gmail_types),
)
@ -355,7 +355,7 @@ def create_send_gmail_email_tool(
date_str=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
body_text=final_body,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_trash_gmail_email_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -57,7 +57,7 @@ def create_trash_gmail_email_tool(
f"trash_gmail_email called: email_subject_or_id='{email_subject_or_id}', delete_from_kb={delete_from_kb}"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Gmail tool not properly configured. Please contact support.",
@ -66,7 +66,7 @@ def create_trash_gmail_email_tool(
try:
metadata_service = GmailToolMetadataService(db_session)
context = await metadata_service.get_trash_context(
search_space_id, user_id, email_subject_or_id
workspace_id, user_id, email_subject_or_id
)
if "error" in context:
@ -144,7 +144,7 @@ def create_trash_gmail_email_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_gmail_types),
)

View file

@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
def create_update_gmail_draft_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -78,7 +78,7 @@ def create_update_gmail_draft_tool(
f"update_gmail_draft called: draft_subject_or_id='{draft_subject_or_id}'"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Gmail tool not properly configured. Please contact support.",
@ -87,7 +87,7 @@ def create_update_gmail_draft_tool(
try:
metadata_service = GmailToolMetadataService(db_session)
context = await metadata_service.get_update_context(
search_space_id, user_id, draft_subject_or_id
workspace_id, user_id, draft_subject_or_id
)
if "error" in context:
@ -174,7 +174,7 @@ def create_update_gmail_draft_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_gmail_types),
)

View file

@ -22,7 +22,7 @@ _MIME_MAP: dict[str, str] = {
def create_create_google_drive_file_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -67,7 +67,7 @@ def create_create_google_drive_file_tool(
f"create_google_drive_file called: name='{name}', type='{file_type}'"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Google Drive tool not properly configured. Please contact support.",
@ -82,7 +82,7 @@ def create_create_google_drive_file_tool(
try:
metadata_service = GoogleDriveToolMetadataService(db_session)
context = await metadata_service.get_creation_context(
search_space_id, user_id
workspace_id, user_id
)
if "error" in context:
@ -149,7 +149,7 @@ def create_create_google_drive_file_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_drive_types),
)
@ -164,7 +164,7 @@ def create_create_google_drive_file_tool(
else:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_drive_types),
)
@ -288,7 +288,7 @@ def create_create_google_drive_file_tool(
web_view_link=created.get("webViewLink"),
content=final_content,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -25,7 +25,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_delete_google_drive_file_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -57,7 +57,7 @@ def create_delete_google_drive_file_tool(
f"delete_google_drive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "Google Drive tool not properly configured. Please contact support.",
@ -66,7 +66,7 @@ def create_delete_google_drive_file_tool(
try:
metadata_service = GoogleDriveToolMetadataService(db_session)
context = await metadata_service.get_trash_context(
search_space_id, user_id, file_name
workspace_id, user_id, file_name
)
if "error" in context:
@ -144,7 +144,7 @@ def create_delete_google_drive_file_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type.in_(_drive_types),
)

View file

@ -10,12 +10,12 @@ LUMA_API = "https://public-api.luma.com/v1"
async def get_luma_connector(
db_session: AsyncSession,
search_space_id: int,
workspace_id: int,
user_id: str,
) -> SearchSourceConnector | None:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.LUMA_CONNECTOR,

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_create_luma_event_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -42,11 +42,11 @@ def create_create_luma_event_tool(
IMPORTANT:
- If status is "rejected", the user explicitly declined. Do NOT retry.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Luma tool not properly configured."}
try:
connector = await get_luma_connector(db_session, search_space_id, user_id)
connector = await get_luma_connector(db_session, workspace_id, user_id)
if not connector:
return {"status": "error", "message": "No Luma connector found."}

View file

@ -26,7 +26,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def create_list_luma_events_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -28,13 +28,13 @@ def create_list_luma_events_tool(
Dictionary with status and a list of events including
event_id, name, start_at, end_at, location, url.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Luma tool not properly configured."}
max_results = min(max_results, 50)
try:
connector = await get_luma_connector(db_session, search_space_id, user_id)
connector = await get_luma_connector(db_session, workspace_id, user_id)
if not connector:
return {"status": "error", "message": "No Luma connector found."}

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def create_read_luma_event_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -26,11 +26,11 @@ def create_read_luma_event_tool(
Dictionary with status and full event details including
description, attendees count, meeting URL.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Luma tool not properly configured."}
try:
connector = await get_luma_connector(db_session, search_space_id, user_id)
connector = await get_luma_connector(db_session, workspace_id, user_id)
if not connector:
return {"status": "error", "message": "No Luma connector found."}

View file

@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
def create_create_notion_page_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
connector_id: int | None = None,
):
@ -24,7 +24,7 @@ def create_create_notion_page_tool(
Args:
db_session: Database session for accessing Notion connector
search_space_id: Search space ID to find the Notion connector
workspace_id: Workspace ID to find the Notion connector
user_id: User ID for fetching user-specific context
connector_id: Optional specific connector ID (if known)
@ -69,7 +69,7 @@ def create_create_notion_page_tool(
"""
logger.info(f"create_notion_page called: title='{title}'")
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
logger.error(
"Notion tool not properly configured - missing required parameters"
)
@ -81,7 +81,7 @@ def create_create_notion_page_tool(
try:
metadata_service = NotionToolMetadataService(db_session)
context = await metadata_service.get_creation_context(
search_space_id, user_id
workspace_id, user_id
)
if "error" in context:
@ -144,7 +144,7 @@ def create_create_notion_page_tool(
if actual_connector_id is None:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.NOTION_CONNECTOR,
@ -154,7 +154,7 @@ def create_create_notion_page_tool(
if not connector:
logger.warning(
f"No Notion connector found for search_space_id={search_space_id}"
f"No Notion connector found for workspace_id={workspace_id}"
)
return {
"status": "error",
@ -167,7 +167,7 @@ def create_create_notion_page_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == actual_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.NOTION_CONNECTOR,
@ -177,7 +177,7 @@ def create_create_notion_page_tool(
if not connector:
logger.error(
f"Invalid connector_id={actual_connector_id} for search_space_id={search_space_id}"
f"Invalid connector_id={actual_connector_id} for workspace_id={workspace_id}"
)
return {
"status": "error",
@ -211,7 +211,7 @@ def create_create_notion_page_tool(
page_url=result.get("url"),
content=final_content,
connector_id=actual_connector_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
def create_delete_notion_page_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
connector_id: int | None = None,
):
@ -28,7 +28,7 @@ def create_delete_notion_page_tool(
Args:
db_session: Database session for accessing Notion connector
search_space_id: Search space ID to find the Notion connector
workspace_id: Workspace ID to find the Notion connector
user_id: User ID for finding the correct Notion connector
connector_id: Optional specific connector ID (if known)
@ -91,7 +91,7 @@ def create_delete_notion_page_tool(
tool_call_id=runtime.tool_call_id,
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
logger.error(
"Notion tool not properly configured - missing required parameters"
)
@ -108,7 +108,7 @@ def create_delete_notion_page_tool(
# Get page context (page_id, account, title) from indexed data
metadata_service = NotionToolMetadataService(db_session)
context = await metadata_service.get_delete_context(
search_space_id, user_id, page_title
workspace_id, user_id, page_title
)
if "error" in context:
@ -193,7 +193,7 @@ def create_delete_notion_page_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.NOTION_CONNECTOR,
@ -203,7 +203,7 @@ def create_delete_notion_page_tool(
if not connector:
logger.error(
f"Invalid connector_id={final_connector_id} for search_space_id={search_space_id}"
f"Invalid connector_id={final_connector_id} for workspace_id={workspace_id}"
)
return _emit(
{

View file

@ -26,7 +26,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
def create_update_notion_page_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
connector_id: int | None = None,
):
@ -24,7 +24,7 @@ def create_update_notion_page_tool(
Args:
db_session: Database session for accessing Notion connector
search_space_id: Search space ID to find the Notion connector
workspace_id: Workspace ID to find the Notion connector
user_id: User ID for fetching user-specific context
connector_id: Optional specific connector ID (if known)
@ -73,7 +73,7 @@ def create_update_notion_page_tool(
f"update_notion_page called: page_title='{page_title}', content_length={len(content) if content else 0}"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
logger.error(
"Notion tool not properly configured - missing required parameters"
)
@ -92,7 +92,7 @@ def create_update_notion_page_tool(
try:
metadata_service = NotionToolMetadataService(db_session)
context = await metadata_service.get_update_context(
search_space_id, user_id, page_title
workspace_id, user_id, page_title
)
if "error" in context:
@ -165,7 +165,7 @@ def create_update_notion_page_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.NOTION_CONNECTOR,
@ -175,7 +175,7 @@ def create_update_notion_page_tool(
if not connector:
logger.error(
f"Invalid connector_id={final_connector_id} for search_space_id={search_space_id}"
f"Invalid connector_id={final_connector_id} for workspace_id={workspace_id}"
)
return {
"status": "error",
@ -212,7 +212,7 @@ def create_update_notion_page_tool(
document_id=document_id,
appended_content=final_content,
user_id=user_id,
search_space_id=search_space_id,
workspace_id=workspace_id,
appended_block_ids=result.get("appended_block_ids"),
)

View file

@ -47,7 +47,7 @@ def _markdown_to_docx(markdown_text: str) -> bytes:
def create_create_onedrive_file_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -72,7 +72,7 @@ def create_create_onedrive_file_tool(
"""
logger.info(f"create_onedrive_file called: name='{name}'")
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "OneDrive tool not properly configured.",
@ -81,7 +81,7 @@ def create_create_onedrive_file_tool(
try:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
@ -179,7 +179,7 @@ def create_create_onedrive_file_tool(
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
@ -221,7 +221,7 @@ def create_create_onedrive_file_tool(
web_url=created.get("webUrl"),
content=final_content,
connector_id=connector.id,
search_space_id=search_space_id,
workspace_id=workspace_id,
user_id=user_id,
)
if kb_result["status"] == "success":

View file

@ -25,7 +25,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
def create_delete_onedrive_file_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -58,7 +58,7 @@ def create_delete_onedrive_file_tool(
f"delete_onedrive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}"
)
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {
"status": "error",
"message": "OneDrive tool not properly configured.",
@ -73,7 +73,7 @@ def create_delete_onedrive_file_tool(
)
.filter(
and_(
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
Document.document_type == DocumentType.ONEDRIVE_FILE,
func.lower(Document.title) == func.lower(file_name),
SearchSourceConnector.user_id == user_id,
@ -93,7 +93,7 @@ def create_delete_onedrive_file_tool(
)
.filter(
and_(
Document.search_space_id == search_space_id,
Document.workspace_id == workspace_id,
Document.document_type == DocumentType.ONEDRIVE_FILE,
func.lower(
cast(
@ -140,7 +140,7 @@ def create_delete_onedrive_file_tool(
select(SearchSourceConnector).filter(
and_(
SearchSourceConnector.id == document.connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
@ -202,7 +202,7 @@ def create_delete_onedrive_file_tool(
select(SearchSourceConnector).filter(
and_(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,

View file

@ -10,12 +10,12 @@ GRAPH_API = "https://graph.microsoft.com/v1.0"
async def get_teams_connector(
db_session: AsyncSession,
search_space_id: int,
workspace_id: int,
user_id: str,
) -> SearchSourceConnector | None:
result = await db_session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.TEAMS_CONNECTOR,

View file

@ -26,7 +26,7 @@ def load_tools(
d = {**(dependencies or {}), **kwargs}
common = {
"db_session": d["db_session"],
"search_space_id": d["search_space_id"],
"workspace_id": d["workspace_id"],
"user_id": d["user_id"],
}
return [

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def create_list_teams_channels_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -23,11 +23,11 @@ def create_list_teams_channels_tool(
Dictionary with status and a list of teams, each containing
team_id, team_name, and a list of channels (id, name).
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Teams tool not properly configured."}
try:
connector = await get_teams_connector(db_session, search_space_id, user_id)
connector = await get_teams_connector(db_session, workspace_id, user_id)
if not connector:
return {"status": "error", "message": "No Teams connector found."}

View file

@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
def create_read_teams_messages_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -32,13 +32,13 @@ def create_read_teams_messages_tool(
Dictionary with status and a list of messages including
id, sender, content, timestamp.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Teams tool not properly configured."}
limit = min(limit, 50)
try:
connector = await get_teams_connector(db_session, search_space_id, user_id)
connector = await get_teams_connector(db_session, workspace_id, user_id)
if not connector:
return {"status": "error", "message": "No Teams connector found."}

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def create_send_teams_message_tool(
db_session: AsyncSession | None = None,
search_space_id: int | None = None,
workspace_id: int | None = None,
user_id: str | None = None,
):
@tool
@ -41,11 +41,11 @@ def create_send_teams_message_tool(
IMPORTANT:
- If status is "rejected", the user explicitly declined. Do NOT retry.
"""
if db_session is None or search_space_id is None or user_id is None:
if db_session is None or workspace_id is None or user_id is None:
return {"status": "error", "message": "Teams tool not properly configured."}
try:
connector = await get_teams_connector(db_session, search_space_id, user_id)
connector = await get_teams_connector(db_session, workspace_id, user_id)
if not connector:
return {"status": "error", "message": "No Teams connector found."}

View file

@ -29,12 +29,12 @@ logger = logging.getLogger(__name__)
async def fetch_mcp_connector_metadata_maps(
session: AsyncSession,
search_space_id: int,
workspace_id: int,
) -> tuple[dict[int, str], dict[str, str]]:
"""Resolve connector id and display name to connector type for MCP tool routing."""
result = await session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.workspace_id == workspace_id,
cast(SearchSourceConnector.config, JSONB).has_key("server_config"),
),
)
@ -101,13 +101,13 @@ def partition_mcp_tools_by_connector(
async def load_mcp_tools_by_connector(
session: AsyncSession,
search_space_id: int,
workspace_id: int,
) -> dict[str, list[BaseTool]]:
"""Load MCP tools and route them to each subagent as a flat list.
``bypass_internal_hitl=True`` is set so tool gating is uniformly the
consuming subagent's :class:`PermissionMiddleware` responsibility.
"""
flat = await load_mcp_tools(session, search_space_id, bypass_internal_hitl=True)
id_map, name_map = await fetch_mcp_connector_metadata_maps(session, search_space_id)
flat = await load_mcp_tools(session, workspace_id, bypass_internal_hitl=True)
id_map, name_map = await fetch_mcp_connector_metadata_maps(session, workspace_id)
return partition_mcp_tools_by_connector(flat, id_map, name_map)

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