diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/constants.py b/surfsense_backend/app/agents/chat/multi_agent_chat/constants.py index 7e4061813..fff608742 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/constants.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/constants.py @@ -26,6 +26,7 @@ CONNECTOR_TYPE_TO_CONNECTOR_AGENT_MAPS: dict[str, str] = { SUBAGENT_TO_REQUIRED_CONNECTOR_MAP: dict[str, frozenset[str]] = { "deliverables": frozenset(), "knowledge_base": frozenset(), + "intelligence_agent": frozenset(), "airtable": frozenset({"AIRTABLE_CONNECTOR"}), "calendar": frozenset({"GOOGLE_CALENDAR_CONNECTOR"}), "clickup": frozenset({"CLICKUP_CONNECTOR"}), diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/__init__.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/agent.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/agent.py new file mode 100644 index 000000000..21933005b --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/agent.py @@ -0,0 +1,43 @@ +"""``intelligence_agent`` route: ``SurfSenseSubagentSpec`` builder for deepagents.""" + +from __future__ import annotations + +from typing import Any + +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import BaseTool + +from app.agents.chat.multi_agent_chat.subagents.shared.md_file_reader import ( + read_md_file, +) +from app.agents.chat.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec +from app.agents.chat.multi_agent_chat.subagents.shared.subagent_builder import ( + pack_subagent, +) + +from .tools.index import NAME, RULESET, load_tools + + +def build_subagent( + *, + dependencies: dict[str, Any], + model: BaseChatModel | None = None, + middleware_stack: dict[str, Any] | None = None, + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Gathers and compares web intelligence for this workspace." + ) + system_prompt = read_md_file(__package__, "system_prompt").strip() + return pack_subagent( + name=NAME, + description=description, + system_prompt=system_prompt, + tools=tools, + ruleset=RULESET, + dependencies=dependencies, + model=model, + middleware_stack=middleware_stack, + ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/description.md b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/description.md new file mode 100644 index 000000000..cefad396c --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/description.md @@ -0,0 +1,2 @@ +Specialist for web intelligence: fetch live pages and search the web, then compare against earlier findings. +Use whenever a task needs current data pulled from specific URLs or discovered on the web — competitor/product monitoring, pricing, listings, "what changed since last time" — and answered from that evidence. diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/system_prompt.md b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/system_prompt.md new file mode 100644 index 000000000..0289f40e2 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/system_prompt.md @@ -0,0 +1,54 @@ +You are the SurfSense intelligence sub-agent. +You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. + + +Pull evidence from the live web with the web.* capability verbs and answer the delegated question, including "what changed" comparisons against evidence already in this conversation. + + + +- `web_discover` — search the web for a query; returns ranked hits (url, title, snippet, provider). +- `web_scrape` — fetch specific URLs; returns clean page content and metadata per URL. + + + +- If the request names exact URLs, call `web_scrape` on them directly. +- If it does not, call `web_discover` first, pick the most relevant hits, then `web_scrape` those URLs to read them. +- Batch URLs into a single `web_scrape` call when reading several pages (up to its limit) instead of many one-URL calls. +- For "what changed" / monitoring requests, compare the freshly scraped values against the prior values already present in this conversation's earlier tool results, and report the concrete deltas (added, removed, changed old -> new). Do not claim a change you cannot point to. + + + +- Use only tools in ``. +- A `web_scrape` row with `status` other than `success` yielded no content — do not invent its content; report it as unavailable. +- Never fabricate facts, URLs, prices, or quotes. + + + +- Report uncertainty explicitly when evidence is incomplete or conflicting. +- Never present unverified claims as facts. + + + +- On tool failure, return `status=error` with a concise recovery `next_step`. +- On no useful evidence, return `status=blocked` with a narrower query or the URLs you still need. + + + +Return **only** one JSON object (no markdown/prose): +{ + "status": "success" | "partial" | "blocked" | "error", + "action_summary": string, + "evidence": { + "findings": string[], + "sources": string[], + "confidence": "high" | "medium" | "low" + }, + "next_step": string | null, + "missing_fields": string[] | null, + "assumptions": string[] | null +} + +Route-specific rules: +- `evidence.findings`: max 10 entries, each a single sentence stating one distinct fact or delta. Do not paste raw scraped pages. +- `evidence.sources`: max 10 URLs, one per finding when applicable. List each URL once. + diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/__init__.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/__init__.py new file mode 100644 index 000000000..e06e600b5 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/__init__.py @@ -0,0 +1 @@ +"""Intelligence-agent tools: the web.* capability verbs, generated from the registry.""" diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/index.py new file mode 100644 index 000000000..f0b088dc1 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/intelligence_agent/tools/index.py @@ -0,0 +1,28 @@ +"""``intelligence_agent`` tools: web.* verbs generated from the capability registry.""" + +from __future__ import annotations + +from typing import Any + +from langchain_core.tools import BaseTool + +from app.agents.chat.multi_agent_chat.shared.permissions import Ruleset +from app.capabilities.access.agent import build_capability_tools +from app.capabilities.web.discover.definition import WEB_DISCOVER +from app.capabilities.web.scrape.definition import WEB_SCRAPE + +NAME = "intelligence_agent" + +RULESET = Ruleset(origin=NAME, rules=[]) + +_CI_VERBS = [WEB_DISCOVER, WEB_SCRAPE] + + +def load_tools( + *, dependencies: dict[str, Any] | None = None, **kwargs: Any +) -> list[BaseTool]: + d = {**(dependencies or {}), **kwargs} + return build_capability_tools( + workspace_id=d.get("workspace_id"), + capabilities=_CI_VERBS, + ) diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/registry.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/registry.py index c48b7f7ac..652ca079e 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/registry.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/registry.py @@ -15,6 +15,9 @@ from app.agents.chat.multi_agent_chat.constants import ( from app.agents.chat.multi_agent_chat.subagents.builtins.deliverables.agent import ( build_subagent as build_deliverables_subagent, ) +from app.agents.chat.multi_agent_chat.subagents.builtins.intelligence_agent.agent import ( + build_subagent as build_intelligence_agent_subagent, +) from app.agents.chat.multi_agent_chat.subagents.builtins.knowledge_base.agent import ( build_subagent as build_knowledge_base_subagent, ) @@ -99,6 +102,7 @@ SUBAGENT_BUILDERS_BY_NAME: dict[str, SubagentBuilder] = { "dropbox": build_dropbox_subagent, "gmail": build_gmail_subagent, "google_drive": build_google_drive_subagent, + "intelligence_agent": build_intelligence_agent_subagent, "jira": build_jira_subagent, "knowledge_base": build_knowledge_base_subagent, "linear": build_linear_subagent, diff --git a/surfsense_backend/tests/unit/agents/multi_agent_chat/test_subagent_composition.py b/surfsense_backend/tests/unit/agents/multi_agent_chat/test_subagent_composition.py index 157f1703b..68ae05bbd 100644 --- a/surfsense_backend/tests/unit/agents/multi_agent_chat/test_subagent_composition.py +++ b/surfsense_backend/tests/unit/agents/multi_agent_chat/test_subagent_composition.py @@ -19,7 +19,7 @@ from app.agents.chat.multi_agent_chat.subagents.registry import ( pytestmark = pytest.mark.unit -# The full specialist roster the main agent composes from: 4 builtins + 15 +# The full specialist roster the main agent composes from: 5 builtins + 15 # connector routes. Adding/removing a specialist is a deliberate product change # and must be reflected here. _EXPECTED_SUBAGENTS = frozenset( @@ -33,6 +33,7 @@ _EXPECTED_SUBAGENTS = frozenset( "dropbox", "gmail", "google_drive", + "intelligence_agent", "jira", "knowledge_base", "linear",