diff --git a/surfsense_backend/app/agents/new_chat/sandbox.py b/surfsense_backend/app/agents/new_chat/sandbox.py index d2afd5df0..996414557 100644 --- a/surfsense_backend/app/agents/new_chat/sandbox.py +++ b/surfsense_backend/app/agents/new_chat/sandbox.py @@ -12,7 +12,12 @@ import asyncio import logging import os -from daytona import CreateSandboxFromSnapshotParams, Daytona, DaytonaConfig, SandboxState +from daytona import ( + CreateSandboxFromSnapshotParams, + Daytona, + DaytonaConfig, + SandboxState, +) from deepagents.backends.protocol import ExecuteResponse from langchain_daytona import DaytonaSandbox @@ -38,9 +43,12 @@ class _TimeoutAwareSandbox(DaytonaSandbox): truncated=False, ) - async def aexecute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse: # type: ignore[override] + async def aexecute( + self, command: str, *, timeout: int | None = None + ) -> ExecuteResponse: # type: ignore[override] return await asyncio.to_thread(self.execute, command, timeout=timeout) + _daytona_client: Daytona | None = None THREAD_LABEL_KEY = "surfsense_thread" @@ -72,9 +80,7 @@ def _find_or_create(thread_id: str) -> _TimeoutAwareSandbox: try: sandbox = client.find_one(labels=labels) - logger.info( - "Found existing sandbox %s (state=%s)", sandbox.id, sandbox.state - ) + logger.info("Found existing sandbox %s (state=%s)", sandbox.id, sandbox.state) if sandbox.state in ( SandboxState.STOPPED, @@ -84,7 +90,11 @@ def _find_or_create(thread_id: str) -> _TimeoutAwareSandbox: logger.info("Starting stopped sandbox %s …", sandbox.id) sandbox.start(timeout=60) logger.info("Sandbox %s is now started", sandbox.id) - elif sandbox.state in (SandboxState.ERROR, SandboxState.BUILD_FAILED, SandboxState.DESTROYED): + elif sandbox.state in ( + SandboxState.ERROR, + SandboxState.BUILD_FAILED, + SandboxState.DESTROYED, + ): logger.warning( "Sandbox %s in unrecoverable state %s — creating a new one", sandbox.id, diff --git a/surfsense_backend/app/agents/new_chat/system_prompt.py b/surfsense_backend/app/agents/new_chat/system_prompt.py index c55a9c756..39ef7b70a 100644 --- a/surfsense_backend/app/agents/new_chat/system_prompt.py +++ b/surfsense_backend/app/agents/new_chat/system_prompt.py @@ -782,7 +782,12 @@ def build_surfsense_system_prompt( tools_instructions = _get_tools_instructions(visibility) citation_instructions = SURFSENSE_CITATION_INSTRUCTIONS sandbox_instructions = SANDBOX_EXECUTION_INSTRUCTIONS if sandbox_enabled else "" - return system_instructions + tools_instructions + citation_instructions + sandbox_instructions + return ( + system_instructions + + tools_instructions + + citation_instructions + + sandbox_instructions + ) def build_configurable_system_prompt( @@ -842,7 +847,12 @@ def build_configurable_system_prompt( sandbox_instructions = SANDBOX_EXECUTION_INSTRUCTIONS if sandbox_enabled else "" - return system_instructions + tools_instructions + citation_instructions + sandbox_instructions + return ( + system_instructions + + tools_instructions + + citation_instructions + + sandbox_instructions + ) def get_default_system_instructions() -> str: diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py index 0dd683f7e..af93ddc8f 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py @@ -58,7 +58,9 @@ def create_create_google_drive_file_tool( - "Create a Google Doc called 'Meeting Notes'" - "Create a spreadsheet named 'Budget 2026' with some sample data" """ - logger.info(f"create_google_drive_file called: name='{name}', type='{file_type}'") + logger.info( + f"create_google_drive_file called: name='{name}', type='{file_type}'" + ) if db_session is None or search_space_id is None or user_id is None: return { @@ -74,7 +76,9 @@ def create_create_google_drive_file_tool( try: metadata_service = GoogleDriveToolMetadataService(db_session) - context = await metadata_service.get_creation_context(search_space_id, user_id) + context = await metadata_service.get_creation_context( + search_space_id, user_id + ) if "error" in context: logger.error(f"Failed to fetch creation context: {context['error']}") @@ -100,8 +104,12 @@ def create_create_google_drive_file_tool( } ) - 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: logger.warning("No approval decision received") @@ -183,7 +191,9 @@ def create_create_google_drive_file_tool( logger.info( f"Creating Google Drive file: name='{final_name}', type='{final_file_type}', connector={actual_connector_id}" ) - client = GoogleDriveClient(session=db_session, connector_id=actual_connector_id) + client = GoogleDriveClient( + session=db_session, connector_id=actual_connector_id + ) try: created = await client.create_file( name=final_name, @@ -203,7 +213,9 @@ def create_create_google_drive_file_tool( } raise - logger.info(f"Google Drive file created: id={created.get('id')}, name={created.get('name')}") + logger.info( + f"Google Drive file created: id={created.get('id')}, name={created.get('name')}" + ) return { "status": "success", "file_id": created.get("id"), diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py index 600aae983..917ba3376 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py @@ -52,7 +52,9 @@ def create_delete_google_drive_file_tool( - "Delete the 'Meeting Notes' file from Google Drive" - "Trash the 'Old Budget' spreadsheet" """ - logger.info(f"delete_google_drive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}") + logger.info( + f"delete_google_drive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}" + ) if db_session is None or search_space_id is None or user_id is None: return { @@ -103,8 +105,12 @@ def create_delete_google_drive_file_tool( } ) - 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: logger.warning("No approval decision received") @@ -130,11 +136,16 @@ def create_delete_google_drive_file_tool( final_params = decision["args"] final_file_id = final_params.get("file_id", file_id) - final_connector_id = final_params.get("connector_id", connector_id_from_context) + final_connector_id = final_params.get( + "connector_id", connector_id_from_context + ) final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb) if not final_connector_id: - return {"status": "error", "message": "No connector found for this file."} + return { + "status": "error", + "message": "No connector found for this file.", + } from sqlalchemy.future import select @@ -174,7 +185,9 @@ def create_delete_google_drive_file_tool( } raise - logger.info(f"Google Drive file deleted (moved to trash): file_id={final_file_id}") + logger.info( + f"Google Drive file deleted (moved to trash): file_id={final_file_id}" + ) trash_result: dict[str, Any] = { "status": "success", @@ -195,7 +208,9 @@ def create_delete_google_drive_file_tool( await db_session.delete(document) await db_session.commit() deleted_from_kb = True - logger.info(f"Deleted document {document_id} from knowledge base") + logger.info( + f"Deleted document {document_id} from knowledge base" + ) else: logger.warning(f"Document {document_id} not found in KB") 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 01342e920..dffed5e86 100644 --- a/surfsense_backend/app/agents/new_chat/tools/registry.py +++ b/surfsense_backend/app/agents/new_chat/tools/registry.py @@ -47,6 +47,10 @@ from app.db import ChatVisibility from .display_image import create_display_image_tool from .generate_image import create_generate_image_tool +from .google_drive import ( + create_create_google_drive_file_tool, + create_delete_google_drive_file_tool, +) from .knowledge_base import create_search_knowledge_base_tool from .linear import ( create_create_linear_issue_tool, @@ -55,10 +59,6 @@ from .linear import ( ) from .link_preview import create_link_preview_tool from .mcp_tool import load_mcp_tools -from .google_drive import ( - create_create_google_drive_file_tool, - create_delete_google_drive_file_tool, -) from .notion import ( create_create_notion_page_tool, create_delete_notion_page_tool, diff --git a/surfsense_backend/app/routes/sandbox_routes.py b/surfsense_backend/app/routes/sandbox_routes.py index af13e48fc..428eea09e 100644 --- a/surfsense_backend/app/routes/sandbox_routes.py +++ b/surfsense_backend/app/routes/sandbox_routes.py @@ -73,7 +73,7 @@ async def download_sandbox_file( try: sandbox = await get_or_create_sandbox(thread_id) - raw_sandbox = sandbox._sandbox # noqa: SLF001 + raw_sandbox = sandbox._sandbox content: bytes = await asyncio.to_thread(raw_sandbox.fs.download_file, path) except Exception as exc: logger.warning("Sandbox file download failed for %s: %s", path, exc) diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py index 327aa7977..ae04a6bee 100644 --- a/surfsense_backend/app/tasks/chat/stream_new_chat.py +++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py @@ -10,14 +10,13 @@ Supports loading LLM configurations from: """ import json +import logging import re from collections.abc import AsyncGenerator from dataclasses import dataclass from typing import Any from uuid import UUID -import logging - from langchain_core.messages import HumanMessage from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select @@ -31,7 +30,13 @@ from app.agents.new_chat.llm_config import ( load_agent_config, load_llm_config_from_yaml, ) -from app.db import ChatVisibility, Document, Report, SurfsenseDocsDocument, async_session_maker +from app.db import ( + ChatVisibility, + Document, + Report, + SurfsenseDocsDocument, + async_session_maker, +) from app.prompts import TITLE_GENERATION_PROMPT_TEMPLATE from app.services.chat_session_state_service import ( clear_ai_responding, @@ -645,9 +650,15 @@ async def _stream_agent_events( m = re.match(r"^Exit code:\s*(\d+)", raw_text) exit_code_val = int(m.group(1)) if m else None if exit_code_val is not None and exit_code_val == 0: - completed_items = [*last_active_step_items, "Completed successfully"] + completed_items = [ + *last_active_step_items, + "Completed successfully", + ] elif exit_code_val is not None: - completed_items = [*last_active_step_items, f"Exit code: {exit_code_val}"] + completed_items = [ + *last_active_step_items, + f"Exit code: {exit_code_val}", + ] else: completed_items = [*last_active_step_items, "Finished"] yield streaming_service.format_thinking_step( @@ -1037,13 +1048,18 @@ async def stream_new_chat( # Optionally provision a sandboxed code execution environment sandbox_backend = None - from app.agents.new_chat.sandbox import is_sandbox_enabled, get_or_create_sandbox + from app.agents.new_chat.sandbox import ( + get_or_create_sandbox, + is_sandbox_enabled, + ) + if is_sandbox_enabled(): try: sandbox_backend = await get_or_create_sandbox(chat_id) except Exception as sandbox_err: logging.getLogger(__name__).warning( - "Sandbox creation failed, continuing without execute tool: %s", sandbox_err + "Sandbox creation failed, continuing without execute tool: %s", + sandbox_err, ) visibility = thread_visibility or ChatVisibility.PRIVATE @@ -1426,13 +1442,18 @@ async def stream_resume_chat( checkpointer = await get_checkpointer() sandbox_backend = None - from app.agents.new_chat.sandbox import is_sandbox_enabled, get_or_create_sandbox + from app.agents.new_chat.sandbox import ( + get_or_create_sandbox, + is_sandbox_enabled, + ) + if is_sandbox_enabled(): try: sandbox_backend = await get_or_create_sandbox(chat_id) except Exception as sandbox_err: logging.getLogger(__name__).warning( - "Sandbox creation failed, continuing without execute tool: %s", sandbox_err + "Sandbox creation failed, continuing without execute tool: %s", + sandbox_err, ) visibility = thread_visibility or ChatVisibility.PRIVATE diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index f8a9cecd0..c33c2e341 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -16,8 +16,8 @@ import { Link2, ShieldUser, Trash2, - UserPlus, User, + UserPlus, Users, } from "lucide-react"; import { motion } from "motion/react"; diff --git a/surfsense_web/components/assistant-ui/tooltip-icon-button.tsx b/surfsense_web/components/assistant-ui/tooltip-icon-button.tsx index 55f7c6a2e..0ca96e912 100644 --- a/surfsense_web/components/assistant-ui/tooltip-icon-button.tsx +++ b/surfsense_web/components/assistant-ui/tooltip-icon-button.tsx @@ -1,7 +1,7 @@ "use client"; import { Slottable } from "@radix-ui/react-slot"; -import { type ComponentPropsWithRef, type ReactNode, forwardRef } from "react"; +import { type ComponentPropsWithRef, forwardRef, type ReactNode } from "react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; diff --git a/surfsense_web/components/settings/roles-manager.tsx b/surfsense_web/components/settings/roles-manager.tsx index 96ab2551f..820fd11e5 100644 --- a/surfsense_web/components/settings/roles-manager.tsx +++ b/surfsense_web/components/settings/roles-manager.tsx @@ -7,6 +7,7 @@ import { Edit2, FileText, Globe, + Logs, type LucideIcon, MessageCircle, MessageSquare, @@ -14,7 +15,6 @@ import { MoreHorizontal, Plug, Plus, - Logs, Settings, Shield, Trash2, @@ -23,13 +23,13 @@ import { import { motion } from "motion/react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { myAccessAtom } from "@/atoms/members/members-query.atoms"; +import { permissionsAtom } from "@/atoms/permissions/permissions-query.atoms"; import { createRoleMutationAtom, deleteRoleMutationAtom, updateRoleMutationAtom, } from "@/atoms/roles/roles-mutation.atoms"; -import { permissionsAtom } from "@/atoms/permissions/permissions-query.atoms"; -import { myAccessAtom } from "@/atoms/members/members-query.atoms"; import { AlertDialog, AlertDialogAction, diff --git a/surfsense_web/components/tool-ui/google-drive/create-file.tsx b/surfsense_web/components/tool-ui/google-drive/create-file.tsx index d6f08653d..f2cc97dcf 100644 --- a/surfsense_web/components/tool-ui/google-drive/create-file.tsx +++ b/surfsense_web/components/tool-ui/google-drive/create-file.tsx @@ -253,29 +253,31 @@ function ApprovalCard({ )} - {/* Display mode */} - {!isEditing && ( -
-
-

Name

-

{committedArgs?.name ?? args.name}

-
-
-

Type

-

- {FILE_TYPE_LABELS[committedArgs?.file_type ?? args.file_type] ?? committedArgs?.file_type ?? args.file_type} -

-
- {(committedArgs?.content ?? args.content) && ( + {/* Display mode */} + {!isEditing && ( +
-

Content

-

- {committedArgs?.content ?? args.content} +

Name

+

{committedArgs?.name ?? args.name}

+
+
+

Type

+

+ {FILE_TYPE_LABELS[committedArgs?.file_type ?? args.file_type] ?? + committedArgs?.file_type ?? + args.file_type}

- )} -
- )} + {(committedArgs?.content ?? args.content) && ( +
+

Content

+

+ {committedArgs?.content ?? args.content} +

+
+ )} +
+ )} {/* Edit mode */} {isEditing && !decided && ( @@ -341,26 +343,26 @@ function ApprovalCard({

) : isEditing ? ( <> - + + )} {canEdit && ( ); } @@ -270,14 +279,11 @@ function ExecuteCompleted({ variant={success ? "secondary" : "destructive"} className={cn( "ml-auto gap-1 text-[10px] px-1.5 py-0", - success && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20" + success && + "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20" )} > - {success ? ( - - ) : ( - - )} + {success ? : } {parsed.exitCode} ); @@ -306,7 +312,10 @@ function ExecuteCompleted({ {truncateCommand(command)} {hasFiles && !open && ( - + {parsed.files.length} @@ -355,11 +364,7 @@ function ExecuteCompleted({

{parsed.files.map((file) => ( - + ))}
@@ -412,9 +417,4 @@ export const SandboxExecuteToolUI = makeAssistantToolUI , - document.body, + document.body ); } diff --git a/surfsense_web/components/ui/hero-carousel.tsx b/surfsense_web/components/ui/hero-carousel.tsx index c623aa525..141583d67 100644 --- a/surfsense_web/components/ui/hero-carousel.tsx +++ b/surfsense_web/components/ui/hero-carousel.tsx @@ -18,8 +18,7 @@ const carouselItems = [ }, { title: "Search & Citation", - description: - "Ask questions and get cited responses from your knowledge base.", + description: "Ask questions and get cited responses from your knowledge base.", src: "/homepage/hero_tutorial/BSNCGif.gif", }, { @@ -121,9 +120,7 @@ function HeroCarouselCard({

{title}

-

- {description} -

+

{description}

) : frozenFrame ? ( - {title} + {title} ) : (
)} @@ -174,7 +167,7 @@ function HeroCarousel() { directionRef.current = newIndex >= activeIndex ? "forward" : "backward"; setActiveIndex(newIndex); }, - [activeIndex], + [activeIndex] ); useEffect(() => { @@ -246,7 +239,7 @@ function HeroCarousel() { blur: t * 6, }; }, - [activeIndex, cardWidth, baseOffset, stackGap], + [activeIndex, cardWidth, baseOffset, stackGap] ); return ( @@ -287,18 +280,18 @@ function HeroCarousel() { transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }} > - - + animate={{ filter: `blur(${style.blur}px)` }} + transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }} + > + +