feat(reddit): implement Reddit scraping subagent and associated capabilities

- Added a new `reddit` subagent to scrape structured data from Reddit posts, comments, and users.
- Introduced `reddit.scrape` capability for fetching data using URLs and search queries.
- Implemented tools for scraping and parsing Reddit data, including handling pagination and rate limits.
- Created input/output models for the Reddit scraper to define request and response structures.
- Added documentation for the new Reddit scraping functionality and its usage.
- Integrated the Reddit subagent into the existing multi-agent chat framework.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-07-04 17:31:11 -07:00
parent 701f888b9e
commit ff2e5f390f
35 changed files with 510 additions and 15 deletions

View file

@ -30,6 +30,7 @@ SUBAGENT_TO_REQUIRED_CONNECTOR_MAP: dict[str, frozenset[str]] = {
"youtube": frozenset(),
"google_maps": frozenset(),
"google_search": frozenset(),
"reddit": frozenset(),
"airtable": frozenset({"AIRTABLE_CONNECTOR"}),
"calendar": frozenset({"GOOGLE_CALENDAR_CONNECTOR"}),
"clickup": frozenset({"CLICKUP_CONNECTOR"}),

View file

@ -0,0 +1 @@
"""``reddit`` builtin subagent: structured Reddit posts, comments, and users."""

View file

@ -0,0 +1,43 @@
"""``reddit`` 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 Reddit posts, comments, subreddits, and users."
)
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 @@
Reddit specialist: pulls structured Reddit data — posts (title, body, score, upvote ratio, comment count, subreddit, author, flair, timestamps), their comment threads, and community/user metadata. Discovers posts by search query (optionally scoped to a single subreddit), or scrapes a known post, subreddit, or user URL as-is, with sort (hot/top/new/rising) and time-window controls. Also compares fresh Reddit results against earlier findings in this chat.
Use whenever the task is to find what people say on Reddit about a topic, gather posts/comments from a subreddit or user, discover discussion threads, or read community sentiment. Triggers include "search Reddit for X", "what does r/X say about Y", "find Reddit posts/threads about X", "top posts in r/X", and comparisons against earlier Reddit results in this chat. Not for general web pages (use the web crawling specialist), Google results (use the Google Search specialist), or YouTube (use the YouTube specialist).

View file

@ -0,0 +1,62 @@
You are the SurfSense Reddit sub-agent.
You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis.
<goal>
Answer the delegated question from live Reddit data gathered with your verb, comparing against earlier results already in this conversation when the task calls for it.
</goal>
<available_tools>
- `reddit_scrape`
</available_tools>
<playbook>
- Finding discussion on a topic: call `reddit_scrape` with `search_queries`; set `community` to scope the search to one subreddit (e.g. "python").
- Scraping a subreddit's listing: pass `community` with no `search_queries`, and tune `sort` (hot/top/new/rising) and `time_filter` for top/controversial.
- Scraping a specific post, subreddit, or user: pass its Reddit URL in `urls`.
- Reading comment sentiment: keep `skip_comments` false and raise `max_comments`; set `skip_comments` true when you only need posts (faster).
- Controlling volume: use `max_items` for the total cap, `max_posts` per target, `max_comments` per post.
- Batch multiple search terms into one call rather than many single-term calls.
- Comparison requests: pull the current results, compare against prior values already in this conversation's earlier tool results, and report concrete deltas (added, removed, score/rank changes).
</playbook>
<tool_policy>
- Use only tools in `<available_tools>`.
- Report only results present in the tool output. Never invent titles, URLs, scores, authors, or comment text.
</tool_policy>
<out_of_scope>
- Do not read arbitrary web pages — that belongs to the web crawling specialist.
- Do not generate deliverables or perform connector mutations; return findings for the supervisor to act on.
- Google results belong to the Google Search specialist; YouTube belongs to the YouTube specialist.
</out_of_scope>
<safety>
- Report uncertainty explicitly when evidence is incomplete or conflicting.
- Never present unverified claims as facts.
</safety>
<failure_policy>
- Underspecified request — no usable query, community, or URL — 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 scope 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 post, comment, or delta. Do not paste raw payloads.
- `evidence.sources`: max 10 Reddit URLs, one per finding when applicable. List each URL once.
</output_contract>

View file

@ -0,0 +1,27 @@
"""``reddit`` sub-agent tools: the Reddit scrape capability verb."""
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.reddit.scrape.definition import REDDIT_SCRAPE
NAME = "reddit"
RULESET = Ruleset(origin=NAME, rules=[])
_CI_VERBS = [REDDIT_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

@ -27,6 +27,9 @@ from app.agents.chat.multi_agent_chat.subagents.builtins.knowledge_base.agent im
from app.agents.chat.multi_agent_chat.subagents.builtins.memory.agent import (
build_subagent as build_memory_subagent,
)
from app.agents.chat.multi_agent_chat.subagents.builtins.reddit.agent import (
build_subagent as build_reddit_subagent,
)
from app.agents.chat.multi_agent_chat.subagents.builtins.web_crawler.agent import (
build_subagent as build_web_crawler_subagent,
)
@ -117,6 +120,7 @@ SUBAGENT_BUILDERS_BY_NAME: dict[str, SubagentBuilder] = {
"memory": build_memory_subagent,
"notion": build_notion_subagent,
"onedrive": build_onedrive_subagent,
"reddit": build_reddit_subagent,
"slack": build_slack_subagent,
"teams": build_teams_subagent,
"web_crawler": build_web_crawler_subagent,