Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7ed6e9c
fix: update message handling in NewChatPage to conditionally set auth…
AnishSarkar22 Mar 29, 2026
75fd39c
refactor: simplify author metadata handling in NewChatPage and UserMe…
AnishSarkar22 Mar 29, 2026
fec5c00
refactor: enhance button loading states in various components for imp…
AnishSarkar22 Mar 29, 2026
db26373
refactor: update styling for button and command components in ImageMo…
AnishSarkar22 Mar 29, 2026
430372a
refactor: move ImageConfigDialog to shared components and update impo…
AnishSarkar22 Mar 29, 2026
f4adfb5
refactor: update global image model and configuration messages for cl…
AnishSarkar22 Mar 29, 2026
d88236d
refactor: replace ModelConfigDialog with a shared component and updat…
AnishSarkar22 Mar 29, 2026
32ff5f0
refactor: simplify onboarding page logic by temporarily disabling aut…
AnishSarkar22 Mar 29, 2026
ba926bb
refactor: integrate global loading effect into onboarding page and st…
AnishSarkar22 Mar 29, 2026
a5f41cf
refactor: update LLM configuration terminology and enhance user feedb…
AnishSarkar22 Mar 29, 2026
4a05229
refactor: enhance image generation configuration handling and user fe…
AnishSarkar22 Mar 29, 2026
b5cc45e
refactor: streamline image and model configuration dialogs by removin…
AnishSarkar22 Mar 29, 2026
7632291
refactor: improve UI consistency by standardizing header and sidebar …
AnishSarkar22 Mar 29, 2026
b54aa51
refactor: implement chat tab removal functionality and enhance tab ti…
AnishSarkar22 Mar 29, 2026
69b8eef
refactor: enhance chat tab management by implementing fallback naviga…
AnishSarkar22 Mar 29, 2026
38b77df
refactor: update editable document types to include 'FILE' and enhanc…
AnishSarkar22 Mar 29, 2026
73016b4
refactor: enhance TabBar component with active tab highlighting and r…
AnishSarkar22 Mar 29, 2026
0e3f5d8
refactor: add OneDrive support to connector icons and types for enhan…
AnishSarkar22 Mar 29, 2026
9eab427
feat: introduce citation components from tool-ui with hover popover f…
AnishSarkar22 Mar 29, 2026
74826b3
feat: enhance web search tool integration with citation management an…
AnishSarkar22 Mar 29, 2026
04691d5
chore: ran linting
AnishSarkar22 Mar 29, 2026
cbcaa7a
feat: add mobile citation drawer and enhance citation metadata contex…
AnishSarkar22 Mar 29, 2026
411ea3e
fix: adjust DrawerHandle dimensions for improved UI consistency
AnishSarkar22 Mar 29, 2026
46f8553
feat: update MobileCitationDrawer layout and enhance DrawerHeader for…
AnishSarkar22 Mar 29, 2026
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: 1 addition & 1 deletion docs/chinese-llm-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ SurfSense 现已支持以下国产 LLM:

1. 登录 SurfSense Dashboard
2. 进入 **Settings****API Keys** (或 **LLM Configurations**)
3. 点击 **Add New Configuration**
3. 点击 **Add LLM Model**
4.**Provider** 下拉菜单中选择你的国产 LLM 提供商
5. 填写必填字段(见下方各提供商详细配置)
6. 点击 **Save**
Expand Down
4 changes: 3 additions & 1 deletion surfsense_backend/alembic/versions/111_add_prompts_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def upgrade() -> None:
)
""")
op.execute("CREATE INDEX ix_prompts_user_id ON prompts (user_id)")
op.execute("CREATE INDEX ix_prompts_search_space_id ON prompts (search_space_id)")
op.execute(
"CREATE INDEX ix_prompts_search_space_id ON prompts (search_space_id)"
)


def downgrade() -> None:
Expand Down
49 changes: 34 additions & 15 deletions surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ async def create_onedrive_file(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
)
)
connectors = result.scalars().all()
Expand All @@ -95,12 +96,14 @@ async def create_onedrive_file(
accounts = []
for c in connectors:
cfg = c.config or {}
accounts.append({
"id": c.id,
"name": c.name,
"user_email": cfg.get("user_email"),
"auth_expired": cfg.get("auth_expired", False),
})
accounts.append(
{
"id": c.id,
"name": c.name,
"user_email": cfg.get("user_email"),
"auth_expired": cfg.get("auth_expired", False),
}
)

if all(a.get("auth_expired") for a in accounts):
return {
Expand All @@ -119,16 +122,22 @@ async def create_onedrive_file(
client = OneDriveClient(session=db_session, connector_id=cid)
items, err = await client.list_children("root")
if err:
logger.warning("Failed to list folders for connector %s: %s", cid, err)
logger.warning(
"Failed to list folders for connector %s: %s", cid, err
)
parent_folders[cid] = []
else:
parent_folders[cid] = [
{"folder_id": item["id"], "name": item["name"]}
for item in items
if item.get("folder") is not None and item.get("id") and item.get("name")
if item.get("folder") is not None
and item.get("id")
and item.get("name")
]
except Exception:
logger.warning("Error fetching folders for connector %s", cid, exc_info=True)
logger.warning(
"Error fetching folders for connector %s", cid, exc_info=True
)
parent_folders[cid] = []

context: dict[str, Any] = {
Expand All @@ -152,8 +161,12 @@ async def create_onedrive_file(
}
)

decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
decisions_raw = (
approval.get("decisions", []) if isinstance(approval, dict) else []
)
decisions = (
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
)
decisions = [d for d in decisions if isinstance(d, dict)]
if not decisions:
return {"status": "error", "message": "No approval decision received"}
Expand Down Expand Up @@ -192,15 +205,19 @@ async def create_onedrive_file(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
)
)
connector = result.scalars().first()
else:
connector = connectors[0]

if not connector:
return {"status": "error", "message": "Selected OneDrive connector is invalid."}
return {
"status": "error",
"message": "Selected OneDrive connector is invalid.",
}

docx_bytes = _markdown_to_docx(final_content or "")

Expand All @@ -212,7 +229,9 @@ async def create_onedrive_file(
mime_type=DOCX_MIME,
)

logger.info(f"OneDrive file created: id={created.get('id')}, name={created.get('name')}")
logger.info(
f"OneDrive file created: id={created.get('id')}, name={created.get('name')}"
)

kb_message_suffix = ""
try:
Expand Down
59 changes: 45 additions & 14 deletions surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ async def delete_onedrive_file(
- If status is "not_found", relay the exact message to the user and ask them
to verify the file name or check if it has been indexed.
"""
logger.info(f"delete_onedrive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}")
logger.info(
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:
return {"status": "error", "message": "OneDrive tool not properly configured."}
return {
"status": "error",
"message": "OneDrive tool not properly configured.",
}

try:
doc_result = await db_session.execute(
Expand Down Expand Up @@ -89,8 +94,12 @@ async def delete_onedrive_file(
Document.search_space_id == search_space_id,
Document.document_type == DocumentType.ONEDRIVE_FILE,
func.lower(
cast(Document.document_metadata["onedrive_file_name"], String)
) == func.lower(file_name),
cast(
Document.document_metadata["onedrive_file_name"],
String,
)
)
== func.lower(file_name),
SearchSourceConnector.user_id == user_id,
)
)
Expand All @@ -110,28 +119,38 @@ async def delete_onedrive_file(
}

if not document.connector_id:
return {"status": "error", "message": "Document has no associated connector."}
return {
"status": "error",
"message": "Document has no associated connector.",
}

meta = document.document_metadata or {}
file_id = meta.get("onedrive_file_id")
document_id = document.id

if not file_id:
return {"status": "error", "message": "File ID is missing. Please re-index the file."}
return {
"status": "error",
"message": "File ID is missing. Please re-index the file.",
}

conn_result = await db_session.execute(
select(SearchSourceConnector).filter(
and_(
SearchSourceConnector.id == document.connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
)
)
)
connector = conn_result.scalars().first()
if not connector:
return {"status": "error", "message": "OneDrive connector not found or access denied."}
return {
"status": "error",
"message": "OneDrive connector not found or access denied.",
}

cfg = connector.config or {}
if cfg.get("auth_expired"):
Expand Down Expand Up @@ -170,8 +189,12 @@ async def delete_onedrive_file(
}
)

decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
decisions_raw = (
approval.get("decisions", []) if isinstance(approval, dict) else []
)
decisions = (
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
)
decisions = [d for d in decisions if isinstance(d, dict)]
if not decisions:
return {"status": "error", "message": "No approval decision received"}
Expand Down Expand Up @@ -206,7 +229,8 @@ async def delete_onedrive_file(
SearchSourceConnector.id == final_connector_id,
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
)
)
)
Expand All @@ -224,10 +248,14 @@ async def delete_onedrive_file(
f"Deleting OneDrive file: file_id='{final_file_id}', connector={actual_connector_id}"
)

client = OneDriveClient(session=db_session, connector_id=actual_connector_id)
client = OneDriveClient(
session=db_session, connector_id=actual_connector_id
)
await client.trash_file(final_file_id)

logger.info(f"OneDrive file deleted (moved to recycle bin): file_id={final_file_id}")
logger.info(
f"OneDrive file deleted (moved to recycle bin): file_id={final_file_id}"
)

trash_result: dict[str, Any] = {
"status": "success",
Expand Down Expand Up @@ -272,6 +300,9 @@ async def delete_onedrive_file(
if isinstance(e, GraphInterrupt):
raise
logger.error(f"Error deleting OneDrive file: {e}", exc_info=True)
return {"status": "error", "message": "Something went wrong while trashing the file. Please try again."}
return {
"status": "error",
"message": "Something went wrong while trashing the file. Please try again.",
}

return delete_onedrive_file
22 changes: 13 additions & 9 deletions surfsense_backend/app/connectors/onedrive/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ async def _get_valid_token(self) -> str:

cfg = connector.config or {}
is_encrypted = cfg.get("_token_encrypted", False)
token_encryption = TokenEncryption(config.SECRET_KEY) if config.SECRET_KEY else None
token_encryption = (
TokenEncryption(config.SECRET_KEY) if config.SECRET_KEY else None
)

access_token = cfg.get("access_token", "")
refresh_token = cfg.get("refresh_token")
Expand Down Expand Up @@ -206,18 +208,20 @@ async def download_file(self, item_id: str) -> tuple[bytes | None, str | None]:
async def download_file_to_disk(self, item_id: str, dest_path: str) -> str | None:
"""Stream file content to disk. Returns error message on failure."""
token = await self._get_valid_token()
async with httpx.AsyncClient(follow_redirects=True) as client:
async with client.stream(
async with (
httpx.AsyncClient(follow_redirects=True) as client,
client.stream(
"GET",
f"{GRAPH_API_BASE}/me/drive/items/{item_id}/content",
headers={"Authorization": f"Bearer {token}"},
timeout=120.0,
) as resp:
if resp.status_code != 200:
return f"Download failed: {resp.status_code}"
with open(dest_path, "wb") as f:
async for chunk in resp.aiter_bytes(chunk_size=5 * 1024 * 1024):
f.write(chunk)
) as resp,
):
if resp.status_code != 200:
return f"Download failed: {resp.status_code}"
with open(dest_path, "wb") as f:
async for chunk in resp.aiter_bytes(chunk_size=5 * 1024 * 1024):
f.write(chunk)
return None

async def create_file(
Expand Down
32 changes: 22 additions & 10 deletions surfsense_backend/app/connectors/onedrive/content_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import asyncio
import contextlib
import logging
import os
import tempfile
Expand Down Expand Up @@ -60,7 +61,9 @@ async def download_and_extract_content(

temp_file_path = None
try:
extension = Path(file_name).suffix or get_extension_from_mime(mime_type) or ".bin"
extension = (
Path(file_name).suffix or get_extension_from_mime(mime_type) or ".bin"
)
with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as tmp:
temp_file_path = tmp.name

Expand All @@ -76,10 +79,8 @@ async def download_and_extract_content(
return None, metadata, str(e)
finally:
if temp_file_path and os.path.exists(temp_file_path):
try:
with contextlib.suppress(Exception):
os.unlink(temp_file_path)
except Exception:
pass


async def _parse_file_to_markdown(file_path: str, filename: str) -> str:
Expand All @@ -94,9 +95,10 @@ async def _parse_file_to_markdown(file_path: str, filename: str) -> str:
return f.read()

if lower.endswith((".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm")):
from app.config import config as app_config
from litellm import atranscription

from app.config import config as app_config

stt_service_type = (
"local"
if app_config.STT_SERVICE and app_config.STT_SERVICE.startswith("local/")
Expand All @@ -106,9 +108,13 @@ async def _parse_file_to_markdown(file_path: str, filename: str) -> str:
from app.services.stt_service import stt_service

t0 = time.monotonic()
logger.info(f"[local-stt] START file={filename} thread={threading.current_thread().name}")
logger.info(
f"[local-stt] START file={filename} thread={threading.current_thread().name}"
)
result = await asyncio.to_thread(stt_service.transcribe_file, file_path)
logger.info(f"[local-stt] END file={filename} elapsed={time.monotonic() - t0:.2f}s")
logger.info(
f"[local-stt] END file={filename} elapsed={time.monotonic() - t0:.2f}s"
)
text = result.get("text", "")
else:
with open(file_path, "rb") as audio_file:
Expand Down Expand Up @@ -150,7 +156,9 @@ async def _parse_file_to_markdown(file_path: str, filename: str) -> str:
parse_with_llamacloud_retry,
)

result = await parse_with_llamacloud_retry(file_path=file_path, estimated_pages=50)
result = await parse_with_llamacloud_retry(
file_path=file_path, estimated_pages=50
)
markdown_documents = await result.aget_markdown_documents(split_by_page=False)
if not markdown_documents:
raise RuntimeError(f"LlamaCloud returned no documents for {filename}")
Expand All @@ -161,9 +169,13 @@ async def _parse_file_to_markdown(file_path: str, filename: str) -> str:

converter = DocumentConverter()
t0 = time.monotonic()
logger.info(f"[docling] START file={filename} thread={threading.current_thread().name}")
logger.info(
f"[docling] START file={filename} thread={threading.current_thread().name}"
)
result = await asyncio.to_thread(converter.convert, file_path)
logger.info(f"[docling] END file={filename} elapsed={time.monotonic() - t0:.2f}s")
logger.info(
f"[docling] END file={filename} elapsed={time.monotonic() - t0:.2f}s"
)
return result.document.export_to_markdown()

raise RuntimeError(f"Unknown ETL_SERVICE: {app_config.ETL_SERVICE}")
Loading
Loading