Files
wehub-resource-sync 6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:44 +08:00

189 lines
6.6 KiB
Python

"""Knowledge-base read tools: semantic search, list, and read one document.
Search and list default to the active workspace; a document read is addressed by
id, which is unique across the account, so it needs no workspace.
"""
from __future__ import annotations
from typing import Annotated
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from ...core.client import SurfSenseClient
from ...core.rendering import ResponseFormatParam, clip, to_json
from ...core.workspace_context import WorkspaceContext, WorkspaceParam
from .annotations import READ, DocumentId, DocumentTypes
def register(
mcp: FastMCP, client: SurfSenseClient, context: WorkspaceContext
) -> None:
"""Register the knowledge-base read tools."""
@mcp.tool(
name="surfsense_search_knowledge_base",
title="Search knowledge base",
annotations=READ,
structured_output=False,
)
async def search_knowledge_base(
query: Annotated[
str,
Field(
min_length=1,
description="Natural-language search, e.g. "
"'notebooklm user complaints'.",
),
],
top_k: Annotated[
int, Field(ge=1, le=20, description="Maximum documents to return.")
] = 5,
document_types: DocumentTypes = None,
workspace: WorkspaceParam = None,
response_format: ResponseFormatParam = "markdown",
) -> str:
"""Search the workspace's knowledge base by meaning and keywords.
Use this FIRST when a question might be answered by content already
stored in SurfSense — notes, uploaded files, saved pages, past
research. Do NOT use it to fetch new data from the web; use the
scraper tools for that. Returns the most relevant documents with the
passages that matched, ranked by relevance score.
Example: query='pricing feedback', top_k=5.
"""
resolved = await context.resolve(workspace)
hits = await client.request(
"POST",
"/documents/search-semantic",
json={
"workspace_id": resolved.id,
"query": query,
"top_k": max(1, min(top_k, 20)),
"document_types": document_types,
},
)
items = (hits or {}).get("items", [])
if response_format == "json":
return to_json(items)
return _render_search(query, items)
@mcp.tool(
name="surfsense_list_documents",
title="List documents",
annotations=READ,
structured_output=False,
)
async def list_documents(
document_types: DocumentTypes = None,
folder_id: Annotated[
int | None,
Field(description="Only documents in this folder. Omit for all."),
] = None,
page: Annotated[
int, Field(ge=0, description="Zero-based page number.")
] = 0,
page_size: Annotated[
int, Field(ge=1, description="Documents per page.")
] = 20,
workspace: WorkspaceParam = None,
response_format: ResponseFormatParam = "markdown",
) -> str:
"""List documents in the workspace's knowledge base, newest first.
Use this to browse or inventory what is stored; to find documents
about a topic, prefer surfsense_search_knowledge_base. Returns each
document's title, id, type, and update time, plus a has_more flag —
request the next page by increasing page.
Example: document_types=['FILE'], page=0, page_size=20.
"""
resolved = await context.resolve(workspace)
result = await client.request(
"GET",
"/documents",
params={
"workspace_id": resolved.id,
"page": page,
"page_size": page_size,
"document_types": _join(document_types),
"folder_id": folder_id,
},
)
if response_format == "json":
return to_json(result)
return _render_document_list(result)
@mcp.tool(
name="surfsense_get_document",
title="Read one document",
annotations=READ,
structured_output=False,
)
async def get_document(
document_id: DocumentId,
response_format: ResponseFormatParam = "markdown",
) -> str:
"""Read one document's full content and metadata by id.
Use this after surfsense_search_knowledge_base or
surfsense_list_documents to open a specific document — search results
only include the matching passages, this returns the whole text.
"""
document = await client.request("GET", f"/documents/{document_id}")
if response_format == "json":
return clip(to_json(document))
return _render_document(document)
def _join(values: list[str] | None) -> str | None:
return ",".join(values) if values else None
def _render_search(query: str, items: list[dict]) -> str:
if not items:
return f'No matches for "{query}".'
lines = [f'# {len(items)} result(s) for "{query}"', ""]
for hit in items:
lines.append(
f"## {hit.get('title', 'Untitled')} "
f"(id {hit.get('document_id')}) — score {hit.get('score', 0):.3f}"
)
for chunk in hit.get("chunks", []):
excerpt = clip(chunk.get("content", "").strip(), 500)
lines.append(f"> {excerpt}")
lines.append("")
return "\n".join(lines).strip()
def _render_document_list(result: dict | None) -> str:
items = (result or {}).get("items", [])
if not items:
return "No documents found."
lines = ["# Documents", ""]
for doc in items:
lines.append(
f"- **{doc.get('title', 'Untitled')}** (id {doc.get('id')}) · "
f"{doc.get('document_type')} · updated {doc.get('updated_at')}"
)
total = (result or {}).get("total", len(items))
page = (result or {}).get("page", 0)
has_more = (result or {}).get("has_more", False)
lines.append("")
lines.append(
f"_Page {page} · showing {len(items)} of {total}"
+ (" · more available_" if has_more else "_")
)
return "\n".join(lines)
def _render_document(document: dict) -> str:
content = clip(document.get("content", "") or "(empty)")
return (
f"# {document.get('title', 'Untitled')} (id {document.get('id')})\n"
f"- type: {document.get('document_type')}\n"
f"- workspace: {document.get('workspace_id')}\n"
f"- updated: {document.get('updated_at')}\n\n"
f"{content}"
)