Skip to content

Commit 5c76363

Browse files
Merge pull request #9 from AnishSarkar22/feat/obsidian-plugin
Feat/obsidian plugin
2 parents 9340774 + 4f1c870 commit 5c76363

197 files changed

Lines changed: 11204 additions & 2586 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-obsidian-plugin.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,15 @@ jobs:
104104
fi
105105
106106
# Publish release under bare `manifest.json` version (no `obsidian-v` prefix) for BRAT/store compatibility.
107+
# `make_latest: "false"` keeps the desktop app's `v*` release headlined since Obsidian and BRAT resolve plugins via getReleaseByTag, not the latest flag.
107108
- name: Create GitHub release
108109
if: steps.release_mode.outputs.should_publish == 'true'
109110
uses: softprops/action-gh-release@v3
110111
with:
111112
tag_name: ${{ steps.version.outputs.version }}
112113
name: SurfSense Obsidian Plugin ${{ steps.version.outputs.version }}
113114
generate_release_notes: true
115+
make_latest: "false"
114116
files: |
115117
surfsense_obsidian/main.js
116118
surfsense_obsidian/manifest.json

docker/docker-compose.dev.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ services:
7777
- shared_temp:/shared_tmp
7878
env_file:
7979
- ../surfsense_backend/.env
80+
extra_hosts:
81+
- "host.docker.internal:host-gateway"
8082
environment:
8183
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
8284
- CELERY_BROKER_URL=${REDIS_URL:-redis://redis:6379/0}
@@ -118,6 +120,8 @@ services:
118120
- shared_temp:/shared_tmp
119121
env_file:
120122
- ../surfsense_backend/.env
123+
extra_hosts:
124+
- "host.docker.internal:host-gateway"
121125
environment:
122126
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
123127
- CELERY_BROKER_URL=${REDIS_URL:-redis://redis:6379/0}

docker/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ services:
6060
- shared_temp:/shared_tmp
6161
env_file:
6262
- .env
63+
extra_hosts:
64+
- "host.docker.internal:host-gateway"
6365
environment:
6466
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
6567
CELERY_BROKER_URL: ${REDIS_URL:-redis://redis:6379/0}
@@ -100,6 +102,8 @@ services:
100102
- shared_temp:/shared_tmp
101103
env_file:
102104
- .env
105+
extra_hosts:
106+
- "host.docker.internal:host-gateway"
103107
environment:
104108
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://${DB_USER:-surfsense}:${DB_PASSWORD:-surfsense}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-surfsense}}
105109
CELERY_BROKER_URL: ${REDIS_URL:-redis://redis:6379/0}

manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"id": "surfsense",
2+
"id": "surfsense-obsidian",
33
"name": "SurfSense",
4-
"version": "0.1.1",
4+
"version": "0.1.0",
55
"minAppVersion": "1.5.4",
66
"description": "Turn your vault into a searchable second brain with SurfSense.",
77
"author": "SurfSense",
8-
"authorUrl": "https://github.com/MODSetter/SurfSense",
8+
"authorUrl": "https://www.surfsense.com",
99
"isDesktopOnly": false
1010
}

surfsense_backend/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ LLAMA_CLOUD_API_KEY=llx-nnn
239239
# DAYTONA_TARGET=us
240240
# DAYTONA_SNAPSHOT_ID=
241241

242+
# Desktop local filesystem mode (chat file tools run against a local folder root)
243+
# ENABLE_DESKTOP_LOCAL_FILESYSTEM=FALSE
244+
242245
# OPTIONAL: Add these for LangSmith Observability
243246
LANGSMITH_TRACING=true
244247
LANGSMITH_ENDPOINT=https://api.smith.langchain.com

surfsense_backend/app/agents/new_chat/chat_deepagent.py

Lines changed: 37 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from deepagents.graph import BASE_AGENT_PROMPT
2525
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
2626
from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT
27-
from deepagents.middleware.summarization import create_summarization_middleware
2827
from langchain.agents import create_agent
2928
from langchain.agents.middleware import TodoListMiddleware
3029
from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
@@ -34,18 +33,24 @@
3433
from sqlalchemy.ext.asyncio import AsyncSession
3534

3635
from app.agents.new_chat.context import SurfSenseContextSchema
36+
from app.agents.new_chat.filesystem_backends import build_backend_resolver
37+
from app.agents.new_chat.filesystem_selection import FilesystemSelection
3738
from app.agents.new_chat.llm_config import AgentConfig
3839
from app.agents.new_chat.middleware import (
3940
DedupHITLToolCallsMiddleware,
41+
FileIntentMiddleware,
4042
KnowledgeBaseSearchMiddleware,
4143
MemoryInjectionMiddleware,
4244
SurfSenseFilesystemMiddleware,
4345
)
46+
from app.agents.new_chat.middleware.safe_summarization import (
47+
create_safe_summarization_middleware,
48+
)
4449
from app.agents.new_chat.system_prompt import (
4550
build_configurable_system_prompt,
4651
build_surfsense_system_prompt,
4752
)
48-
from app.agents.new_chat.tools.registry import build_tools_async
53+
from app.agents.new_chat.tools.registry import build_tools_async, get_connector_gated_tools
4954
from app.db import ChatVisibility
5055
from app.services.connector_service import ConnectorService
5156
from app.utils.perf import get_perf_logger
@@ -162,6 +167,7 @@ async def create_surfsense_deep_agent(
162167
thread_visibility: ChatVisibility | None = None,
163168
mentioned_document_ids: list[int] | None = None,
164169
anon_session_id: str | None = None,
170+
filesystem_selection: FilesystemSelection | None = None,
165171
):
166172
"""
167173
Create a SurfSense deep agent with configurable tools and prompts.
@@ -236,6 +242,8 @@ async def create_surfsense_deep_agent(
236242
)
237243
"""
238244
_t_agent_total = time.perf_counter()
245+
filesystem_selection = filesystem_selection or FilesystemSelection()
246+
backend_resolver = build_backend_resolver(filesystem_selection)
239247

240248
# Discover available connectors and document types for this search space
241249
available_connectors: list[str] | None = None
@@ -285,105 +293,10 @@ async def create_surfsense_deep_agent(
285293
"llm": llm,
286294
}
287295

288-
# Disable Notion action tools if no Notion connector is configured
289296
modified_disabled_tools = list(disabled_tools) if disabled_tools else []
290-
has_notion_connector = (
291-
available_connectors is not None and "NOTION_CONNECTOR" in available_connectors
292-
)
293-
if not has_notion_connector:
294-
notion_tools = [
295-
"create_notion_page",
296-
"update_notion_page",
297-
"delete_notion_page",
298-
]
299-
modified_disabled_tools.extend(notion_tools)
300-
301-
# Disable Linear action tools if no Linear connector is configured
302-
has_linear_connector = (
303-
available_connectors is not None and "LINEAR_CONNECTOR" in available_connectors
304-
)
305-
if not has_linear_connector:
306-
linear_tools = [
307-
"create_linear_issue",
308-
"update_linear_issue",
309-
"delete_linear_issue",
310-
]
311-
modified_disabled_tools.extend(linear_tools)
312-
313-
# Disable Google Drive action tools if no Google Drive connector is configured
314-
has_google_drive_connector = (
315-
available_connectors is not None and "GOOGLE_DRIVE_FILE" in available_connectors
316-
)
317-
if not has_google_drive_connector:
318-
google_drive_tools = [
319-
"create_google_drive_file",
320-
"delete_google_drive_file",
321-
]
322-
modified_disabled_tools.extend(google_drive_tools)
323-
324-
has_dropbox_connector = (
325-
available_connectors is not None and "DROPBOX_FILE" in available_connectors
326-
)
327-
if not has_dropbox_connector:
328-
modified_disabled_tools.extend(["create_dropbox_file", "delete_dropbox_file"])
329-
330-
has_onedrive_connector = (
331-
available_connectors is not None and "ONEDRIVE_FILE" in available_connectors
332-
)
333-
if not has_onedrive_connector:
334-
modified_disabled_tools.extend(["create_onedrive_file", "delete_onedrive_file"])
335-
336-
# Disable Google Calendar action tools if no Google Calendar connector is configured
337-
has_google_calendar_connector = (
338-
available_connectors is not None
339-
and "GOOGLE_CALENDAR_CONNECTOR" in available_connectors
340-
)
341-
if not has_google_calendar_connector:
342-
calendar_tools = [
343-
"create_calendar_event",
344-
"update_calendar_event",
345-
"delete_calendar_event",
346-
]
347-
modified_disabled_tools.extend(calendar_tools)
348-
349-
# Disable Gmail action tools if no Gmail connector is configured
350-
has_gmail_connector = (
351-
available_connectors is not None
352-
and "GOOGLE_GMAIL_CONNECTOR" in available_connectors
353-
)
354-
if not has_gmail_connector:
355-
gmail_tools = [
356-
"create_gmail_draft",
357-
"update_gmail_draft",
358-
"send_gmail_email",
359-
"trash_gmail_email",
360-
]
361-
modified_disabled_tools.extend(gmail_tools)
362-
363-
# Disable Jira action tools if no Jira connector is configured
364-
has_jira_connector = (
365-
available_connectors is not None and "JIRA_CONNECTOR" in available_connectors
366-
)
367-
if not has_jira_connector:
368-
jira_tools = [
369-
"create_jira_issue",
370-
"update_jira_issue",
371-
"delete_jira_issue",
372-
]
373-
modified_disabled_tools.extend(jira_tools)
374-
375-
# Disable Confluence action tools if no Confluence connector is configured
376-
has_confluence_connector = (
377-
available_connectors is not None
378-
and "CONFLUENCE_CONNECTOR" in available_connectors
297+
modified_disabled_tools.extend(
298+
get_connector_gated_tools(available_connectors)
379299
)
380-
if not has_confluence_connector:
381-
confluence_tools = [
382-
"create_confluence_page",
383-
"update_confluence_page",
384-
"delete_confluence_page",
385-
]
386-
modified_disabled_tools.extend(confluence_tools)
387300

388301
# Remove direct KB search tool; we now pre-seed a scoped filesystem via middleware.
389302
if "search_knowledge_base" not in modified_disabled_tools:
@@ -407,6 +320,20 @@ async def create_surfsense_deep_agent(
407320
_t0 = time.perf_counter()
408321
_enabled_tool_names = {t.name for t in tools}
409322
_user_disabled_tool_names = set(disabled_tools) if disabled_tools else set()
323+
324+
# Collect generic MCP connector info so the system prompt can route queries
325+
# to their tools instead of falling back to "not in knowledge base".
326+
_mcp_connector_tools: dict[str, list[str]] = {}
327+
for t in tools:
328+
meta = getattr(t, "metadata", None) or {}
329+
if meta.get("mcp_is_generic") and meta.get("mcp_connector_name"):
330+
_mcp_connector_tools.setdefault(
331+
meta["mcp_connector_name"], [],
332+
).append(t.name)
333+
334+
if _mcp_connector_tools:
335+
_perf_log.info("MCP connector tool routing: %s", _mcp_connector_tools)
336+
410337
if agent_config is not None:
411338
system_prompt = build_configurable_system_prompt(
412339
custom_system_instructions=agent_config.system_instructions,
@@ -415,12 +342,14 @@ async def create_surfsense_deep_agent(
415342
thread_visibility=thread_visibility,
416343
enabled_tool_names=_enabled_tool_names,
417344
disabled_tool_names=_user_disabled_tool_names,
345+
mcp_connector_tools=_mcp_connector_tools,
418346
)
419347
else:
420348
system_prompt = build_surfsense_system_prompt(
421349
thread_visibility=thread_visibility,
422350
enabled_tool_names=_enabled_tool_names,
423351
disabled_tool_names=_user_disabled_tool_names,
352+
mcp_connector_tools=_mcp_connector_tools,
424353
)
425354
_perf_log.info(
426355
"[create_agent] System prompt built in %.3fs", time.perf_counter() - _t0
@@ -437,12 +366,15 @@ async def create_surfsense_deep_agent(
437366
gp_middleware = [
438367
TodoListMiddleware(),
439368
_memory_middleware,
369+
FileIntentMiddleware(llm=llm),
440370
SurfSenseFilesystemMiddleware(
371+
backend=backend_resolver,
372+
filesystem_mode=filesystem_selection.mode,
441373
search_space_id=search_space_id,
442374
created_by_id=user_id,
443375
thread_id=thread_id,
444376
),
445-
create_summarization_middleware(llm, StateBackend),
377+
create_safe_summarization_middleware(llm, StateBackend),
446378
PatchToolCallsMiddleware(),
447379
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
448380
]
@@ -458,21 +390,25 @@ async def create_surfsense_deep_agent(
458390
deepagent_middleware = [
459391
TodoListMiddleware(),
460392
_memory_middleware,
393+
FileIntentMiddleware(llm=llm),
461394
KnowledgeBaseSearchMiddleware(
462395
llm=llm,
463396
search_space_id=search_space_id,
397+
filesystem_mode=filesystem_selection.mode,
464398
available_connectors=available_connectors,
465399
available_document_types=available_document_types,
466400
mentioned_document_ids=mentioned_document_ids,
467401
anon_session_id=anon_session_id,
468402
),
469403
SurfSenseFilesystemMiddleware(
404+
backend=backend_resolver,
405+
filesystem_mode=filesystem_selection.mode,
470406
search_space_id=search_space_id,
471407
created_by_id=user_id,
472408
thread_id=thread_id,
473409
),
474410
SubAgentMiddleware(backend=StateBackend, subagents=[general_purpose_spec]),
475-
create_summarization_middleware(llm, StateBackend),
411+
create_safe_summarization_middleware(llm, StateBackend),
476412
PatchToolCallsMiddleware(),
477413
DedupHITLToolCallsMiddleware(agent_tools=tools),
478414
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),

surfsense_backend/app/agents/new_chat/context.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
This module defines the custom state schema used by the SurfSense deep agent.
55
"""
66

7-
from typing import TypedDict
7+
from typing import NotRequired, TypedDict
8+
9+
10+
class FileOperationContractState(TypedDict):
11+
intent: str
12+
confidence: float
13+
suggested_path: str
14+
timestamp: str
15+
turn_id: str
816

917

1018
class SurfSenseContextSchema(TypedDict):
@@ -24,5 +32,8 @@ class SurfSenseContextSchema(TypedDict):
2432
"""
2533

2634
search_space_id: int
35+
file_operation_contract: NotRequired[FileOperationContractState]
36+
turn_id: NotRequired[str]
37+
request_id: NotRequired[str]
2738
# These are runtime-injected and won't be serialized
2839
# db_session and connector_service are passed when invoking the agent
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Filesystem backend resolver for cloud and desktop-local modes."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from functools import lru_cache
7+
8+
from deepagents.backends.state import StateBackend
9+
from langgraph.prebuilt.tool_node import ToolRuntime
10+
11+
from app.agents.new_chat.filesystem_selection import FilesystemMode, FilesystemSelection
12+
from app.agents.new_chat.middleware.multi_root_local_folder_backend import (
13+
MultiRootLocalFolderBackend,
14+
)
15+
16+
17+
@lru_cache(maxsize=64)
18+
def _cached_multi_root_backend(
19+
mounts: tuple[tuple[str, str], ...],
20+
) -> MultiRootLocalFolderBackend:
21+
return MultiRootLocalFolderBackend(mounts)
22+
23+
24+
def build_backend_resolver(
25+
selection: FilesystemSelection,
26+
) -> Callable[[ToolRuntime], StateBackend | MultiRootLocalFolderBackend]:
27+
"""Create deepagents backend resolver for the selected filesystem mode."""
28+
29+
if selection.mode == FilesystemMode.DESKTOP_LOCAL_FOLDER and selection.local_mounts:
30+
31+
def _resolve_local(_runtime: ToolRuntime) -> MultiRootLocalFolderBackend:
32+
mounts = tuple(
33+
(entry.mount_id, entry.root_path) for entry in selection.local_mounts
34+
)
35+
return _cached_multi_root_backend(mounts)
36+
37+
return _resolve_local
38+
39+
def _resolve_cloud(runtime: ToolRuntime) -> StateBackend:
40+
return StateBackend(runtime)
41+
42+
return _resolve_cloud

0 commit comments

Comments
 (0)