diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py
index 73a39ccbf..ddf87cf2a 100644
--- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py
+++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py
@@ -50,7 +50,10 @@ from app.agents.new_chat.system_prompt import (
build_configurable_system_prompt,
build_surfsense_system_prompt,
)
-from app.agents.new_chat.tools.registry import build_tools_async, get_connector_gated_tools
+from app.agents.new_chat.tools.registry import (
+ build_tools_async,
+ get_connector_gated_tools,
+)
from app.db import ChatVisibility
from app.services.connector_service import ConnectorService
from app.utils.perf import get_perf_logger
@@ -294,9 +297,7 @@ async def create_surfsense_deep_agent(
}
modified_disabled_tools = list(disabled_tools) if disabled_tools else []
- modified_disabled_tools.extend(
- get_connector_gated_tools(available_connectors)
- )
+ modified_disabled_tools.extend(get_connector_gated_tools(available_connectors))
# Remove direct KB search tool; we now pre-seed a scoped filesystem via middleware.
if "search_knowledge_base" not in modified_disabled_tools:
@@ -328,7 +329,8 @@ async def create_surfsense_deep_agent(
meta = getattr(t, "metadata", None) or {}
if meta.get("mcp_is_generic") and meta.get("mcp_connector_name"):
_mcp_connector_tools.setdefault(
- meta["mcp_connector_name"], [],
+ meta["mcp_connector_name"],
+ [],
).append(t.name)
if _mcp_connector_tools:
diff --git a/surfsense_backend/app/agents/new_chat/middleware/__init__.py b/surfsense_backend/app/agents/new_chat/middleware/__init__.py
index 5a24b2f9e..6e4542e1a 100644
--- a/surfsense_backend/app/agents/new_chat/middleware/__init__.py
+++ b/surfsense_backend/app/agents/new_chat/middleware/__init__.py
@@ -3,12 +3,12 @@
from app.agents.new_chat.middleware.dedup_tool_calls import (
DedupHITLToolCallsMiddleware,
)
-from app.agents.new_chat.middleware.filesystem import (
- SurfSenseFilesystemMiddleware,
-)
from app.agents.new_chat.middleware.file_intent import (
FileIntentMiddleware,
)
+from app.agents.new_chat.middleware.filesystem import (
+ SurfSenseFilesystemMiddleware,
+)
from app.agents.new_chat.middleware.knowledge_search import (
KnowledgeBaseSearchMiddleware,
)
diff --git a/surfsense_backend/app/agents/new_chat/middleware/file_intent.py b/surfsense_backend/app/agents/new_chat/middleware/file_intent.py
index 4bf5dcfe4..05cb230ce 100644
--- a/surfsense_backend/app/agents/new_chat/middleware/file_intent.py
+++ b/surfsense_backend/app/agents/new_chat/middleware/file_intent.py
@@ -213,7 +213,9 @@ def _build_classifier_prompt(*, recent_conversation: str, user_text: str) -> str
)
-def _build_recent_conversation(messages: list[BaseMessage], *, max_messages: int = 6) -> str:
+def _build_recent_conversation(
+ messages: list[BaseMessage], *, max_messages: int = 6
+) -> str:
rows: list[str] = []
for msg in messages[-max_messages:]:
role = "user" if isinstance(msg, HumanMessage) else "assistant"
@@ -246,7 +248,9 @@ class FileIntentMiddleware(AgentMiddleware): # type: ignore[type-arg]
[HumanMessage(content=prompt)],
config={"tags": ["surfsense:internal"]},
)
- payload = json.loads(_extract_json_payload(_extract_text_from_message(response)))
+ payload = json.loads(
+ _extract_json_payload(_extract_text_from_message(response))
+ )
plan = FileIntentPlan.model_validate(payload)
return plan
except (json.JSONDecodeError, ValidationError, ValueError) as exc:
@@ -317,4 +321,3 @@ class FileIntentMiddleware(AgentMiddleware): # type: ignore[type-arg]
insert_at = max(len(new_messages) - 1, 0)
new_messages.insert(insert_at, contract_msg)
return {"messages": new_messages, "file_operation_contract": contract}
-
diff --git a/surfsense_backend/app/agents/new_chat/middleware/filesystem.py b/surfsense_backend/app/agents/new_chat/middleware/filesystem.py
index 8dfa89ef2..cb50693f1 100644
--- a/surfsense_backend/app/agents/new_chat/middleware/filesystem.py
+++ b/surfsense_backend/app/agents/new_chat/middleware/filesystem.py
@@ -877,7 +877,9 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
suggested_path = contract.get("suggested_path")
if isinstance(suggested_path, str) and suggested_path.strip():
normalized_suggested = self._normalize_absolute_path(suggested_path)
- suggested_mount = self._extract_mount_from_path(normalized_suggested, mounts)
+ suggested_mount = self._extract_mount_from_path(
+ normalized_suggested, mounts
+ )
matching_mounts = [
mount
@@ -1071,14 +1073,18 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
] = False,
) -> Command | str:
if self._filesystem_mode != FilesystemMode.DESKTOP_LOCAL_FOLDER:
- return "Error: move_file is only available in desktop local-folder mode."
+ return (
+ "Error: move_file is only available in desktop local-folder mode."
+ )
if not source_path.strip() or not destination_path.strip():
return "Error: source_path and destination_path are required."
resolved_backend = self._get_backend(runtime)
source_target = self._resolve_move_target_path(source_path, runtime)
- destination_target = self._resolve_move_target_path(destination_path, runtime)
+ destination_target = self._resolve_move_target_path(
+ destination_path, runtime
+ )
try:
validated_source = validate_path(source_target)
validated_destination = validate_path(destination_target)
@@ -1106,7 +1112,9 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
],
}
)
- return f"Moved '{validated_source}' to '{res.path or validated_destination}'"
+ return (
+ f"Moved '{validated_source}' to '{res.path or validated_destination}'"
+ )
async def async_move_file(
source_path: Annotated[
@@ -1125,14 +1133,18 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
] = False,
) -> Command | str:
if self._filesystem_mode != FilesystemMode.DESKTOP_LOCAL_FOLDER:
- return "Error: move_file is only available in desktop local-folder mode."
+ return (
+ "Error: move_file is only available in desktop local-folder mode."
+ )
if not source_path.strip() or not destination_path.strip():
return "Error: source_path and destination_path are required."
resolved_backend = self._get_backend(runtime)
source_target = self._resolve_move_target_path(source_path, runtime)
- destination_target = self._resolve_move_target_path(destination_path, runtime)
+ destination_target = self._resolve_move_target_path(
+ destination_path, runtime
+ )
try:
validated_source = validate_path(source_target)
validated_destination = validate_path(destination_target)
@@ -1160,7 +1172,9 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
],
}
)
- return f"Moved '{validated_source}' to '{res.path or validated_destination}'"
+ return (
+ f"Moved '{validated_source}' to '{res.path or validated_destination}'"
+ )
return StructuredTool.from_function(
name="move_file",
@@ -1201,7 +1215,9 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
] = True,
) -> str:
if self._filesystem_mode != FilesystemMode.DESKTOP_LOCAL_FOLDER:
- return "Error: list_tree is only available in desktop local-folder mode."
+ return (
+ "Error: list_tree is only available in desktop local-folder mode."
+ )
if max_depth < 0:
return "Error: max_depth must be >= 0."
if page_size < 1:
@@ -1253,7 +1269,9 @@ class SurfSenseFilesystemMiddleware(FilesystemMiddleware):
] = True,
) -> str:
if self._filesystem_mode != FilesystemMode.DESKTOP_LOCAL_FOLDER:
- return "Error: list_tree is only available in desktop local-folder mode."
+ return (
+ "Error: list_tree is only available in desktop local-folder mode."
+ )
if max_depth < 0:
return "Error: max_depth must be >= 0."
if page_size < 1:
diff --git a/surfsense_backend/app/agents/new_chat/middleware/knowledge_search.py b/surfsense_backend/app/agents/new_chat/middleware/knowledge_search.py
index 51378a013..6df317aaa 100644
--- a/surfsense_backend/app/agents/new_chat/middleware/knowledge_search.py
+++ b/surfsense_backend/app/agents/new_chat/middleware/knowledge_search.py
@@ -27,8 +27,8 @@ from pydantic import BaseModel, Field, ValidationError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from app.agents.new_chat.utils import parse_date_or_datetime, resolve_date_range
from app.agents.new_chat.filesystem_selection import FilesystemMode
+from app.agents.new_chat.utils import parse_date_or_datetime, resolve_date_range
from app.db import (
NATIVE_TO_LEGACY_DOCTYPE,
Chunk,
diff --git a/surfsense_backend/app/agents/new_chat/middleware/local_folder_backend.py b/surfsense_backend/app/agents/new_chat/middleware/local_folder_backend.py
index 0cee3e007..565fcb48b 100644
--- a/surfsense_backend/app/agents/new_chat/middleware/local_folder_backend.py
+++ b/surfsense_backend/app/agents/new_chat/middleware/local_folder_backend.py
@@ -120,7 +120,9 @@ class LocalFolderBackend:
if not target.exists() or not target.is_dir():
return []
infos: list[FileInfo] = []
- for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
+ for child in sorted(
+ target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())
+ ):
infos.append(
FileInfo(
path=self._to_virtual(child, self._root),
@@ -317,7 +319,9 @@ class LocalFolderBackend:
return WriteResult(error="Error: source and destination paths are the same")
with self._acquire_path_locks(source_path, destination_path):
if not source.exists():
- return WriteResult(error=f"Error: source path '{source_path}' not found")
+ return WriteResult(
+ error=f"Error: source path '{source_path}' not found"
+ )
if destination.exists():
if not overwrite:
return WriteResult(
@@ -339,8 +343,12 @@ class LocalFolderBackend:
else:
source.rename(destination)
except OSError as exc:
- return WriteResult(error=f"Error: failed to move '{source_path}': {exc}")
- return WriteResult(path=self._to_virtual(destination, self._root), files_update=None)
+ return WriteResult(
+ error=f"Error: failed to move '{source_path}': {exc}"
+ )
+ return WriteResult(
+ path=self._to_virtual(destination, self._root), files_update=None
+ )
async def amove(
self,
@@ -368,12 +376,16 @@ class LocalFolderBackend:
if not path.exists() or not path.is_file():
return EditResult(error=f"Error: File '{file_path}' not found")
content = path.read_text(encoding="utf-8", errors="replace")
- result = perform_string_replacement(content, old_string, new_string, replace_all)
+ result = perform_string_replacement(
+ content, old_string, new_string, replace_all
+ )
if isinstance(result, str):
return EditResult(error=result)
updated_content, occurrences = result
self._write_text_atomic(path, updated_content)
- return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
+ return EditResult(
+ path=file_path, files_update=None, occurrences=int(occurrences)
+ )
async def aedit(
self,
@@ -447,7 +459,9 @@ class LocalFolderBackend:
matches: list[GrepMatch] = []
for file_path in self._iter_candidate_files(path, glob):
try:
- lines = file_path.read_text(encoding="utf-8", errors="replace").splitlines()
+ lines = file_path.read_text(
+ encoding="utf-8", errors="replace"
+ ).splitlines()
except Exception:
continue
for idx, line in enumerate(lines, start=1):
@@ -481,12 +495,18 @@ class LocalFolderBackend:
FileUploadResponse(path=virtual_path, error=_FILE_NOT_FOUND)
)
except IsADirectoryError:
- responses.append(FileUploadResponse(path=virtual_path, error=_IS_DIRECTORY))
+ responses.append(
+ FileUploadResponse(path=virtual_path, error=_IS_DIRECTORY)
+ )
except Exception:
- responses.append(FileUploadResponse(path=virtual_path, error=_INVALID_PATH))
+ responses.append(
+ FileUploadResponse(path=virtual_path, error=_INVALID_PATH)
+ )
return responses
- async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
+ async def aupload_files(
+ self, files: list[tuple[str, bytes]]
+ ) -> list[FileUploadResponse]:
return await asyncio.to_thread(self.upload_files, files)
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
@@ -515,7 +535,9 @@ class LocalFolderBackend:
)
except Exception:
responses.append(
- FileDownloadResponse(path=virtual_path, content=None, error=_INVALID_PATH)
+ FileDownloadResponse(
+ path=virtual_path, content=None, error=_INVALID_PATH
+ )
)
return responses
diff --git a/surfsense_backend/app/agents/new_chat/middleware/multi_root_local_folder_backend.py b/surfsense_backend/app/agents/new_chat/middleware/multi_root_local_folder_backend.py
index 82914f9ce..93eabe6ff 100644
--- a/surfsense_backend/app/agents/new_chat/middleware/multi_root_local_folder_backend.py
+++ b/surfsense_backend/app/agents/new_chat/middleware/multi_root_local_folder_backend.py
@@ -127,7 +127,9 @@ class MultiRootLocalFolderBackend:
mount, local_path = self._split_mount_path(path)
except ValueError:
return []
- return self._transform_infos(mount, self._mount_to_backend[mount].ls_info(local_path))
+ return self._transform_infos(
+ mount, self._mount_to_backend[mount].ls_info(local_path)
+ )
async def als_info(self, path: str) -> list[FileInfo]:
return await asyncio.to_thread(self.ls_info, path)
@@ -355,7 +357,9 @@ class MultiRootLocalFolderBackend:
all_matches.extend(
[
GrepMatch(
- path=self._prefix_mount_path(mount, self._get_str(match, "path")),
+ path=self._prefix_mount_path(
+ mount, self._get_str(match, "path")
+ ),
line=self._get_int(match, "line"),
text=self._get_str(match, "text"),
)
@@ -394,7 +398,9 @@ class MultiRootLocalFolderBackend:
try:
mount, local_path = self._split_mount_path(virtual_path)
except ValueError:
- invalid.append(FileUploadResponse(path=virtual_path, error=_INVALID_PATH))
+ invalid.append(
+ FileUploadResponse(path=virtual_path, error=_INVALID_PATH)
+ )
continue
grouped.setdefault(mount, []).append((local_path, content))
@@ -404,7 +410,9 @@ class MultiRootLocalFolderBackend:
responses.extend(
[
FileUploadResponse(
- path=self._prefix_mount_path(mount, self._get_str(item, "path")),
+ path=self._prefix_mount_path(
+ mount, self._get_str(item, "path")
+ ),
error=self._get_str(item, "error") or None,
)
for item in result
@@ -412,7 +420,9 @@ class MultiRootLocalFolderBackend:
)
return responses
- async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
+ async def aupload_files(
+ self, files: list[tuple[str, bytes]]
+ ) -> list[FileUploadResponse]:
return await asyncio.to_thread(self.upload_files, files)
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
@@ -423,7 +433,9 @@ class MultiRootLocalFolderBackend:
mount, local_path = self._split_mount_path(virtual_path)
except ValueError:
invalid.append(
- FileDownloadResponse(path=virtual_path, content=None, error=_INVALID_PATH)
+ FileDownloadResponse(
+ path=virtual_path, content=None, error=_INVALID_PATH
+ )
)
continue
grouped.setdefault(mount, []).append(local_path)
@@ -434,7 +446,9 @@ class MultiRootLocalFolderBackend:
responses.extend(
[
FileDownloadResponse(
- path=self._prefix_mount_path(mount, self._get_str(item, "path")),
+ path=self._prefix_mount_path(
+ mount, self._get_str(item, "path")
+ ),
content=self._get_value(item, "content"),
error=self._get_str(item, "error") or None,
)
diff --git a/surfsense_backend/app/agents/new_chat/tools/connected_accounts.py b/surfsense_backend/app/agents/new_chat/tools/connected_accounts.py
index e0b1978e1..5675a42e6 100644
--- a/surfsense_backend/app/agents/new_chat/tools/connected_accounts.py
+++ b/surfsense_backend/app/agents/new_chat/tools/connected_accounts.py
@@ -57,7 +57,11 @@ def create_get_connected_accounts_tool(
async def _run(service: str) -> list[dict[str, Any]]:
svc_cfg = MCP_SERVICES.get(service)
if not svc_cfg:
- return [{"error": f"Unknown service '{service}'. Valid: {', '.join(sorted(MCP_SERVICES.keys()))}"}]
+ return [
+ {
+ "error": f"Unknown service '{service}'. Valid: {', '.join(sorted(MCP_SERVICES.keys()))}"
+ }
+ ]
try:
connector_type = SearchSourceConnectorType(svc_cfg.connector_type)
@@ -74,7 +78,11 @@ def create_get_connected_accounts_tool(
connectors = result.scalars().all()
if not connectors:
- return [{"error": f"No {svc_cfg.name} accounts connected. Ask the user to connect one in settings."}]
+ return [
+ {
+ "error": f"No {svc_cfg.name} accounts connected. Ask the user to connect one in settings."
+ }
+ ]
is_multi = len(connectors) > 1
diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/_auth.py b/surfsense_backend/app/agents/new_chat/tools/discord/_auth.py
index 1f51e3660..c345f8a5e 100644
--- a/surfsense_backend/app/agents/new_chat/tools/discord/_auth.py
+++ b/surfsense_backend/app/agents/new_chat/tools/discord/_auth.py
@@ -19,7 +19,8 @@ async def get_discord_connector(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
- SearchSourceConnector.connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR,
+ SearchSourceConnector.connector_type
+ == SearchSourceConnectorType.DISCORD_CONNECTOR,
)
)
return result.scalars().first()
diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/list_channels.py b/surfsense_backend/app/agents/new_chat/tools/discord/list_channels.py
index a33b88aa0..3cc99ac17 100644
--- a/surfsense_backend/app/agents/new_chat/tools/discord/list_channels.py
+++ b/surfsense_backend/app/agents/new_chat/tools/discord/list_channels.py
@@ -23,16 +23,24 @@ def create_list_discord_channels_tool(
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:
- return {"status": "error", "message": "Discord tool not properly configured."}
+ return {
+ "status": "error",
+ "message": "Discord tool not properly configured.",
+ }
try:
- connector = await get_discord_connector(db_session, search_space_id, user_id)
+ connector = await get_discord_connector(
+ db_session, search_space_id, user_id
+ )
if not connector:
return {"status": "error", "message": "No Discord connector found."}
guild_id = get_guild_id(connector)
if not guild_id:
- return {"status": "error", "message": "No guild ID in Discord connector config."}
+ return {
+ "status": "error",
+ "message": "No guild ID in Discord connector config.",
+ }
token = get_bot_token(connector)
@@ -44,9 +52,16 @@ def create_list_discord_channels_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Discord bot token is invalid.", "connector_type": "discord"}
+ return {
+ "status": "auth_error",
+ "message": "Discord bot token is invalid.",
+ "connector_type": "discord",
+ }
if resp.status_code != 200:
- return {"status": "error", "message": f"Discord API error: {resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Discord API error: {resp.status_code}",
+ }
# Type 0 = text channel
channels = [
@@ -54,7 +69,12 @@ def create_list_discord_channels_tool(
for ch in resp.json()
if ch.get("type") == 0
]
- return {"status": "success", "guild_id": guild_id, "channels": channels, "total": len(channels)}
+ return {
+ "status": "success",
+ "guild_id": guild_id,
+ "channels": channels,
+ "total": len(channels),
+ }
except Exception as e:
from langgraph.errors import GraphInterrupt
diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/read_messages.py b/surfsense_backend/app/agents/new_chat/tools/discord/read_messages.py
index 852a9297b..d8bf989a1 100644
--- a/surfsense_backend/app/agents/new_chat/tools/discord/read_messages.py
+++ b/surfsense_backend/app/agents/new_chat/tools/discord/read_messages.py
@@ -31,12 +31,17 @@ def create_read_discord_messages_tool(
id, author, content, timestamp.
"""
if db_session is None or search_space_id is None or user_id is None:
- return {"status": "error", "message": "Discord tool not properly configured."}
+ return {
+ "status": "error",
+ "message": "Discord tool not properly configured.",
+ }
limit = min(limit, 50)
try:
- connector = await get_discord_connector(db_session, search_space_id, user_id)
+ connector = await get_discord_connector(
+ db_session, search_space_id, user_id
+ )
if not connector:
return {"status": "error", "message": "No Discord connector found."}
@@ -51,11 +56,21 @@ def create_read_discord_messages_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Discord bot token is invalid.", "connector_type": "discord"}
+ return {
+ "status": "auth_error",
+ "message": "Discord bot token is invalid.",
+ "connector_type": "discord",
+ }
if resp.status_code == 403:
- return {"status": "error", "message": "Bot lacks permission to read this channel."}
+ return {
+ "status": "error",
+ "message": "Bot lacks permission to read this channel.",
+ }
if resp.status_code != 200:
- return {"status": "error", "message": f"Discord API error: {resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Discord API error: {resp.status_code}",
+ }
messages = [
{
@@ -67,7 +82,12 @@ def create_read_discord_messages_tool(
for m in resp.json()
]
- return {"status": "success", "channel_id": channel_id, "messages": messages, "total": len(messages)}
+ return {
+ "status": "success",
+ "channel_id": channel_id,
+ "messages": messages,
+ "total": len(messages),
+ }
except Exception as e:
from langgraph.errors import GraphInterrupt
diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/send_message.py b/surfsense_backend/app/agents/new_chat/tools/discord/send_message.py
index be4e6fdb2..236cd017a 100644
--- a/surfsense_backend/app/agents/new_chat/tools/discord/send_message.py
+++ b/surfsense_backend/app/agents/new_chat/tools/discord/send_message.py
@@ -35,13 +35,21 @@ def create_send_discord_message_tool(
- 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:
- return {"status": "error", "message": "Discord tool not properly configured."}
+ return {
+ "status": "error",
+ "message": "Discord tool not properly configured.",
+ }
if len(content) > 2000:
- return {"status": "error", "message": "Message exceeds Discord's 2000-character limit."}
+ return {
+ "status": "error",
+ "message": "Message exceeds Discord's 2000-character limit.",
+ }
try:
- connector = await get_discord_connector(db_session, search_space_id, user_id)
+ connector = await get_discord_connector(
+ db_session, search_space_id, user_id
+ )
if not connector:
return {"status": "error", "message": "No Discord connector found."}
@@ -53,7 +61,10 @@ def create_send_discord_message_tool(
)
if result.rejected:
- return {"status": "rejected", "message": "User declined. Message was not sent."}
+ return {
+ "status": "rejected",
+ "message": "User declined. Message was not sent.",
+ }
final_content = result.params.get("content", content)
final_channel = result.params.get("channel_id", channel_id)
@@ -72,11 +83,21 @@ def create_send_discord_message_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Discord bot token is invalid.", "connector_type": "discord"}
+ return {
+ "status": "auth_error",
+ "message": "Discord bot token is invalid.",
+ "connector_type": "discord",
+ }
if resp.status_code == 403:
- return {"status": "error", "message": "Bot lacks permission to send messages in this channel."}
+ return {
+ "status": "error",
+ "message": "Bot lacks permission to send messages in this channel.",
+ }
if resp.status_code not in (200, 201):
- return {"status": "error", "message": f"Discord API error: {resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Discord API error: {resp.status_code}",
+ }
msg_data = resp.json()
return {
diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py b/surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py
index 9071f129a..deec1627c 100644
--- a/surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py
+++ b/surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py
@@ -65,12 +65,22 @@ def create_read_gmail_email_tool(
detail, error = await gmail.get_message_details(message_id)
if error:
- if "re-authenticate" in error.lower() or "authentication failed" in error.lower():
- return {"status": "auth_error", "message": error, "connector_type": "gmail"}
+ if (
+ "re-authenticate" in error.lower()
+ or "authentication failed" in error.lower()
+ ):
+ return {
+ "status": "auth_error",
+ "message": error,
+ "connector_type": "gmail",
+ }
return {"status": "error", "message": error}
if not detail:
- return {"status": "not_found", "message": f"Email with ID '{message_id}' not found."}
+ return {
+ "status": "not_found",
+ "message": f"Email with ID '{message_id}' not found.",
+ }
content = gmail.format_message_to_markdown(detail)
@@ -82,6 +92,9 @@ def create_read_gmail_email_tool(
if isinstance(e, GraphInterrupt):
raise
logger.error("Error reading Gmail email: %s", e, exc_info=True)
- return {"status": "error", "message": "Failed to read email. Please try again."}
+ return {
+ "status": "error",
+ "message": "Failed to read email. Please try again.",
+ }
return read_gmail_email
diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/search_emails.py b/surfsense_backend/app/agents/new_chat/tools/gmail/search_emails.py
index de43f03d0..2e363609e 100644
--- a/surfsense_backend/app/agents/new_chat/tools/gmail/search_emails.py
+++ b/surfsense_backend/app/agents/new_chat/tools/gmail/search_emails.py
@@ -125,12 +125,24 @@ def create_search_gmail_tool(
max_results=max_results, query=query
)
if error:
- if "re-authenticate" in error.lower() or "authentication failed" in error.lower():
- return {"status": "auth_error", "message": error, "connector_type": "gmail"}
+ if (
+ "re-authenticate" in error.lower()
+ or "authentication failed" in error.lower()
+ ):
+ return {
+ "status": "auth_error",
+ "message": error,
+ "connector_type": "gmail",
+ }
return {"status": "error", "message": error}
if not messages_list:
- return {"status": "success", "emails": [], "total": 0, "message": "No emails found."}
+ return {
+ "status": "success",
+ "emails": [],
+ "total": 0,
+ "message": "No emails found.",
+ }
emails = []
for msg in messages_list:
@@ -141,16 +153,18 @@ def create_search_gmail_tool(
h["name"].lower(): h["value"]
for h in detail.get("payload", {}).get("headers", [])
}
- emails.append({
- "message_id": detail.get("id"),
- "thread_id": detail.get("threadId"),
- "subject": headers.get("subject", "No Subject"),
- "from": headers.get("from", "Unknown"),
- "to": headers.get("to", ""),
- "date": headers.get("date", ""),
- "snippet": detail.get("snippet", ""),
- "labels": detail.get("labelIds", []),
- })
+ emails.append(
+ {
+ "message_id": detail.get("id"),
+ "thread_id": detail.get("threadId"),
+ "subject": headers.get("subject", "No Subject"),
+ "from": headers.get("from", "Unknown"),
+ "to": headers.get("to", ""),
+ "date": headers.get("date", ""),
+ "snippet": detail.get("snippet", ""),
+ "labels": detail.get("labelIds", []),
+ }
+ )
return {"status": "success", "emails": emails, "total": len(emails)}
@@ -160,6 +174,9 @@ def create_search_gmail_tool(
if isinstance(e, GraphInterrupt):
raise
logger.error("Error searching Gmail: %s", e, exc_info=True)
- return {"status": "error", "message": "Failed to search Gmail. Please try again."}
+ return {
+ "status": "error",
+ "message": "Failed to search Gmail. Please try again.",
+ }
return search_gmail
diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py
index a622b0efa..dc6adb822 100644
--- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py
+++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py
@@ -39,7 +39,10 @@ def create_search_calendar_events_tool(
event_id, summary, start, end, location, attendees.
"""
if db_session is None or search_space_id is None or user_id is None:
- return {"status": "error", "message": "Calendar tool not properly configured."}
+ return {
+ "status": "error",
+ "message": "Calendar tool not properly configured.",
+ }
max_results = min(max_results, 50)
@@ -76,10 +79,22 @@ def create_search_calendar_events_tool(
)
if error:
- if "re-authenticate" in error.lower() or "authentication failed" in error.lower():
- return {"status": "auth_error", "message": error, "connector_type": "google_calendar"}
+ if (
+ "re-authenticate" in error.lower()
+ or "authentication failed" in error.lower()
+ ):
+ return {
+ "status": "auth_error",
+ "message": error,
+ "connector_type": "google_calendar",
+ }
if "no events found" in error.lower():
- return {"status": "success", "events": [], "total": 0, "message": error}
+ return {
+ "status": "success",
+ "events": [],
+ "total": 0,
+ "message": error,
+ }
return {"status": "error", "message": error}
events = []
@@ -87,19 +102,19 @@ def create_search_calendar_events_tool(
start = ev.get("start", {})
end = ev.get("end", {})
attendees_raw = ev.get("attendees", [])
- events.append({
- "event_id": ev.get("id"),
- "summary": ev.get("summary", "No Title"),
- "start": start.get("dateTime") or start.get("date", ""),
- "end": end.get("dateTime") or end.get("date", ""),
- "location": ev.get("location", ""),
- "description": ev.get("description", ""),
- "html_link": ev.get("htmlLink", ""),
- "attendees": [
- a.get("email", "") for a in attendees_raw[:10]
- ],
- "status": ev.get("status", ""),
- })
+ events.append(
+ {
+ "event_id": ev.get("id"),
+ "summary": ev.get("summary", "No Title"),
+ "start": start.get("dateTime") or start.get("date", ""),
+ "end": end.get("dateTime") or end.get("date", ""),
+ "location": ev.get("location", ""),
+ "description": ev.get("description", ""),
+ "html_link": ev.get("htmlLink", ""),
+ "attendees": [a.get("email", "") for a in attendees_raw[:10]],
+ "status": ev.get("status", ""),
+ }
+ )
return {"status": "success", "events": events, "total": len(events)}
@@ -109,6 +124,9 @@ def create_search_calendar_events_tool(
if isinstance(e, GraphInterrupt):
raise
logger.error("Error searching calendar events: %s", e, exc_info=True)
- return {"status": "error", "message": "Failed to search calendar events. Please try again."}
+ return {
+ "status": "error",
+ "message": "Failed to search calendar events. Please try again.",
+ }
return search_calendar_events
diff --git a/surfsense_backend/app/agents/new_chat/tools/hitl.py b/surfsense_backend/app/agents/new_chat/tools/hitl.py
index 89f02abf6..8480e57b1 100644
--- a/surfsense_backend/app/agents/new_chat/tools/hitl.py
+++ b/surfsense_backend/app/agents/new_chat/tools/hitl.py
@@ -130,7 +130,9 @@ def request_approval(
try:
decision_type, edited_params = _parse_decision(approval)
except ValueError:
- logger.warning("No approval decision received for %s — rejecting for safety", tool_name)
+ logger.warning(
+ "No approval decision received for %s — rejecting for safety", tool_name
+ )
return HITLResult(rejected=True, decision_type="error", params=params)
logger.info("User decision for %s: %s", tool_name, decision_type)
diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/_auth.py b/surfsense_backend/app/agents/new_chat/tools/luma/_auth.py
index 1d88161d6..37deb1525 100644
--- a/surfsense_backend/app/agents/new_chat/tools/luma/_auth.py
+++ b/surfsense_backend/app/agents/new_chat/tools/luma/_auth.py
@@ -17,7 +17,8 @@ async def get_luma_connector(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
- SearchSourceConnector.connector_type == SearchSourceConnectorType.LUMA_CONNECTOR,
+ SearchSourceConnector.connector_type
+ == SearchSourceConnectorType.LUMA_CONNECTOR,
)
)
return result.scalars().first()
diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/create_event.py b/surfsense_backend/app/agents/new_chat/tools/luma/create_event.py
index 2217d29e6..0a24a988f 100644
--- a/surfsense_backend/app/agents/new_chat/tools/luma/create_event.py
+++ b/surfsense_backend/app/agents/new_chat/tools/luma/create_event.py
@@ -62,7 +62,10 @@ def create_create_luma_event_tool(
)
if result.rejected:
- return {"status": "rejected", "message": "User declined. Event was not created."}
+ return {
+ "status": "rejected",
+ "message": "User declined. Event was not created.",
+ }
final_name = result.params.get("name", name)
final_start = result.params.get("start_at", start_at)
@@ -90,11 +93,21 @@ def create_create_luma_event_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Luma API key is invalid.", "connector_type": "luma"}
+ return {
+ "status": "auth_error",
+ "message": "Luma API key is invalid.",
+ "connector_type": "luma",
+ }
if resp.status_code == 403:
- return {"status": "error", "message": "Luma Plus subscription required to create events via API."}
+ return {
+ "status": "error",
+ "message": "Luma Plus subscription required to create events via API.",
+ }
if resp.status_code not in (200, 201):
- return {"status": "error", "message": f"Luma API error: {resp.status_code} — {resp.text[:200]}"}
+ return {
+ "status": "error",
+ "message": f"Luma API error: {resp.status_code} — {resp.text[:200]}",
+ }
data = resp.json()
event_id = data.get("api_id") or data.get("event", {}).get("api_id")
diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/list_events.py b/surfsense_backend/app/agents/new_chat/tools/luma/list_events.py
index cd4721758..aec5ad220 100644
--- a/surfsense_backend/app/agents/new_chat/tools/luma/list_events.py
+++ b/surfsense_backend/app/agents/new_chat/tools/luma/list_events.py
@@ -46,7 +46,9 @@ def create_list_luma_events_tool(
async with httpx.AsyncClient(timeout=20.0) as client:
while len(all_entries) < max_results:
- params: dict[str, Any] = {"limit": min(100, max_results - len(all_entries))}
+ params: dict[str, Any] = {
+ "limit": min(100, max_results - len(all_entries))
+ }
if cursor:
params["cursor"] = cursor
@@ -57,9 +59,16 @@ def create_list_luma_events_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Luma API key is invalid.", "connector_type": "luma"}
+ return {
+ "status": "auth_error",
+ "message": "Luma API key is invalid.",
+ "connector_type": "luma",
+ }
if resp.status_code != 200:
- return {"status": "error", "message": f"Luma API error: {resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Luma API error: {resp.status_code}",
+ }
data = resp.json()
entries = data.get("entries", [])
@@ -76,16 +85,18 @@ def create_list_luma_events_tool(
for entry in all_entries[:max_results]:
ev = entry.get("event", {})
geo = ev.get("geo_info", {})
- events.append({
- "event_id": entry.get("api_id"),
- "name": ev.get("name", "Untitled"),
- "start_at": ev.get("start_at", ""),
- "end_at": ev.get("end_at", ""),
- "timezone": ev.get("timezone", ""),
- "location": geo.get("name", ""),
- "url": ev.get("url", ""),
- "visibility": ev.get("visibility", ""),
- })
+ events.append(
+ {
+ "event_id": entry.get("api_id"),
+ "name": ev.get("name", "Untitled"),
+ "start_at": ev.get("start_at", ""),
+ "end_at": ev.get("end_at", ""),
+ "timezone": ev.get("timezone", ""),
+ "location": geo.get("name", ""),
+ "url": ev.get("url", ""),
+ "visibility": ev.get("visibility", ""),
+ }
+ )
return {"status": "success", "events": events, "total": len(events)}
diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/read_event.py b/surfsense_backend/app/agents/new_chat/tools/luma/read_event.py
index eb3ac55c6..b37a9d617 100644
--- a/surfsense_backend/app/agents/new_chat/tools/luma/read_event.py
+++ b/surfsense_backend/app/agents/new_chat/tools/luma/read_event.py
@@ -44,11 +44,21 @@ def create_read_luma_event_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Luma API key is invalid.", "connector_type": "luma"}
+ return {
+ "status": "auth_error",
+ "message": "Luma API key is invalid.",
+ "connector_type": "luma",
+ }
if resp.status_code == 404:
- return {"status": "not_found", "message": f"Event '{event_id}' not found."}
+ return {
+ "status": "not_found",
+ "message": f"Event '{event_id}' not found.",
+ }
if resp.status_code != 200:
- return {"status": "error", "message": f"Luma API error: {resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Luma API error: {resp.status_code}",
+ }
data = resp.json()
ev = data.get("event", data)
diff --git a/surfsense_backend/app/agents/new_chat/tools/mcp_client.py b/surfsense_backend/app/agents/new_chat/tools/mcp_client.py
index b46ddbcc5..e28ac8bda 100644
--- a/surfsense_backend/app/agents/new_chat/tools/mcp_client.py
+++ b/surfsense_backend/app/agents/new_chat/tools/mcp_client.py
@@ -220,10 +220,8 @@ class MCPClient:
logger.info("MCP tool '%s' succeeded: %s", tool_name, result_str[:200])
return result_str
- except asyncio.TimeoutError:
- logger.error(
- "MCP tool '%s' timed out after %.0fs", tool_name, timeout
- )
+ except TimeoutError:
+ logger.error("MCP tool '%s' timed out after %.0fs", tool_name, timeout)
return f"Error: MCP tool '{tool_name}' timed out after {timeout:.0f}s"
except RuntimeError as e:
if "Invalid structured content" in str(e):
diff --git a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py b/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py
index dfee24516..5b96ab374 100644
--- a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py
+++ b/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py
@@ -35,7 +35,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.new_chat.tools.hitl import request_approval
from app.agents.new_chat.tools.mcp_client import MCPClient
-from app.db import SearchSourceConnector, SearchSourceConnectorType
+from app.db import SearchSourceConnector
from app.services.mcp_oauth.registry import MCP_SERVICES, get_service_by_connector_type
logger = logging.getLogger(__name__)
@@ -105,13 +105,15 @@ def _create_dynamic_input_model_from_schema(
description=(
"Arguments to pass to this tool as a JSON object. "
"Infer sensible key names from the tool name and description "
- "(e.g. {\"search\": \"my query\"} for a search tool)."
+ '(e.g. {"search": "my query"} for a search tool).'
),
),
)
model_name = f"{tool_name.replace(' ', '').replace('-', '_')}Input"
- model = create_model(model_name, __config__=ConfigDict(extra="allow"), **field_definitions)
+ model = create_model(
+ model_name, __config__=ConfigDict(extra="allow"), **field_definitions
+ )
return model
@@ -187,16 +189,23 @@ async def _create_mcp_tool_from_definition_stdio(
except Exception as e:
last_error = e
if attempt < _TOOL_CALL_MAX_RETRIES - 1:
- delay = _TOOL_CALL_RETRY_DELAY * (2 ** attempt)
+ delay = _TOOL_CALL_RETRY_DELAY * (2**attempt)
logger.warning(
"MCP tool '%s' failed (attempt %d/%d): %s. Retrying in %.1fs...",
- tool_name, attempt + 1, _TOOL_CALL_MAX_RETRIES, e, delay,
+ tool_name,
+ attempt + 1,
+ _TOOL_CALL_MAX_RETRIES,
+ e,
+ delay,
)
await asyncio.sleep(delay)
else:
logger.error(
"MCP tool '%s' failed after %d attempts: %s",
- tool_name, _TOOL_CALL_MAX_RETRIES, e, exc_info=True,
+ tool_name,
+ _TOOL_CALL_MAX_RETRIES,
+ e,
+ exc_info=True,
)
return f"Error: MCP tool '{tool_name}' failed after {_TOOL_CALL_MAX_RETRIES} attempts: {last_error!s}"
@@ -318,17 +327,22 @@ async def _create_mcp_tool_from_definition_http(
try:
result_str = await _do_mcp_call(headers, call_kwargs)
- logger.debug("MCP HTTP tool '%s' succeeded (len=%d)", exposed_name, len(result_str))
+ logger.debug(
+ "MCP HTTP tool '%s' succeeded (len=%d)", exposed_name, len(result_str)
+ )
return result_str
except Exception as first_err:
if not _is_auth_error(first_err) or connector_id is None:
- logger.exception("MCP HTTP tool '%s' execution failed: %s", exposed_name, first_err)
+ logger.exception(
+ "MCP HTTP tool '%s' execution failed: %s", exposed_name, first_err
+ )
return f"Error: MCP HTTP tool '{exposed_name}' execution failed: {first_err!s}"
logger.warning(
"MCP HTTP tool '%s' got 401 — attempting token refresh for connector %s",
- exposed_name, connector_id,
+ exposed_name,
+ connector_id,
)
fresh_headers = await _force_refresh_and_get_headers(connector_id)
if fresh_headers is None:
@@ -348,7 +362,8 @@ async def _create_mcp_tool_from_definition_http(
except Exception as retry_err:
logger.exception(
"MCP HTTP tool '%s' still failing after token refresh: %s",
- exposed_name, retry_err,
+ exposed_name,
+ retry_err,
)
if _is_auth_error(retry_err):
await _mark_connector_auth_expired(connector_id)
@@ -393,7 +408,8 @@ async def _load_stdio_mcp_tools(
if not command or not isinstance(command, str):
logger.warning(
"MCP connector %d (name: '%s') missing or invalid command field, skipping",
- connector_id, connector_name,
+ connector_id,
+ connector_name,
)
return tools
@@ -401,7 +417,8 @@ async def _load_stdio_mcp_tools(
if not isinstance(args, list):
logger.warning(
"MCP connector %d (name: '%s') has invalid args field (must be list), skipping",
- connector_id, connector_name,
+ connector_id,
+ connector_name,
)
return tools
@@ -409,7 +426,8 @@ async def _load_stdio_mcp_tools(
if not isinstance(env, dict):
logger.warning(
"MCP connector %d (name: '%s') has invalid env field (must be dict), skipping",
- connector_id, connector_name,
+ connector_id,
+ connector_name,
)
return tools
@@ -420,7 +438,9 @@ async def _load_stdio_mcp_tools(
logger.info(
"Discovered %d tools from stdio MCP server '%s' (connector %d)",
- len(tool_definitions), command, connector_id,
+ len(tool_definitions),
+ command,
+ connector_id,
)
for tool_def in tool_definitions:
@@ -436,7 +456,9 @@ async def _load_stdio_mcp_tools(
except Exception as e:
logger.exception(
"Failed to create tool '%s' from connector %d: %s",
- tool_def.get("name"), connector_id, e,
+ tool_def.get("name"),
+ connector_id,
+ e,
)
return tools
@@ -468,7 +490,8 @@ async def _load_http_mcp_tools(
if not url or not isinstance(url, str):
logger.warning(
"MCP connector %d (name: '%s') missing or invalid url field, skipping",
- connector_id, connector_name,
+ connector_id,
+ connector_name,
)
return tools
@@ -476,7 +499,8 @@ async def _load_http_mcp_tools(
if not isinstance(headers, dict):
logger.warning(
"MCP connector %d (name: '%s') has invalid headers field (must be dict), skipping",
- connector_id, connector_name,
+ connector_id,
+ connector_name,
)
return tools
@@ -507,7 +531,9 @@ async def _load_http_mcp_tools(
if not _is_auth_error(first_err) or connector_id is None:
logger.exception(
"Failed to connect to HTTP MCP server at '%s' (connector %d): %s",
- url, connector_id, first_err,
+ url,
+ connector_id,
+ first_err,
)
return tools
@@ -534,7 +560,8 @@ async def _load_http_mcp_tools(
except Exception as retry_err:
logger.exception(
"HTTP MCP discovery for connector %d still failing after refresh: %s",
- connector_id, retry_err,
+ connector_id,
+ retry_err,
)
if _is_auth_error(retry_err):
await _mark_connector_auth_expired(connector_id)
@@ -543,17 +570,20 @@ async def _load_http_mcp_tools(
total_discovered = len(tool_definitions)
if allowed_set:
- tool_definitions = [
- td for td in tool_definitions if td["name"] in allowed_set
- ]
+ tool_definitions = [td for td in tool_definitions if td["name"] in allowed_set]
logger.info(
"HTTP MCP server '%s' (connector %d): %d/%d tools after allowlist filter",
- url, connector_id, len(tool_definitions), total_discovered,
+ url,
+ connector_id,
+ len(tool_definitions),
+ total_discovered,
)
else:
logger.info(
"Discovered %d tools from HTTP MCP server '%s' (connector %d) — no allowlist, loading all",
- total_discovered, url, connector_id,
+ total_discovered,
+ url,
+ connector_id,
)
for tool_def in tool_definitions:
@@ -573,7 +603,9 @@ async def _load_http_mcp_tools(
except Exception as e:
logger.exception(
"Failed to create HTTP tool '%s' from connector %d: %s",
- tool_def.get("name"), connector_id, e,
+ tool_def.get("name"),
+ connector_id,
+ e,
)
return tools
@@ -628,7 +660,7 @@ def _inject_oauth_headers(
async def _refresh_connector_token(
session: AsyncSession,
- connector: "SearchSourceConnector",
+ connector: SearchSourceConnector,
) -> str | None:
"""Refresh the OAuth token for an MCP connector and persist the result.
@@ -692,12 +724,8 @@ async def _refresh_connector_token(
updated_oauth = dict(mcp_oauth)
updated_oauth["access_token"] = enc.encrypt_token(new_access)
if token_json.get("refresh_token"):
- updated_oauth["refresh_token"] = enc.encrypt_token(
- token_json["refresh_token"]
- )
- updated_oauth["expires_at"] = (
- new_expires_at.isoformat() if new_expires_at else None
- )
+ updated_oauth["refresh_token"] = enc.encrypt_token(token_json["refresh_token"])
+ updated_oauth["expires_at"] = new_expires_at.isoformat() if new_expires_at else None
updated_cfg = {**cfg, "mcp_oauth": updated_oauth}
updated_cfg.pop("auth_expired", None)
@@ -713,7 +741,7 @@ async def _refresh_connector_token(
async def _maybe_refresh_mcp_oauth_token(
session: AsyncSession,
- connector: "SearchSourceConnector",
+ connector: SearchSourceConnector,
cfg: dict[str, Any],
server_config: dict[str, Any],
) -> dict[str, Any]:
@@ -731,10 +759,11 @@ async def _maybe_refresh_mcp_oauth_token(
try:
expires_at = datetime.fromisoformat(expires_at_str)
if expires_at.tzinfo is None:
- from datetime import timezone
- expires_at = expires_at.replace(tzinfo=timezone.utc)
+ expires_at = expires_at.replace(tzinfo=UTC)
- if datetime.now(UTC) < expires_at - timedelta(seconds=_TOKEN_REFRESH_BUFFER_SECONDS):
+ if datetime.now(UTC) < expires_at - timedelta(
+ seconds=_TOKEN_REFRESH_BUFFER_SECONDS
+ ):
return server_config
except (ValueError, TypeError):
return server_config
@@ -744,7 +773,9 @@ async def _maybe_refresh_mcp_oauth_token(
if not new_access:
return server_config
- logger.info("Proactively refreshed MCP OAuth token for connector %s", connector.id)
+ logger.info(
+ "Proactively refreshed MCP OAuth token for connector %s", connector.id
+ )
refreshed_config = dict(server_config)
refreshed_config["headers"] = {
@@ -920,7 +951,7 @@ async def load_mcp_tools(
result = await session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
- cast(SearchSourceConnector.config, JSONB).has_key("server_config"), # noqa: W601
+ cast(SearchSourceConnector.config, JSONB).has_key("server_config"),
),
)
@@ -956,13 +987,17 @@ async def load_mcp_tools(
if not server_config or not isinstance(server_config, dict):
logger.warning(
"MCP connector %d (name: '%s') has invalid or missing server_config, skipping",
- connector.id, connector.name,
+ connector.id,
+ connector.name,
)
continue
if cfg.get("mcp_oauth"):
server_config = await _maybe_refresh_mcp_oauth_token(
- session, connector, cfg, server_config,
+ session,
+ connector,
+ cfg,
+ server_config,
)
cfg = connector.config or {}
server_config = _inject_oauth_headers(cfg, server_config)
@@ -995,22 +1030,25 @@ async def load_mcp_tools(
if service_key:
tool_name_prefix = f"{service_key}_{connector.id}"
- discovery_tasks.append({
- "connector_id": connector.id,
- "connector_name": connector.name,
- "server_config": server_config,
- "trusted_tools": trusted_tools,
- "allowed_tools": allowed_tools,
- "readonly_tools": readonly_tools,
- "tool_name_prefix": tool_name_prefix,
- "transport": server_config.get("transport", "stdio"),
- "is_generic_mcp": svc_cfg is None,
- })
+ discovery_tasks.append(
+ {
+ "connector_id": connector.id,
+ "connector_name": connector.name,
+ "server_config": server_config,
+ "trusted_tools": trusted_tools,
+ "allowed_tools": allowed_tools,
+ "readonly_tools": readonly_tools,
+ "tool_name_prefix": tool_name_prefix,
+ "transport": server_config.get("transport", "stdio"),
+ "is_generic_mcp": svc_cfg is None,
+ }
+ )
except Exception as e:
logger.exception(
"Failed to prepare MCP connector %d: %s",
- connector.id, e,
+ connector.id,
+ e,
)
async def _discover_one(task: dict[str, Any]) -> list[StructuredTool]:
@@ -1039,23 +1077,23 @@ async def load_mcp_tools(
),
timeout=_MCP_DISCOVERY_TIMEOUT_SECONDS,
)
- except asyncio.TimeoutError:
+ except TimeoutError:
logger.error(
"MCP connector %d timed out after %ds during discovery",
- task["connector_id"], _MCP_DISCOVERY_TIMEOUT_SECONDS,
+ task["connector_id"],
+ _MCP_DISCOVERY_TIMEOUT_SECONDS,
)
return []
except Exception as e:
logger.exception(
"Failed to load tools from MCP connector %d: %s",
- task["connector_id"], e,
+ task["connector_id"],
+ e,
)
return []
results = await asyncio.gather(*[_discover_one(t) for t in discovery_tasks])
- tools: list[StructuredTool] = [
- tool for sublist in results for tool in sublist
- ]
+ tools: list[StructuredTool] = [tool for sublist in results for tool in sublist]
_mcp_tools_cache[search_space_id] = (now, tools)
@@ -1063,7 +1101,9 @@ async def load_mcp_tools(
oldest_key = min(_mcp_tools_cache, key=lambda k: _mcp_tools_cache[k][0])
del _mcp_tools_cache[oldest_key]
- logger.info("Loaded %d MCP tools for search space %d", len(tools), search_space_id)
+ logger.info(
+ "Loaded %d MCP tools for search space %d", len(tools), search_space_id
+ )
return tools
except Exception as e:
diff --git a/surfsense_backend/app/agents/new_chat/tools/registry.py b/surfsense_backend/app/agents/new_chat/tools/registry.py
index 85c89b114..3ac8677b9 100644
--- a/surfsense_backend/app/agents/new_chat/tools/registry.py
+++ b/surfsense_backend/app/agents/new_chat/tools/registry.py
@@ -50,6 +50,7 @@ from .confluence import (
create_delete_confluence_page_tool,
create_update_confluence_page_tool,
)
+from .connected_accounts import create_get_connected_accounts_tool
from .discord import (
create_list_discord_channels_tool,
create_read_discord_messages_tool,
@@ -78,7 +79,6 @@ from .google_drive import (
create_create_google_drive_file_tool,
create_delete_google_drive_file_tool,
)
-from .connected_accounts import create_get_connected_accounts_tool
from .luma import (
create_create_luma_event_tool,
create_list_luma_events_tool,
@@ -675,10 +675,7 @@ def get_connector_gated_tools(
available_connectors: list[str] | None,
) -> list[str]:
"""Return tool names to disable"""
- if available_connectors is None:
- available = set()
- else:
- available = set(available_connectors)
+ available = set() if available_connectors is None else set(available_connectors)
disabled: list[str] = []
for tool_def in BUILTIN_TOOLS:
@@ -829,14 +826,16 @@ async def build_tools_async(
tools.extend(mcp_tools)
logging.info(
"Registered %d MCP tools: %s",
- len(mcp_tools), [t.name for t in mcp_tools],
+ len(mcp_tools),
+ [t.name for t in mcp_tools],
)
except Exception as e:
logging.exception("Failed to load MCP tools: %s", e)
logging.info(
"Total tools for agent: %d — %s",
- len(tools), [t.name for t in tools],
+ len(tools),
+ [t.name for t in tools],
)
return tools
diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/_auth.py b/surfsense_backend/app/agents/new_chat/tools/teams/_auth.py
index f24f5502e..4345bb476 100644
--- a/surfsense_backend/app/agents/new_chat/tools/teams/_auth.py
+++ b/surfsense_backend/app/agents/new_chat/tools/teams/_auth.py
@@ -17,7 +17,8 @@ async def get_teams_connector(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == search_space_id,
SearchSourceConnector.user_id == user_id,
- SearchSourceConnector.connector_type == SearchSourceConnectorType.TEAMS_CONNECTOR,
+ SearchSourceConnector.connector_type
+ == SearchSourceConnectorType.TEAMS_CONNECTOR,
)
)
return result.scalars().first()
diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/list_channels.py b/surfsense_backend/app/agents/new_chat/tools/teams/list_channels.py
index a676595c1..d7b000853 100644
--- a/surfsense_backend/app/agents/new_chat/tools/teams/list_channels.py
+++ b/surfsense_backend/app/agents/new_chat/tools/teams/list_channels.py
@@ -35,12 +35,21 @@ def create_list_teams_channels_tool(
headers = {"Authorization": f"Bearer {token}"}
async with httpx.AsyncClient(timeout=20.0) as client:
- teams_resp = await client.get(f"{GRAPH_API}/me/joinedTeams", headers=headers)
+ teams_resp = await client.get(
+ f"{GRAPH_API}/me/joinedTeams", headers=headers
+ )
if teams_resp.status_code == 401:
- return {"status": "auth_error", "message": "Teams token expired. Please re-authenticate.", "connector_type": "teams"}
+ return {
+ "status": "auth_error",
+ "message": "Teams token expired. Please re-authenticate.",
+ "connector_type": "teams",
+ }
if teams_resp.status_code != 200:
- return {"status": "error", "message": f"Graph API error: {teams_resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Graph API error: {teams_resp.status_code}",
+ }
teams_data = teams_resp.json().get("value", [])
result_teams = []
@@ -58,13 +67,19 @@ def create_list_teams_channels_tool(
{"id": ch["id"], "name": ch.get("displayName", "")}
for ch in ch_resp.json().get("value", [])
]
- result_teams.append({
- "team_id": team_id,
- "team_name": team.get("displayName", ""),
- "channels": channels,
- })
+ result_teams.append(
+ {
+ "team_id": team_id,
+ "team_name": team.get("displayName", ""),
+ "channels": channels,
+ }
+ )
- return {"status": "success", "teams": result_teams, "total_teams": len(result_teams)}
+ return {
+ "status": "success",
+ "teams": result_teams,
+ "total_teams": len(result_teams),
+ }
except Exception as e:
from langgraph.errors import GraphInterrupt
diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/read_messages.py b/surfsense_backend/app/agents/new_chat/tools/teams/read_messages.py
index 90896cb95..d24a7e4d3 100644
--- a/surfsense_backend/app/agents/new_chat/tools/teams/read_messages.py
+++ b/surfsense_backend/app/agents/new_chat/tools/teams/read_messages.py
@@ -52,11 +52,21 @@ def create_read_teams_messages_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Teams token expired. Please re-authenticate.", "connector_type": "teams"}
+ return {
+ "status": "auth_error",
+ "message": "Teams token expired. Please re-authenticate.",
+ "connector_type": "teams",
+ }
if resp.status_code == 403:
- return {"status": "error", "message": "Insufficient permissions to read this channel."}
+ return {
+ "status": "error",
+ "message": "Insufficient permissions to read this channel.",
+ }
if resp.status_code != 200:
- return {"status": "error", "message": f"Graph API error: {resp.status_code}"}
+ return {
+ "status": "error",
+ "message": f"Graph API error: {resp.status_code}",
+ }
raw_msgs = resp.json().get("value", [])
messages = []
@@ -64,13 +74,15 @@ def create_read_teams_messages_tool(
sender = m.get("from", {})
user_info = sender.get("user", {}) if sender else {}
body = m.get("body", {})
- messages.append({
- "id": m.get("id"),
- "sender": user_info.get("displayName", "Unknown"),
- "content": body.get("content", ""),
- "content_type": body.get("contentType", "text"),
- "timestamp": m.get("createdDateTime", ""),
- })
+ messages.append(
+ {
+ "id": m.get("id"),
+ "sender": user_info.get("displayName", "Unknown"),
+ "content": body.get("content", ""),
+ "content_type": body.get("contentType", "text"),
+ "timestamp": m.get("createdDateTime", ""),
+ }
+ )
return {
"status": "success",
diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/send_message.py b/surfsense_backend/app/agents/new_chat/tools/teams/send_message.py
index ba3a515d9..fd8d00870 100644
--- a/surfsense_backend/app/agents/new_chat/tools/teams/send_message.py
+++ b/surfsense_backend/app/agents/new_chat/tools/teams/send_message.py
@@ -50,12 +50,19 @@ def create_send_teams_message_tool(
result = request_approval(
action_type="teams_send_message",
tool_name="send_teams_message",
- params={"team_id": team_id, "channel_id": channel_id, "content": content},
+ params={
+ "team_id": team_id,
+ "channel_id": channel_id,
+ "content": content,
+ },
context={"connector_id": connector.id},
)
if result.rejected:
- return {"status": "rejected", "message": "User declined. Message was not sent."}
+ return {
+ "status": "rejected",
+ "message": "User declined. Message was not sent.",
+ }
final_content = result.params.get("content", content)
final_team = result.params.get("team_id", team_id)
@@ -74,20 +81,27 @@ def create_send_teams_message_tool(
)
if resp.status_code == 401:
- return {"status": "auth_error", "message": "Teams token expired. Please re-authenticate.", "connector_type": "teams"}
+ return {
+ "status": "auth_error",
+ "message": "Teams token expired. Please re-authenticate.",
+ "connector_type": "teams",
+ }
if resp.status_code == 403:
return {
"status": "insufficient_permissions",
"message": "Missing ChannelMessage.Send permission. Please re-authenticate with updated scopes.",
}
if resp.status_code not in (200, 201):
- return {"status": "error", "message": f"Graph API error: {resp.status_code} — {resp.text[:200]}"}
+ return {
+ "status": "error",
+ "message": f"Graph API error: {resp.status_code} — {resp.text[:200]}",
+ }
msg_data = resp.json()
return {
"status": "success",
"message_id": msg_data.get("id"),
- "message": f"Message sent to Teams channel.",
+ "message": "Message sent to Teams channel.",
}
except Exception as e:
diff --git a/surfsense_backend/app/agents/new_chat/tools/tool_response.py b/surfsense_backend/app/agents/new_chat/tools/tool_response.py
index 5fb1864b7..8644ada5c 100644
--- a/surfsense_backend/app/agents/new_chat/tools/tool_response.py
+++ b/surfsense_backend/app/agents/new_chat/tools/tool_response.py
@@ -6,7 +6,6 @@ from typing import Any
class ToolResponse:
-
@staticmethod
def success(message: str, **data: Any) -> dict[str, Any]:
return {"status": "success", "message": message, **data}
@@ -31,9 +30,7 @@ class ToolResponse:
return {"status": "rejected", "message": message}
@staticmethod
- def not_found(
- resource: str, identifier: str, **data: Any
- ) -> dict[str, Any]:
+ def not_found(resource: str, identifier: str, **data: Any) -> dict[str, Any]:
return {
"status": "not_found",
"error": f"{resource} '{identifier}' was not found.",
diff --git a/surfsense_backend/app/connectors/exceptions.py b/surfsense_backend/app/connectors/exceptions.py
index 32a1e7bdc..027adbb87 100644
--- a/surfsense_backend/app/connectors/exceptions.py
+++ b/surfsense_backend/app/connectors/exceptions.py
@@ -13,7 +13,6 @@ from typing import Any
class ConnectorError(Exception):
-
def __init__(
self,
message: str,
diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py
index 8df930f30..de4e05423 100644
--- a/surfsense_backend/app/routes/__init__.py
+++ b/surfsense_backend/app/routes/__init__.py
@@ -98,7 +98,9 @@ router.include_router(logs_router)
router.include_router(circleback_webhook_router) # Circleback meeting webhooks
router.include_router(surfsense_docs_router) # Surfsense documentation for citations
router.include_router(notifications_router) # Notifications with Zero sync
-router.include_router(mcp_oauth_router) # MCP OAuth 2.1 for Linear, Jira, ClickUp, Slack, Airtable
+router.include_router(
+ mcp_oauth_router
+) # MCP OAuth 2.1 for Linear, Jira, ClickUp, Slack, Airtable
router.include_router(composio_router) # Composio OAuth and toolkit management
router.include_router(public_chat_router) # Public chat sharing and cloning
router.include_router(incentive_tasks_router) # Incentive tasks for earning free pages
diff --git a/surfsense_backend/app/routes/mcp_oauth_route.py b/surfsense_backend/app/routes/mcp_oauth_route.py
index e14be83d0..1abc1f1ec 100644
--- a/surfsense_backend/app/routes/mcp_oauth_route.py
+++ b/surfsense_backend/app/routes/mcp_oauth_route.py
@@ -29,7 +29,11 @@ from app.db import (
)
from app.users import current_active_user
from app.utils.connector_naming import generate_unique_connector_name
-from app.utils.oauth_security import OAuthStateManager, TokenEncryption, generate_pkce_pair
+from app.utils.oauth_security import (
+ OAuthStateManager,
+ TokenEncryption,
+ generate_pkce_pair,
+)
logger = logging.getLogger(__name__)
@@ -37,7 +41,9 @@ router = APIRouter()
async def _fetch_account_metadata(
- service_key: str, access_token: str, token_json: dict[str, Any],
+ service_key: str,
+ access_token: str,
+ token_json: dict[str, Any],
) -> dict[str, Any]:
"""Fetch display-friendly account metadata after a successful token exchange.
@@ -86,7 +92,8 @@ async def _fetch_account_metadata(
meta["display_name"] = whoami.get("email", "Airtable")
else:
logger.warning(
- "Airtable whoami API returned %d (non-blocking)", resp.status_code,
+ "Airtable whoami API returned %d (non-blocking)",
+ resp.status_code,
)
except Exception:
@@ -98,6 +105,7 @@ async def _fetch_account_metadata(
return meta
+
_state_manager: OAuthStateManager | None = None
_token_encryption: TokenEncryption | None = None
@@ -151,6 +159,7 @@ def _frontend_redirect(
# /add — start MCP OAuth flow
# ---------------------------------------------------------------------------
+
@router.get("/auth/mcp/{service}/connector/add")
async def connect_mcp_service(
service: str,
@@ -170,9 +179,12 @@ async def connect_mcp_service(
)
metadata = await discover_oauth_metadata(
- svc.mcp_url, origin_override=svc.oauth_discovery_origin,
+ svc.mcp_url,
+ origin_override=svc.oauth_discovery_origin,
+ )
+ auth_endpoint = svc.auth_endpoint_override or metadata.get(
+ "authorization_endpoint"
)
- auth_endpoint = svc.auth_endpoint_override or metadata.get("authorization_endpoint")
token_endpoint = svc.token_endpoint_override or metadata.get("token_endpoint")
registration_endpoint = metadata.get("registration_endpoint")
@@ -236,7 +248,9 @@ async def connect_mcp_service(
logger.info(
"Generated %s MCP OAuth URL for user %s, space %s",
- svc.name, user.id, space_id,
+ svc.name,
+ user.id,
+ space_id,
)
return {"auth_url": auth_url}
@@ -245,7 +259,8 @@ async def connect_mcp_service(
except Exception as e:
logger.error("Failed to initiate %s MCP OAuth: %s", service, e, exc_info=True)
raise HTTPException(
- status_code=500, detail=f"Failed to initiate {service} MCP OAuth.",
+ status_code=500,
+ detail=f"Failed to initiate {service} MCP OAuth.",
) from e
@@ -253,6 +268,7 @@ async def connect_mcp_service(
# /callback — handle OAuth redirect
# ---------------------------------------------------------------------------
+
@router.get("/auth/mcp/{service}/connector/callback")
async def mcp_oauth_callback(
service: str,
@@ -271,7 +287,9 @@ async def mcp_oauth_callback(
except Exception:
pass
return _frontend_redirect(
- space_id, error=f"{service}_mcp_oauth_denied", service=service,
+ space_id,
+ error=f"{service}_mcp_oauth_denied",
+ service=service,
)
if not code:
@@ -337,9 +355,7 @@ async def mcp_oauth_callback(
expires_at = None
if expires_in:
- expires_at = datetime.now(UTC) + timedelta(
- seconds=int(expires_in)
- )
+ expires_at = datetime.now(UTC) + timedelta(seconds=int(expires_in))
connector_config = {
"server_config": {
@@ -349,10 +365,14 @@ async def mcp_oauth_callback(
"mcp_service": svc_key,
"mcp_oauth": {
"client_id": client_id,
- "client_secret": enc.encrypt_token(client_secret) if client_secret else "",
+ "client_secret": enc.encrypt_token(client_secret)
+ if client_secret
+ else "",
"token_endpoint": token_endpoint,
"access_token": enc.encrypt_token(access_token),
- "refresh_token": enc.encrypt_token(refresh_token) if refresh_token else None,
+ "refresh_token": enc.encrypt_token(refresh_token)
+ if refresh_token
+ else None,
"expires_at": expires_at.isoformat() if expires_at else None,
"scope": scope,
},
@@ -361,15 +381,27 @@ async def mcp_oauth_callback(
account_meta = await _fetch_account_metadata(svc_key, access_token, token_json)
if account_meta:
- _SAFE_META_KEYS = {"display_name", "team_id", "team_name", "user_id", "user_email",
- "workspace_id", "workspace_name", "organization_name",
- "organization_url_key", "cloud_id", "site_name", "base_url"}
+ safe_meta_keys = {
+ "display_name",
+ "team_id",
+ "team_name",
+ "user_id",
+ "user_email",
+ "workspace_id",
+ "workspace_name",
+ "organization_name",
+ "organization_url_key",
+ "cloud_id",
+ "site_name",
+ "base_url",
+ }
for k, v in account_meta.items():
- if k in _SAFE_META_KEYS:
+ if k in safe_meta_keys:
connector_config[k] = v
logger.info(
"Stored account metadata for %s: display_name=%s",
- svc_key, account_meta.get("display_name", ""),
+ svc_key,
+ account_meta.get("display_name", ""),
)
# ---- Re-auth path ----
@@ -400,15 +432,24 @@ async def mcp_oauth_callback(
logger.info(
"Re-authenticated %s MCP connector %s for user %s",
- svc.name, db_connector.id, user_id,
+ svc.name,
+ db_connector.id,
+ user_id,
)
reauth_return_url = data.get("return_url")
- if reauth_return_url and reauth_return_url.startswith("/") and not reauth_return_url.startswith("//"):
+ if (
+ reauth_return_url
+ and reauth_return_url.startswith("/")
+ and not reauth_return_url.startswith("//")
+ ):
return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}{reauth_return_url}"
)
return _frontend_redirect(
- space_id, success=True, connector_id=db_connector.id, service=service,
+ space_id,
+ success=True,
+ connector_id=db_connector.id,
+ service=service,
)
# ---- New connector path ----
@@ -436,24 +477,34 @@ async def mcp_oauth_callback(
except IntegrityError as e:
await session.rollback()
raise HTTPException(
- status_code=409, detail="A connector for this service already exists.",
+ status_code=409,
+ detail="A connector for this service already exists.",
) from e
_invalidate_cache(space_id)
logger.info(
"Created %s MCP connector %s for user %s in space %s",
- svc.name, new_connector.id, user_id, space_id,
+ svc.name,
+ new_connector.id,
+ user_id,
+ space_id,
)
return _frontend_redirect(
- space_id, success=True, connector_id=new_connector.id, service=service,
+ space_id,
+ success=True,
+ connector_id=new_connector.id,
+ service=service,
)
except HTTPException:
raise
except Exception as e:
logger.error(
- "Failed to complete %s MCP OAuth: %s", service, e, exc_info=True,
+ "Failed to complete %s MCP OAuth: %s",
+ service,
+ e,
+ exc_info=True,
)
raise HTTPException(
status_code=500,
@@ -465,6 +516,7 @@ async def mcp_oauth_callback(
# /reauth — re-authenticate an existing MCP connector
# ---------------------------------------------------------------------------
+
@router.get("/auth/mcp/{service}/connector/reauth")
async def reauth_mcp_service(
service: str,
@@ -491,7 +543,8 @@ async def reauth_mcp_service(
)
if not result.scalars().first():
raise HTTPException(
- status_code=404, detail="Connector not found or access denied",
+ status_code=404,
+ detail="Connector not found or access denied",
)
try:
@@ -501,9 +554,12 @@ async def reauth_mcp_service(
)
metadata = await discover_oauth_metadata(
- svc.mcp_url, origin_override=svc.oauth_discovery_origin,
+ svc.mcp_url,
+ origin_override=svc.oauth_discovery_origin,
+ )
+ auth_endpoint = svc.auth_endpoint_override or metadata.get(
+ "authorization_endpoint"
)
- auth_endpoint = svc.auth_endpoint_override or metadata.get("authorization_endpoint")
token_endpoint = svc.token_endpoint_override or metadata.get("token_endpoint")
registration_endpoint = metadata.get("registration_endpoint")
@@ -545,7 +601,9 @@ async def reauth_mcp_service(
"service": service,
"code_verifier": verifier,
"mcp_client_id": client_id,
- "mcp_client_secret": enc.encrypt_token(client_secret) if client_secret else "",
+ "mcp_client_secret": enc.encrypt_token(client_secret)
+ if client_secret
+ else "",
"mcp_token_endpoint": token_endpoint,
"mcp_url": svc.mcp_url,
"connector_id": connector_id,
@@ -554,7 +612,9 @@ async def reauth_mcp_service(
extra["return_url"] = return_url
state = _get_state_manager().generate_secure_state(
- space_id, user.id, **extra,
+ space_id,
+ user.id,
+ **extra,
)
auth_params: dict[str, str] = {
@@ -572,7 +632,9 @@ async def reauth_mcp_service(
logger.info(
"Initiating %s MCP re-auth for user %s, connector %s",
- svc.name, user.id, connector_id,
+ svc.name,
+ user.id,
+ connector_id,
)
return {"auth_url": auth_url}
@@ -580,7 +642,10 @@ async def reauth_mcp_service(
raise
except Exception as e:
logger.error(
- "Failed to initiate %s MCP re-auth: %s", service, e, exc_info=True,
+ "Failed to initiate %s MCP re-auth: %s",
+ service,
+ e,
+ exc_info=True,
)
raise HTTPException(
status_code=500,
@@ -592,6 +657,7 @@ async def reauth_mcp_service(
# Helpers
# ---------------------------------------------------------------------------
+
def _invalidate_cache(space_id: int) -> None:
try:
from app.agents.new_chat.tools.mcp_tool import invalidate_mcp_tools_cache
diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py
index 85a8658ec..091e87737 100644
--- a/surfsense_backend/app/routes/new_chat_routes.py
+++ b/surfsense_backend/app/routes/new_chat_routes.py
@@ -24,9 +24,9 @@ from sqlalchemy.orm import selectinload
from app.agents.new_chat.filesystem_selection import (
ClientPlatform,
- LocalFilesystemMount,
FilesystemMode,
FilesystemSelection,
+ LocalFilesystemMount,
)
from app.config import config
from app.db import (
diff --git a/surfsense_backend/app/routes/oauth_connector_base.py b/surfsense_backend/app/routes/oauth_connector_base.py
index 0638e8f34..5b75d8519 100644
--- a/surfsense_backend/app/routes/oauth_connector_base.py
+++ b/surfsense_backend/app/routes/oauth_connector_base.py
@@ -9,6 +9,7 @@ Call ``build_router()`` to get a FastAPI ``APIRouter`` with ``/connector/add``,
from __future__ import annotations
import base64
+import contextlib
import logging
from datetime import UTC, datetime, timedelta
from typing import Any
@@ -41,7 +42,6 @@ logger = logging.getLogger(__name__)
class OAuthConnectorRoute:
-
def __init__(
self,
*,
@@ -244,10 +244,8 @@ class OAuthConnectorRoute:
if resp.status_code != 200:
detail = resp.text
- try:
+ with contextlib.suppress(Exception):
detail = resp.json().get("error_description", detail)
- except Exception:
- pass
raise HTTPException(
status_code=400, detail=f"Token exchange failed: {detail}"
)
@@ -430,7 +428,11 @@ class OAuthConnectorRoute:
state_mgr = oauth._get_state_manager()
extra: dict[str, Any] = {"connector_id": connector_id}
- if return_url and return_url.startswith("/") and not return_url.startswith("//"):
+ if (
+ return_url
+ and return_url.startswith("/")
+ and not return_url.startswith("//")
+ ):
extra["return_url"] = return_url
auth_params: dict[str, str] = {
@@ -450,9 +452,7 @@ class OAuthConnectorRoute:
auth_params.update(oauth.extra_auth_params)
- state_encoded = state_mgr.generate_secure_state(
- space_id, user.id, **extra
- )
+ state_encoded = state_mgr.generate_secure_state(space_id, user.id, **extra)
auth_params["state"] = state_encoded
auth_url = f"{oauth.authorize_url}?{urlencode(auth_params)}"
@@ -489,9 +489,7 @@ class OAuthConnectorRoute:
status_code=400, detail="Missing authorization code"
)
if not state:
- raise HTTPException(
- status_code=400, detail="Missing state parameter"
- )
+ raise HTTPException(status_code=400, detail="Missing state parameter")
state_mgr = oauth._get_state_manager()
try:
@@ -552,7 +550,11 @@ class OAuthConnectorRoute:
db_connector.id,
user_id,
)
- if reauth_return_url and reauth_return_url.startswith("/") and not reauth_return_url.startswith("//"):
+ if (
+ reauth_return_url
+ and reauth_return_url.startswith("/")
+ and not reauth_return_url.startswith("//")
+ ):
return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}{reauth_return_url}"
)
@@ -603,7 +605,8 @@ class OAuthConnectorRoute:
except IntegrityError as e:
await session.rollback()
raise HTTPException(
- status_code=409, detail="A connector for this service already exists."
+ status_code=409,
+ detail="A connector for this service already exists.",
) from e
logger.info(
diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py
index d42a7fa1a..9037d275a 100644
--- a/surfsense_backend/app/routes/search_source_connectors_routes.py
+++ b/surfsense_backend/app/routes/search_source_connectors_routes.py
@@ -3092,7 +3092,7 @@ async def trust_mcp_tool(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == connector_id,
SearchSourceConnector.user_id == user.id,
- cast(SearchSourceConnector.config, PG_JSONB).has_key("server_config"), # noqa: W601
+ cast(SearchSourceConnector.config, PG_JSONB).has_key("server_config"),
)
)
connector = result.scalars().first()
@@ -3147,7 +3147,7 @@ async def untrust_mcp_tool(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == connector_id,
SearchSourceConnector.user_id == user.id,
- cast(SearchSourceConnector.config, PG_JSONB).has_key("server_config"), # noqa: W601
+ cast(SearchSourceConnector.config, PG_JSONB).has_key("server_config"),
)
)
connector = result.scalars().first()
diff --git a/surfsense_backend/app/services/mcp_oauth/discovery.py b/surfsense_backend/app/services/mcp_oauth/discovery.py
index b0f3fef2a..dc21443bc 100644
--- a/surfsense_backend/app/services/mcp_oauth/discovery.py
+++ b/surfsense_backend/app/services/mcp_oauth/discovery.py
@@ -55,7 +55,9 @@ async def register_client(
async with httpx.AsyncClient(follow_redirects=True) as client:
resp = await client.post(
- registration_endpoint, json=payload, timeout=timeout,
+ registration_endpoint,
+ json=payload,
+ timeout=timeout,
)
resp.raise_for_status()
return resp.json()
diff --git a/surfsense_backend/app/services/mcp_oauth/registry.py b/surfsense_backend/app/services/mcp_oauth/registry.py
index 49bc74d3d..835d70184 100644
--- a/surfsense_backend/app/services/mcp_oauth/registry.py
+++ b/surfsense_backend/app/services/mcp_oauth/registry.py
@@ -70,12 +70,14 @@ MCP_SERVICES: dict[str, MCPServiceConfig] = {
"createJiraIssue",
"editJiraIssue",
],
- readonly_tools=frozenset({
- "getAccessibleAtlassianResources",
- "searchJiraIssuesUsingJql",
- "getVisibleJiraProjects",
- "getJiraProjectIssueTypesMetadata",
- }),
+ readonly_tools=frozenset(
+ {
+ "getAccessibleAtlassianResources",
+ "searchJiraIssuesUsingJql",
+ "getVisibleJiraProjects",
+ "getJiraProjectIssueTypesMetadata",
+ }
+ ),
account_metadata_keys=["cloud_id", "site_name", "base_url"],
),
"clickup": MCPServiceConfig(
@@ -99,15 +101,23 @@ MCP_SERVICES: dict[str, MCPServiceConfig] = {
auth_endpoint_override="https://slack.com/oauth/v2_user/authorize",
token_endpoint_override="https://slack.com/api/oauth.v2.user.access",
scopes=[
- "search:read.public", "search:read.private", "search:read.mpim", "search:read.im",
- "channels:history", "groups:history", "mpim:history", "im:history",
+ "search:read.public",
+ "search:read.private",
+ "search:read.mpim",
+ "search:read.im",
+ "channels:history",
+ "groups:history",
+ "mpim:history",
+ "im:history",
],
allowed_tools=[
"slack_search_channels",
"slack_read_channel",
"slack_read_thread",
],
- readonly_tools=frozenset({"slack_search_channels", "slack_read_channel", "slack_read_thread"}),
+ readonly_tools=frozenset(
+ {"slack_search_channels", "slack_read_channel", "slack_read_thread"}
+ ),
# TODO: oauth.v2.user.access only returns team.id, not team.name.
# To populate team_name, either add "team:read" scope and call
# GET /api/team.info during OAuth callback, or switch to oauth.v2.access.
@@ -127,7 +137,9 @@ MCP_SERVICES: dict[str, MCPServiceConfig] = {
"list_tables_for_base",
"list_records_for_table",
],
- readonly_tools=frozenset({"list_bases", "list_tables_for_base", "list_records_for_table"}),
+ readonly_tools=frozenset(
+ {"list_bases", "list_tables_for_base", "list_records_for_table"}
+ ),
account_metadata_keys=["user_id", "user_email"],
),
}
@@ -136,20 +148,22 @@ _CONNECTOR_TYPE_TO_SERVICE: dict[str, MCPServiceConfig] = {
svc.connector_type: svc for svc in MCP_SERVICES.values()
}
-LIVE_CONNECTOR_TYPES: frozenset[SearchSourceConnectorType] = frozenset({
- SearchSourceConnectorType.SLACK_CONNECTOR,
- SearchSourceConnectorType.TEAMS_CONNECTOR,
- SearchSourceConnectorType.LINEAR_CONNECTOR,
- SearchSourceConnectorType.JIRA_CONNECTOR,
- SearchSourceConnectorType.CLICKUP_CONNECTOR,
- SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR,
- SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR,
- SearchSourceConnectorType.AIRTABLE_CONNECTOR,
- SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
- SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
- SearchSourceConnectorType.DISCORD_CONNECTOR,
- SearchSourceConnectorType.LUMA_CONNECTOR,
-})
+LIVE_CONNECTOR_TYPES: frozenset[SearchSourceConnectorType] = frozenset(
+ {
+ SearchSourceConnectorType.SLACK_CONNECTOR,
+ SearchSourceConnectorType.TEAMS_CONNECTOR,
+ SearchSourceConnectorType.LINEAR_CONNECTOR,
+ SearchSourceConnectorType.JIRA_CONNECTOR,
+ SearchSourceConnectorType.CLICKUP_CONNECTOR,
+ SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR,
+ SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR,
+ SearchSourceConnectorType.AIRTABLE_CONNECTOR,
+ SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
+ SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
+ SearchSourceConnectorType.DISCORD_CONNECTOR,
+ SearchSourceConnectorType.LUMA_CONNECTOR,
+ }
+)
def get_service(key: str) -> MCPServiceConfig | None:
diff --git a/surfsense_backend/app/services/obsidian_plugin_indexer.py b/surfsense_backend/app/services/obsidian_plugin_indexer.py
index 8fbdad269..0fc4f30f4 100644
--- a/surfsense_backend/app/services/obsidian_plugin_indexer.py
+++ b/surfsense_backend/app/services/obsidian_plugin_indexer.py
@@ -156,7 +156,9 @@ async def _extract_binary_attachment_markdown(
try:
raw_bytes = base64.b64decode(payload.binary_base64, validate=True)
except Exception:
- logger.warning("obsidian attachment payload had invalid base64: %s", payload.path)
+ logger.warning(
+ "obsidian attachment payload had invalid base64: %s", payload.path
+ )
return "", {"attachment_extraction_status": "invalid_binary_payload"}
suffix = f".{payload.extension.lstrip('.')}"
@@ -180,7 +182,10 @@ async def _extract_binary_attachment_markdown(
return result.markdown_content, metadata
except Exception as exc:
logger.warning(
- "obsidian attachment ETL failed for %s: %s", payload.path, exc, exc_info=True
+ "obsidian attachment ETL failed for %s: %s",
+ payload.path,
+ exc,
+ exc_info=True,
)
return "", {
"attachment_extraction_status": "etl_failed",
diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py
index 5a6117808..7239c57a5 100644
--- a/surfsense_backend/app/tasks/chat/stream_new_chat.py
+++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py
@@ -31,7 +31,6 @@ from sqlalchemy.orm import selectinload
from app.agents.new_chat.chat_deepagent import create_surfsense_deep_agent
from app.agents.new_chat.checkpointer import get_checkpointer
from app.agents.new_chat.filesystem_selection import FilesystemSelection
-from app.config import config
from app.agents.new_chat.llm_config import (
AgentConfig,
create_chat_litellm_from_agent_config,
@@ -182,9 +181,9 @@ def _tool_output_has_error(tool_output: Any) -> bool:
if tool_output.get("error"):
return True
result = tool_output.get("result")
- if isinstance(result, str) and result.strip().lower().startswith("error:"):
- return True
- return False
+ return bool(
+ isinstance(result, str) and result.strip().lower().startswith("error:")
+ )
if isinstance(tool_output, str):
return tool_output.strip().lower().startswith("error:")
return False
@@ -230,7 +229,9 @@ def _log_file_contract(stage: str, result: StreamResult, **extra: Any) -> None:
"stage": stage,
"request_id": result.request_id or "unknown",
"turn_id": result.turn_id or "unknown",
- "chat_id": result.turn_id.split(":", 1)[0] if ":" in result.turn_id else "unknown",
+ "chat_id": result.turn_id.split(":", 1)[0]
+ if ":" in result.turn_id
+ else "unknown",
"filesystem_mode": result.filesystem_mode,
"client_platform": result.client_platform,
"intent_detected": result.intent_detected,
@@ -242,7 +243,9 @@ def _log_file_contract(stage: str, result: StreamResult, **extra: Any) -> None:
"commit_gate_reason": result.commit_gate_reason or None,
}
payload.update(extra)
- _perf_log.info("[file_operation_contract] %s", json.dumps(payload, ensure_ascii=False))
+ _perf_log.info(
+ "[file_operation_contract] %s", json.dumps(payload, ensure_ascii=False)
+ )
async def _stream_agent_events(
@@ -1289,7 +1292,8 @@ async def _stream_agent_events(
result.intent_detected = intent_value
if (
isinstance(intent_value, str)
- and intent_value in (
+ and intent_value
+ in (
"chat_only",
"file_write",
"file_read",
@@ -1308,18 +1312,17 @@ async def _stream_agent_events(
result.commit_gate_passed, result.commit_gate_reason = (
_evaluate_file_contract_outcome(result)
)
- if not result.commit_gate_passed:
- if _contract_enforcement_active(result):
- gate_notice = (
- "I could not complete the requested file write because no successful "
- "write_file/edit_file operation was confirmed."
- )
- gate_text_id = streaming_service.generate_text_id()
- yield streaming_service.format_text_start(gate_text_id)
- yield streaming_service.format_text_delta(gate_text_id, gate_notice)
- yield streaming_service.format_text_end(gate_text_id)
- yield streaming_service.format_terminal_info(gate_notice, "error")
- accumulated_text = gate_notice
+ if not result.commit_gate_passed and _contract_enforcement_active(result):
+ gate_notice = (
+ "I could not complete the requested file write because no successful "
+ "write_file/edit_file operation was confirmed."
+ )
+ gate_text_id = streaming_service.generate_text_id()
+ yield streaming_service.format_text_start(gate_text_id)
+ yield streaming_service.format_text_delta(gate_text_id, gate_notice)
+ yield streaming_service.format_text_end(gate_text_id)
+ yield streaming_service.format_terminal_info(gate_notice, "error")
+ accumulated_text = gate_notice
else:
result.commit_gate_passed = True
result.commit_gate_reason = ""
diff --git a/surfsense_backend/app/utils/async_retry.py b/surfsense_backend/app/utils/async_retry.py
index c3bdd5386..a56f6550a 100644
--- a/surfsense_backend/app/utils/async_retry.py
+++ b/surfsense_backend/app/utils/async_retry.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import contextlib
import logging
from collections.abc import Callable
from typing import TypeVar
@@ -32,9 +33,7 @@ F = TypeVar("F", bound=Callable)
def _is_retryable(exc: BaseException) -> bool:
if isinstance(exc, ConnectorError):
return exc.retryable
- if isinstance(exc, (httpx.TimeoutException, httpx.ConnectError)):
- return True
- return False
+ return bool(isinstance(exc, (httpx.TimeoutException, httpx.ConnectError)))
def build_retry(
@@ -86,10 +85,8 @@ def raise_for_status(
retry_after_raw = response.headers.get("Retry-After")
retry_after: float | None = None
if retry_after_raw:
- try:
+ with contextlib.suppress(ValueError, TypeError):
retry_after = float(retry_after_raw)
- except (ValueError, TypeError):
- pass
raise ConnectorRateLimitError(
f"{service} rate limited (429)",
service=service,
diff --git a/surfsense_backend/app/utils/connector_naming.py b/surfsense_backend/app/utils/connector_naming.py
index 889bf1464..99c8243a5 100644
--- a/surfsense_backend/app/utils/connector_naming.py
+++ b/surfsense_backend/app/utils/connector_naming.py
@@ -233,7 +233,10 @@ async def generate_unique_connector_name(
if identifier:
name = f"{base} - {identifier}"
return await ensure_unique_connector_name(
- session, name, search_space_id, user_id,
+ session,
+ name,
+ search_space_id,
+ user_id,
)
count = await count_connectors_of_type(
diff --git a/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py b/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py
index 41779a570..22f6c6de5 100644
--- a/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py
+++ b/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py
@@ -499,7 +499,9 @@ class TestWireContractSmoke:
"app.routes.obsidian_plugin_routes.upsert_note",
new=AsyncMock(return_value=fake_doc),
) as upsert_mock,
- patch("app.routes.obsidian_plugin_routes._queue_obsidian_attachment") as queue_mock,
+ patch(
+ "app.routes.obsidian_plugin_routes._queue_obsidian_attachment"
+ ) as queue_mock,
):
sync_resp = await obsidian_sync(
SyncBatchRequest(
@@ -548,7 +550,9 @@ class TestWireContractSmoke:
"app.routes.obsidian_plugin_routes.upsert_note",
new=AsyncMock(return_value=fake_doc),
),
- patch("app.routes.obsidian_plugin_routes._queue_obsidian_attachment") as queue_mock,
+ patch(
+ "app.routes.obsidian_plugin_routes._queue_obsidian_attachment"
+ ) as queue_mock,
):
sync_resp = await obsidian_sync(
SyncBatchRequest(
@@ -600,7 +604,9 @@ class TestWireContractSmoke:
"app.routes.obsidian_plugin_routes.upsert_note",
new=AsyncMock(return_value=fake_doc),
),
- patch("app.routes.obsidian_plugin_routes._queue_obsidian_attachment") as queue_mock,
+ patch(
+ "app.routes.obsidian_plugin_routes._queue_obsidian_attachment"
+ ) as queue_mock,
):
sync_resp = await obsidian_sync(
SyncBatchRequest(
@@ -619,7 +625,5 @@ class TestWireContractSmoke:
items_by_path = {it.path: it for it in sync_resp.items}
assert items_by_path["ok.md"].status == "ok"
assert items_by_path["image.png"].status == "error"
- assert "does not match extension" in (
- items_by_path["image.png"].error or ""
- )
+ assert "does not match extension" in (items_by_path["image.png"].error or "")
queue_mock.assert_not_called()
diff --git a/surfsense_backend/tests/unit/middleware/test_file_intent_middleware.py b/surfsense_backend/tests/unit/middleware/test_file_intent_middleware.py
index 673331b0a..7fd3fe4a7 100644
--- a/surfsense_backend/tests/unit/middleware/test_file_intent_middleware.py
+++ b/surfsense_backend/tests/unit/middleware/test_file_intent_middleware.py
@@ -45,9 +45,7 @@ async def test_file_write_intent_injects_contract_message():
@pytest.mark.asyncio
async def test_non_write_intent_does_not_inject_contract_message():
- llm = _FakeLLM(
- '{"intent":"file_read","confidence":0.88,"suggested_filename":null}'
- )
+ llm = _FakeLLM('{"intent":"file_read","confidence":0.88,"suggested_filename":null}')
middleware = FileIntentMiddleware(llm=llm)
original_messages = [HumanMessage(content="Read /notes.md")]
state = {"messages": original_messages, "turn_id": "abc:def"}
@@ -55,7 +53,10 @@ async def test_non_write_intent_does_not_inject_contract_message():
result = await middleware.abefore_agent(state, runtime=None) # type: ignore[arg-type]
assert result is not None
- assert result["file_operation_contract"]["intent"] == FileOperationIntent.FILE_READ.value
+ assert (
+ result["file_operation_contract"]["intent"]
+ == FileOperationIntent.FILE_READ.value
+ )
assert "messages" not in result
@@ -211,4 +212,3 @@ def test_fallback_path_keeps_posix_style_absolute_path_for_linux_and_macos() ->
)
assert resolved == "/var/log/surfsense/notes.md"
-
diff --git a/surfsense_backend/tests/unit/middleware/test_filesystem_verification.py b/surfsense_backend/tests/unit/middleware/test_filesystem_verification.py
index d00365032..cca15e789 100644
--- a/surfsense_backend/tests/unit/middleware/test_filesystem_verification.py
+++ b/surfsense_backend/tests/unit/middleware/test_filesystem_verification.py
@@ -2,11 +2,11 @@ from pathlib import Path
import pytest
+from app.agents.new_chat.filesystem_selection import FilesystemMode
+from app.agents.new_chat.middleware.filesystem import SurfSenseFilesystemMiddleware
from app.agents.new_chat.middleware.multi_root_local_folder_backend import (
MultiRootLocalFolderBackend,
)
-from app.agents.new_chat.filesystem_selection import FilesystemMode
-from app.agents.new_chat.middleware.filesystem import SurfSenseFilesystemMiddleware
pytestmark = pytest.mark.unit
diff --git a/surfsense_backend/tests/unit/test_obsidian_plugin_indexer.py b/surfsense_backend/tests/unit/test_obsidian_plugin_indexer.py
index 7ab3c52e0..20795c739 100644
--- a/surfsense_backend/tests/unit/test_obsidian_plugin_indexer.py
+++ b/surfsense_backend/tests/unit/test_obsidian_plugin_indexer.py
@@ -15,7 +15,6 @@ from app.services.obsidian_plugin_indexer import (
_require_extracted_attachment_content,
)
-
_FAKE_PNG_B64 = base64.b64encode(b"\x89PNG\r\n\x1a\n").decode("ascii")
@@ -102,9 +101,7 @@ async def test_extract_binary_attachment_markdown_uses_etl(monkeypatch) -> None:
mime_type="application/pdf",
)
- async def _fake_run_etl_extract( # noqa: ANN001
- *, file_path, filename, vision_llm
- ):
+ async def _fake_run_etl_extract(*, file_path, filename, vision_llm):
assert filename == "spec.pdf"
assert file_path
assert vision_llm is None
@@ -216,7 +213,7 @@ def test_note_payload_rejects_markdown_with_binary_fields() -> None:
def test_require_extracted_attachment_content_rejects_empty_content() -> None:
with pytest.raises(
- RuntimeError, match="Attachment extraction failed for assets/img.png"
+ RuntimeError, match=r"Attachment extraction failed for assets/img\.png"
):
_require_extracted_attachment_content(
content=" ",
diff --git a/surfsense_backend/tests/unit/test_stream_new_chat_contract.py b/surfsense_backend/tests/unit/test_stream_new_chat_contract.py
index f4adc3d73..034aa484c 100644
--- a/surfsense_backend/tests/unit/test_stream_new_chat_contract.py
+++ b/surfsense_backend/tests/unit/test_stream_new_chat_contract.py
@@ -45,4 +45,3 @@ def test_contract_enforcement_local_only():
result.filesystem_mode = "cloud"
assert not _contract_enforcement_active(result)
-
diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
index 06f3bf79f..9f569398e 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
@@ -45,8 +45,8 @@ import {
} from "@/components/assistant-ui/token-usage-context";
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
import { useMessagesSync } from "@/hooks/use-messages-sync";
-import { documentsApiService } from "@/lib/apis/documents-api.service";
import { getAgentFilesystemSelection } from "@/lib/agent-filesystem";
+import { documentsApiService } from "@/lib/apis/documents-api.service";
import { getBearerToken } from "@/lib/auth-utils";
import { convertToThreadMessage } from "@/lib/chat/message-utils";
import {
@@ -661,8 +661,7 @@ export default function NewChatPage() {
const selection = await getAgentFilesystemSelection(searchSpaceId);
if (
selection.filesystem_mode === "desktop_local_folder" &&
- (!selection.local_filesystem_mounts ||
- selection.local_filesystem_mounts.length === 0)
+ (!selection.local_filesystem_mounts || selection.local_filesystem_mounts.length === 0)
) {
toast.error("Select a local folder before using Local Folder mode.");
return;
@@ -842,14 +841,7 @@ export default function NewChatPage() {
});
} else {
const tcId = `interrupt-${action.name}`;
- addToolCall(
- contentPartsState,
- toolsWithUI,
- tcId,
- action.name,
- action.args,
- true
- );
+ addToolCall(contentPartsState, toolsWithUI, tcId, action.name, action.args, true);
updateToolCall(contentPartsState, tcId, {
result: { __interrupt__: true, ...interruptData },
});
@@ -1189,14 +1181,7 @@ export default function NewChatPage() {
});
} else {
const tcId = `interrupt-${action.name}`;
- addToolCall(
- contentPartsState,
- toolsWithUI,
- tcId,
- action.name,
- action.args,
- true
- );
+ addToolCall(contentPartsState, toolsWithUI, tcId, action.name, action.args, true);
updateToolCall(contentPartsState, tcId, {
result: {
__interrupt__: true,
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx
index 6207457c4..12a7d00f0 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent.tsx
@@ -111,9 +111,7 @@ function HotkeyRow({
}
>
{recording ? (
-
- Press hotkeys...
-
+ Press hotkeys...
) : (
Hotkeys are only available in the SurfSense desktop app.
++ Hotkeys are only available in the SurfSense desktop app. +
Microsoft Teams Access
- Your agent can search and read messages from Teams channels you have access to, - and send messages on your behalf. Make sure you're a member of the teams - you want to interact with. + Your agent can search and read messages from Teams channels you have access to, and send + messages on your behalf. Make sure you're a member of the teams you want to interact + with.