Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions surfsense_backend/app/agents/new_chat/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class SurfSenseContextSchema:
search_space_id: int | None = None
mentioned_document_ids: list[int] = field(default_factory=list)
mentioned_folder_ids: list[int] = field(default_factory=list)
mentioned_connector_ids: list[int] = field(default_factory=list)
mentioned_connectors: list[dict[str, object]] = field(default_factory=list)
file_operation_contract: FileOperationContractState | None = None
turn_id: str | None = None
request_id: str | None = None
2 changes: 1 addition & 1 deletion surfsense_backend/app/agents/new_chat/mention_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async def resolve_mentions(
kind = chip.kind
if kind == "folder":
chip_folder_ids.append(chip.id)
else:
elif kind == "doc":
chip_doc_ids.append(chip.id)
chip_titles_by_id[(kind, chip.id)] = chip.title

Expand Down
14 changes: 14 additions & 0 deletions surfsense_backend/app/routes/new_chat_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,11 @@ async def handle_new_chat(
if request.mentioned_documents
else None
)
mentioned_connectors_payload = (
[doc.model_dump() for doc in request.mentioned_connectors]
if request.mentioned_connectors
else None
)

return StreamingResponse(
stream_new_chat(
Expand All @@ -1782,6 +1787,8 @@ async def handle_new_chat(
mentioned_document_ids=request.mentioned_document_ids,
mentioned_surfsense_doc_ids=request.mentioned_surfsense_doc_ids,
mentioned_folder_ids=request.mentioned_folder_ids,
mentioned_connector_ids=request.mentioned_connector_ids,
mentioned_connectors=mentioned_connectors_payload,
mentioned_documents=mentioned_documents_payload,
needs_history_bootstrap=thread.needs_history_bootstrap,
thread_visibility=thread.visibility,
Expand Down Expand Up @@ -2258,6 +2265,11 @@ async def stream_with_cleanup():
if request.mentioned_documents
else None
)
mentioned_connectors_payload = (
[doc.model_dump() for doc in request.mentioned_connectors]
if request.mentioned_connectors
else None
)
try:
async for chunk in stream_new_chat(
user_query=str(user_query_to_use),
Expand All @@ -2268,6 +2280,8 @@ async def stream_with_cleanup():
mentioned_document_ids=request.mentioned_document_ids,
mentioned_surfsense_doc_ids=request.mentioned_surfsense_doc_ids,
mentioned_folder_ids=request.mentioned_folder_ids,
mentioned_connector_ids=request.mentioned_connector_ids,
mentioned_connectors=mentioned_connectors_payload,
mentioned_documents=mentioned_documents_payload,
checkpoint_id=target_checkpoint_id,
needs_history_bootstrap=thread.needs_history_bootstrap,
Expand Down
38 changes: 25 additions & 13 deletions surfsense_backend/app/schemas/new_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,32 +203,30 @@ def as_data_url(self) -> str:
class MentionedDocumentInfo(BaseModel):
"""Display metadata for a single ``@``-mention chip.

Carries either a knowledge-base document or a knowledge-base folder
(discriminated by ``kind``). The full triple
``{id, title, document_type}`` is forwarded by the frontend mention
chip so the server can embed it in the persisted user message
``ContentPart[]`` (single ``mentioned-documents`` part). The
history loader then renders the chips on reload without an extra
fetch — mirrors the pre-refactor frontend ``persistUserTurn`` shape.
Carries a knowledge-base document, knowledge-base folder, or
connected account (discriminated by ``kind``). Each kind uses its
real identity fields: docs carry ``document_type``, folders carry
only their folder id/title, and connectors carry ``connector_type``
plus account metadata.

``kind`` defaults to ``"doc"`` so legacy clients and persisted rows
that predate folder mentions deserialise unchanged.
"""

id: int
title: str = Field(..., min_length=1, max_length=500)
document_type: str = Field(..., min_length=1, max_length=100)
kind: Literal["doc", "folder"] = Field(
document_type: str | None = Field(default=None, min_length=1, max_length=100)
kind: Literal["doc", "folder", "connector"] = Field(
default="doc",
description=(
"Discriminator for the chip's referent: ``doc`` is a "
"knowledge-base ``Document`` row, ``folder`` is a "
"knowledge-base ``Folder`` row. Folders carry the sentinel "
"``document_type='FOLDER'`` to keep the frontend dedup key "
"``(kind:document_type:id)`` from colliding doc and folder "
"ids that happen to share an integer value."
"knowledge-base ``Folder`` row, and ``connector`` is a "
"concrete connected account."
),
)
connector_type: str | None = Field(default=None, max_length=100)
account_name: str | None = Field(default=None, max_length=255)


class NewChatRequest(BaseModel):
Expand Down Expand Up @@ -266,6 +264,18 @@ class NewChatRequest(BaseModel):
"a mentioned-documents part."
),
)
mentioned_connector_ids: list[int] | None = Field(
default=None,
description="Optional concrete connector account IDs the user @-mentioned.",
)
mentioned_connectors: list[MentionedDocumentInfo] | None = Field(
default=None,
description=(
"Display/context metadata for selected connector accounts. "
"Kept separate from document/folder id arrays so tools can "
"prefer the exact account the user selected."
),
)
disabled_tools: list[str] | None = (
None # Optional list of tool names the user has disabled from the UI
)
Expand Down Expand Up @@ -335,6 +345,8 @@ class RegenerateRequest(BaseModel):
"new user message. None means no chip metadata."
),
)
mentioned_connector_ids: list[int] | None = None
mentioned_connectors: list[MentionedDocumentInfo] | None = None
disabled_tools: list[str] | None = None
filesystem_mode: Literal["cloud", "desktop_local_folder"] = "cloud"
client_platform: Literal["web", "desktop"] = "web"
Expand Down
37 changes: 23 additions & 14 deletions surfsense_backend/app/tasks/chat/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,16 @@ def _build_user_content(
[{"type": "text", "text": "..."},
{"type": "image", "image": "data:..."},
{"type": "mentioned-documents", "documents": [{"id": int,
"title": str, "document_type": str, "kind": "doc" | "folder"},
"title": str, "kind": "doc" | "folder" | "connector", ...},
...]}]

The companion reader is
``app.utils.user_message_multimodal.split_persisted_user_content_parts``
which expects exactly this shape — keep them in sync.

``mentioned_documents``: optional list of mention chip dicts. Each
dict may include a ``kind`` discriminator (``"doc"`` or ``"folder"``)
so the persisted ContentPart round-trips folder chips on reload.
dict may include a ``kind`` discriminator so the persisted
ContentPart round-trips folder and connector chips on reload.
When ``kind`` is missing we default to ``"doc"`` so legacy clients
that haven't migrated to the union schema still persist correctly.
"""
Expand All @@ -134,18 +134,27 @@ def _build_user_content(
doc_id = doc.get("id")
title = doc.get("title")
document_type = doc.get("document_type")
if doc_id is None or title is None or document_type is None:
continue
kind_raw = doc.get("kind", "doc")
kind = kind_raw if kind_raw in ("doc", "folder") else "doc"
normalized.append(
{
"id": doc_id,
"title": str(title),
"document_type": str(document_type),
"kind": kind,
}
)
kind = kind_raw if kind_raw in ("doc", "folder", "connector") else "doc"
if doc_id is None or title is None:
continue
if kind == "doc" and document_type is None:
continue
item = {
"id": doc_id,
"title": str(title),
"kind": kind,
}
if document_type is not None:
item["document_type"] = str(document_type)
if kind == "connector":
connector_type = doc.get("connector_type")
if connector_type is None:
continue
account_name = doc.get("account_name") or title
item["connector_type"] = str(connector_type)
item["account_name"] = str(account_name)
normalized.append(item)
if normalized:
parts.append({"type": "mentioned-documents", "documents": normalized})
return parts
Expand Down
31 changes: 31 additions & 0 deletions surfsense_backend/app/tasks/chat/stream_new_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ async def stream_new_chat(
mentioned_document_ids: list[int] | None = None,
mentioned_surfsense_doc_ids: list[int] | None = None,
mentioned_folder_ids: list[int] | None = None,
mentioned_connector_ids: list[int] | None = None,
mentioned_connectors: list[dict[str, Any]] | None = None,
mentioned_documents: list[dict[str, Any]] | None = None,
checkpoint_id: str | None = None,
needs_history_bootstrap: bool = False,
Expand Down Expand Up @@ -1385,6 +1387,33 @@ async def _load_llm_bundle(
format_mentioned_surfsense_docs_as_context(mentioned_surfsense_docs)
)

if mentioned_connectors:
connector_lines = []
for connector in mentioned_connectors:
if not isinstance(connector, dict):
continue
connector_id = connector.get("id")
connector_type = connector.get("connector_type") or connector.get(
"document_type"
)
account_name = connector.get("account_name") or connector.get("title")
if connector_id is None or connector_type is None:
continue
connector_lines.append(
f' - connector_id={connector_id}, connector_type="{connector_type}", '
f'account_name="{account_name or ""}"'
)
if connector_lines:
context_parts.append(
"<mentioned_connectors>\n"
"The user selected these exact connector accounts with @. "
"These entries are selection metadata, not retrieved connector content. "
"When a connector-backed tool needs an account, use the matching "
"connector_id from this list if the tool supports connector_id:\n"
+ "\n".join(connector_lines)
+ "\n</mentioned_connectors>"
)

# Surface report IDs prominently so the LLM doesn't have to
# retrieve them from old tool responses in conversation history.
if recent_reports:
Expand Down Expand Up @@ -1778,6 +1807,8 @@ async def _generate_title() -> tuple[str | None, dict | None]:
mentioned_folder_ids=list(
accepted_folder_ids or mentioned_folder_ids or []
),
mentioned_connector_ids=list(mentioned_connector_ids or []),
mentioned_connectors=list(mentioned_connectors or []),
request_id=request_id,
turn_id=stream_result.turn_id,
)
Expand Down
Loading
Loading