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 e6c5b4975..96413e553 100644 --- a/surfsense_backend/app/agents/chat/multi_agent_chat/constants.py +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/constants.py @@ -28,6 +28,7 @@ SUBAGENT_TO_REQUIRED_CONNECTOR_MAP: dict[str, frozenset[str]] = { "knowledge_base": frozenset(), "web_crawler": frozenset(), "youtube": frozenset(), + "google_maps": 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/google_maps/__init__.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/__init__.py new file mode 100644 index 000000000..aef5d5e5f --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/__init__.py @@ -0,0 +1 @@ +"""``google_maps`` builtin subagent: structured Google Maps place/review data.""" diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/agent.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/agent.py new file mode 100644 index 000000000..df5d9e8d4 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/agent.py @@ -0,0 +1,43 @@ +"""``google_maps`` 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 "Pulls structured data from Google Maps — places and their reviews." + ) + 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/google_maps/description.md b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/description.md new file mode 100644 index 000000000..c76722a5c --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/description.md @@ -0,0 +1,2 @@ +Google Maps specialist: pulls structured data from Google Maps — finds places by search query (optionally scoped to a location), or resolves a Maps URL / place ID into place details (name, address, category, phone, website, rating, review count, coordinates, opening hours), and fetches a place's reviews (author, text, star rating, owner response, dates). Also compares fresh Maps data against earlier findings in this chat. +Use whenever the task involves Google Maps places, local businesses, or a google.com/maps link. Triggers include "find near/in X", "get details for this place", "phone/address/hours/rating of X", "how many reviews", "get the reviews for this place", "what are people saying about this business", and recurring "what changed" checks on Maps data. Not for general web pages (use the web crawling specialist) or YouTube (use the YouTube specialist). diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/system_prompt.md b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/system_prompt.md new file mode 100644 index 000000000..a048ab982 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/system_prompt.md @@ -0,0 +1,62 @@ +You are the SurfSense Google Maps sub-agent. +You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. + + +Answer the delegated question from live Google Maps data gathered with your verbs, including "what changed" comparisons against evidence already in this conversation. + + + +- `google_maps_scrape` +- `google_maps_reviews` + + + +- Finding places on a topic: call `google_maps_scrape` with `search_queries`, adding `location` to scope them (e.g. "coffee shops" in "Austin, USA"). +- Known place links or IDs: call `google_maps_scrape` with the links in `urls` or the IDs in `place_ids`. +- Need richer detail (opening hours, popular times, extra contact info): set `include_details=true`. +- Reviews / sentiment on specific places: call `google_maps_reviews` with the place `urls` or `place_ids`. +- Batch multiple queries, URLs, or place IDs into one call rather than many single-item calls. +- "What changed" / monitoring: pull the current values, compare against the prior values in this conversation's earlier tool results, and report concrete deltas (added, removed, old -> new). + + + +- Use only tools in ``. +- An item whose `status` is not `success` returned no data — report it unavailable, never invent it. +- Report only deltas you can point to in the evidence. Never fabricate facts, counts, quotes, ratings, or URLs. + + + +- Do not generate deliverables or perform connector mutations; return findings for the supervisor to act on. +- Non-Maps web pages belong to the web crawling specialist; YouTube belongs to the YouTube specialist. + + + +- Report uncertainty explicitly when evidence is incomplete or conflicting. +- Never present unverified claims as facts. + + + +- Underspecified request — no usable search query, URL, or place ID — return `status=blocked` with the missing fields. +- Tool failure: return `status=error` with a concise recovery `next_step`. +- No useful evidence: return `status=blocked` with a narrower query or the URLs/IDs 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 payloads. +- `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/google_maps/tools/__init__.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/tools/index.py b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/tools/index.py new file mode 100644 index 000000000..bf2f7eb20 --- /dev/null +++ b/surfsense_backend/app/agents/chat/multi_agent_chat/subagents/builtins/google_maps/tools/index.py @@ -0,0 +1,28 @@ +"""``google_maps`` sub-agent tools: the Google Maps scrape + reviews capability verbs.""" + +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.core.access.agent import build_capability_tools +from app.capabilities.google_maps.reviews.definition import GOOGLE_MAPS_REVIEWS +from app.capabilities.google_maps.scrape.definition import GOOGLE_MAPS_SCRAPE + +NAME = "google_maps" + +RULESET = Ruleset(origin=NAME, rules=[]) + +_CI_VERBS = [GOOGLE_MAPS_SCRAPE, GOOGLE_MAPS_REVIEWS] + + +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 8fbd866dc..7ae689eb7 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.google_maps.agent import ( + build_subagent as build_google_maps_subagent, +) from app.agents.chat.multi_agent_chat.subagents.builtins.knowledge_base.agent import ( build_subagent as build_knowledge_base_subagent, ) @@ -102,6 +105,7 @@ SUBAGENT_BUILDERS_BY_NAME: dict[str, SubagentBuilder] = { "dropbox": build_dropbox_subagent, "gmail": build_gmail_subagent, "google_drive": build_google_drive_subagent, + "google_maps": build_google_maps_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 873fe26c5..63978e7d2 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: 5 builtins + 15 +# The full specialist roster the main agent composes from: 6 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", + "google_maps", "jira", "knowledge_base", "linear",