feat(chat): add intelligence_agent subagent over capability verbs

This commit is contained in:
CREDO23 2026-07-02 00:17:22 +02:00
parent 110beb4dd1
commit 7ad83760b5
9 changed files with 135 additions and 1 deletions

View file

@ -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"}),

View file

@ -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,
)

View file

@ -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.

View file

@ -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.
<goal>
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.
</goal>
<available_tools>
- `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.
</available_tools>
<playbook>
- 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.
</playbook>
<tool_policy>
- Use only tools in `<available_tools>`.
- 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.
</tool_policy>
<safety>
- Report uncertainty explicitly when evidence is incomplete or conflicting.
- Never present unverified claims as facts.
</safety>
<failure_policy>
- 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.
</failure_policy>
<output_contract>
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
}
<include snippet="output_contract_base"/>
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.
</output_contract>

View file

@ -0,0 +1 @@
"""Intelligence-agent tools: the web.* capability verbs, generated from the registry."""

View file

@ -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,
)

View file

@ -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,

View file

@ -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",