feat: add @agentic/reddit package

pull/706/head
Travis Fischer 2025-04-09 19:22:35 +07:00
rodzic fa4c88f0df
commit c9fda50c09
11 zmienionych plików z 594 dodań i 1 usunięć

Wyświetl plik

@ -83,6 +83,7 @@
"tools/polygon",
"tools/predict-leads",
"tools/proxycurl",
"tools/reddit",
"tools/rocketreach",
"tools/searxng",
"tools/serpapi",

Wyświetl plik

@ -0,0 +1,45 @@
---
title: Reddit
description: Basic readonly Reddit API for getting top/hot/new/rising posts from subreddits.
---
- package: `@agentic/reddit`
- exports: `class RedditClient`, `namespace reddit`
- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/reddit/src/reddit-client.ts)
- [reddit api docs](https://old.reddit.com/dev/api)
<Note>
This client uses Reddit's free, publicly accessible legacy JSON API aimed at
RSS feeds, so no API key or auth is required. With that being said, Reddit
does impose rate limits on the API, so be considerate.
</Note>
## Install
<CodeGroup>
```bash npm
npm install @agentic/reddit
```
```bash yarn
yarn add @agentic/reddit
```
```bash pnpm
pnpm add @agentic/reddit
```
</CodeGroup>
## Usage
```ts
import { RedditClient } from '@agentic/reddit'
const reddit = new RedditClient()
const result = await reddit.getSubredditPosts({
subreddit: 'AskReddit',
type: 'hot',
limit: 10
})
```

Wyświetl plik

@ -0,0 +1,45 @@
{
"name": "@agentic/reddit",
"version": "0.1.0",
"description": "Agentic SDK for Reddit.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/transitive-bullshit/agentic.git",
"directory": "packages/reddit"
},
"type": "module",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"clean": "del dist",
"test": "run-s test:*",
"test:lint": "eslint .",
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/core": "workspace:*",
"ky": "catalog:",
"p-throttle": "catalog:"
},
"peerDependencies": {
"zod": "catalog:"
},
"publishConfig": {
"access": "public"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# Agentic
**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.**
## License
MIT © [Travis Fischer](https://x.com/transitive_bs)

Wyświetl plik

@ -0,0 +1 @@
export * from './reddit-client'

Wyświetl plik

@ -0,0 +1,452 @@
import {
aiFunction,
AIFunctionsProvider,
pick,
sanitizeSearchParams
} from '@agentic/core'
import defaultKy, { type KyInstance } from 'ky'
import { z } from 'zod'
export namespace reddit {
export const BASE_URL = 'https://www.reddit.com'
export interface Post {
id: string
name: string // name is `t3_<id>`
title: string
subreddit: string
selftext?: string
author: string
author_fullname: string
url: string
permalink: string
thumbnail?: string
thumbnail_width?: number
thumbnail_height?: number
score: number
ups: number
downs: number
num_comments: number
created_utc: number
is_self: boolean
is_video: boolean
}
export interface FullPost {
id: string
name: string
author: string
title: string
subreddit: string
subreddit_name_prefixed: string
score: number
approved_at_utc: string | null
selftext?: string
author_fullname: string
is_self: boolean
saved: boolean
url: string
permalink: string
mod_reason_title: string | null
gilded: number
clicked: boolean
link_flair_richtext: any[]
hidden: boolean
pwls: number
link_flair_css_class: string
downs: number
thumbnail_height: any
top_awarded_type: any
hide_score: boolean
quarantine: boolean
link_flair_text_color: string
upvote_ratio: number
author_flair_background_color: any
subreddit_type: string
ups: number
total_awards_received: number
media_embed?: any
secure_media_embed?: any
thumbnail_width: any
author_flair_template_id: any
is_original_content: boolean
user_reports: any[]
secure_media: any
is_reddit_media_domain: boolean
is_meta: boolean
category: any
link_flair_text: string
can_mod_post: boolean
approved_by: any
is_created_from_ads_ui: boolean
author_premium: boolean
thumbnail?: string
edited: boolean
author_flair_css_class: any
author_flair_richtext: any[]
gildings?: any
content_categories: any
mod_note: any
created: number
link_flair_type: string
wls: number
removed_by_category: any
banned_by: any
author_flair_type: string
domain: string
allow_live_comments: boolean
selftext_html: string
likes: any
suggested_sort: any
banned_at_utc: any
view_count: any
archived: boolean
no_follow: boolean
is_crosspostable: boolean
pinned: boolean
over_18: boolean
all_awardings: any[]
awarders: any[]
media_only: boolean
link_flair_template_id: string
can_gild: boolean
spoiler: boolean
locked: boolean
author_flair_text: any
treatment_tags: any[]
visited: boolean
removed_by: any
num_reports: any
distinguished: any
subreddit_id: string
author_is_blocked: boolean
mod_reason_by: any
removal_reason: any
link_flair_background_color: string
is_robot_indexable: boolean
report_reasons: any
discussion_type: any
num_comments: number
send_replies: boolean
contest_mode: boolean
mod_reports: any[]
author_patreon_flair: boolean
author_flair_text_color: any
stickied: boolean
subreddit_subscribers: number
created_utc: number
num_crossposts: number
media?: any
is_video: boolean
// preview images
preview?: {
enabled: boolean
images: Array<{
id: string
source: Image
resolutions: Image[]
variants?: Record<
string,
{
id: string
source: Image
resolutions: Image[]
}
>
}>
}
}
export interface Image {
url: string
width: number
height: number
}
export interface PostT3 {
kind: 't3'
data: FullPost
}
export interface PostListingResponse {
kind: 'Listing'
data: {
after: string
dist: number
modhash: string
geo_filter?: null
children: PostT3[]
}
before?: null
}
export type PostFilter = 'hot' | 'top' | 'new' | 'rising'
export type GeoFilter =
| 'GLOBAL'
| 'US'
| 'AR'
| 'AU'
| 'BG'
| 'CA'
| 'CL'
| 'CO'
| 'HR'
| 'CZ'
| 'FI'
| 'FR'
| 'DE'
| 'GR'
| 'HU'
| 'IS'
| 'IN'
| 'IE'
| 'IT'
| 'JP'
| 'MY'
| 'MX'
| 'NZ'
| 'PH'
| 'PL'
| 'PT'
| 'PR'
| 'RO'
| 'RS'
| 'SG'
| 'ES'
| 'SE'
| 'TW'
| 'TH'
| 'TR'
| 'GB'
| 'US_WA'
| 'US_DE'
| 'US_DC'
| 'US_WI'
| 'US_WV'
| 'US_HI'
| 'US_FL'
| 'US_WY'
| 'US_NH'
| 'US_NJ'
| 'US_NM'
| 'US_TX'
| 'US_LA'
| 'US_NC'
| 'US_ND'
| 'US_NE'
| 'US_TN'
| 'US_NY'
| 'US_PA'
| 'US_CA'
| 'US_NV'
| 'US_VA'
| 'US_CO'
| 'US_AK'
| 'US_AL'
| 'US_AR'
| 'US_VT'
| 'US_IL'
| 'US_GA'
| 'US_IN'
| 'US_IA'
| 'US_OK'
| 'US_AZ'
| 'US_ID'
| 'US_CT'
| 'US_ME'
| 'US_MD'
| 'US_MA'
| 'US_OH'
| 'US_UT'
| 'US_MO'
| 'US_MN'
| 'US_MI'
| 'US_RI'
| 'US_KS'
| 'US_MT'
| 'US_MS'
| 'US_SC'
| 'US_KY'
| 'US_OR'
| 'US_SD'
export type TimePeriod = 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'
export type GetSubredditPostsOptions = {
subreddit: string
type?: PostFilter
// Pagination size and offset (count)
limit?: number
count?: number
// Pagination by fullnames of posts
before?: string
after?: string
/**
* Geographical filter. Only applicable to 'hot' posts.
*/
geo?: GeoFilter
/**
* Filter by time period. Only applicable to 'top' posts.
*/
time?: TimePeriod
}
export interface PostListingResult {
subreddit: string
type: PostFilter
geo?: GeoFilter
time?: TimePeriod
posts: Post[]
}
}
/**
* Basic readonly Reddit API for fetching top/hot/new/rising posts from subreddits.
*
* Uses Reddit's legacy JSON API aimed at RSS feeds.
*
* @see https://old.reddit.com/dev/api
*/
export class RedditClient extends AIFunctionsProvider {
protected readonly ky: KyInstance
protected readonly baseUrl: string
constructor({
baseUrl = reddit.BASE_URL,
userAgent = 'agentic-reddit-client/1.0.0',
ky = defaultKy
}: {
baseUrl?: string
userAgent?: string
ky?: KyInstance
} = {}) {
super()
this.baseUrl = baseUrl
this.ky = ky.extend({
prefixUrl: this.baseUrl,
headers: {
'User-Agent': userAgent
}
})
}
/**
* Fetches posts from a subreddit.
*
* @see https://old.reddit.com/dev/api/#GET_hot
*/
@aiFunction({
name: 'reddit_get_subreddit_posts',
description: 'Fetches posts from a subreddit.',
inputSchema: z.object({
subreddit: z.string().describe('The subreddit to fetch posts from.'),
type: z
.union([
z.literal('hot'),
z.literal('top'),
z.literal('new'),
z.literal('rising')
])
.optional()
.describe('Type of posts to fetch (defaults to "hot").'),
limit: z
.number()
.int()
.max(100)
.optional()
.describe('Max number of posts to return (defaults to 5).'),
count: z
.number()
.int()
.optional()
.describe('Number of posts to offset by (defaults to 0).'),
time: z
.union([
z.literal('hour'),
z.literal('day'),
z.literal('week'),
z.literal('month'),
z.literal('year'),
z.literal('all')
])
.optional()
.describe(
'Time period to filter posts by (defaults to "all"). Only applicable to "top" posts type.'
)
})
})
async getSubredditPosts(
subredditOrOpts: string | reddit.GetSubredditPostsOptions
): Promise<reddit.PostListingResult> {
const params =
typeof subredditOrOpts === 'string'
? { subreddit: subredditOrOpts }
: subredditOrOpts
const { subreddit, type = 'hot', limit = 5, geo, time, ...opts } = params
const res = await this.ky
.get(`r/${subreddit}/${type}.json`, {
searchParams: sanitizeSearchParams({
...opts,
limit,
g: type === 'hot' ? geo : undefined,
t: type === 'top' ? time : undefined
})
})
.json<reddit.PostListingResponse>()
return {
subreddit,
type,
geo: type === 'hot' ? geo : undefined,
time: type === 'top' ? time : undefined,
posts: res.data.children.map((child) => {
const post = child.data
// Trim the post data to only include the bare minimum
// TODO: add preview images
// TODO: add video media info
return {
...pick(
post,
'id',
'name',
'title',
'subreddit',
'selftext',
'author',
'author_fullname',
'url',
'permalink',
'thumbnail',
'thumbnail_width',
'thumbnail_height',
'score',
'ups',
'downs',
'num_comments',
'created_utc',
'is_self',
'is_video'
),
permalink: `${this.baseUrl}${post.permalink}`,
thumbnail:
post.thumbnail !== 'self' &&
post.thumbnail !== 'default' &&
post.thumbnail !== 'spoiler' &&
post.thumbnail !== 'nsfw'
? post.thumbnail
: undefined
}
})
}
}
}

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@fisch0920/config/tsconfig-node",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

Wyświetl plik

@ -62,6 +62,7 @@
"@agentic/polygon": "workspace:*",
"@agentic/predict-leads": "workspace:*",
"@agentic/proxycurl": "workspace:*",
"@agentic/reddit": "workspace:*",
"@agentic/rocketreach": "workspace:*",
"@agentic/searxng": "workspace:*",
"@agentic/serpapi": "workspace:*",

Wyświetl plik

@ -27,6 +27,7 @@ export * from '@agentic/perigon'
export * from '@agentic/polygon'
export * from '@agentic/predict-leads'
export * from '@agentic/proxycurl'
export * from '@agentic/reddit'
export * from '@agentic/rocketreach'
export * from '@agentic/searxng'
export * from '@agentic/serpapi'

Wyświetl plik

@ -976,6 +976,21 @@ importers:
specifier: 'catalog:'
version: 3.24.2
packages/reddit:
dependencies:
'@agentic/core':
specifier: workspace:*
version: link:../core
ky:
specifier: 'catalog:'
version: 1.8.0
p-throttle:
specifier: 'catalog:'
version: 6.2.0
zod:
specifier: 'catalog:'
version: 3.24.2
packages/rocketreach:
dependencies:
'@agentic/core':
@ -1146,6 +1161,9 @@ importers:
'@agentic/proxycurl':
specifier: workspace:*
version: link:../proxycurl
'@agentic/reddit':
specifier: workspace:*
version: link:../reddit
'@agentic/rocketreach':
specifier: workspace:*
version: link:../rocketreach
@ -5063,7 +5081,6 @@ packages:
libsql@0.4.7:
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lilconfig@3.1.3:

Wyświetl plik

@ -208,6 +208,7 @@ Full docs are available at [agentic.so](https://agentic.so).
| [Polygon](https://polygon.io) | `@agentic/polygon` | [docs](https://agentic.so/tools/polygon) | Stock market and company financial data. |
| [PredictLeads](https://predictleads.com) | `@agentic/predict-leads` | [docs](https://agentic.so/tools/predict-leads) | In-depth company data including signals like fundraising events, hiring news, product launches, technologies used, etc. |
| [Proxycurl](https://nubela.co/proxycurl) | `@agentic/proxycurl` | [docs](https://agentic.so/tools/proxycurl) | People and company data from LinkedIn & Crunchbase. |
| [Reddit](https://old.reddit.com/dev/api) | `@agentic/reddit` | [docs](https://agentic.so/tools/reddit) | Basic readonly Reddit API for getting top/hot/new/rising posts from subreddits. |
| [RocketReach](https://rocketreach.co/api/v2/docs) | `@agentic/rocketreach` | [docs](https://agentic.so/tools/rocketreach) | B2B person and company enrichment API. |
| [Searxng](https://docs.searxng.org) | `@agentic/searxng` | [docs](https://agentic.so/tools/searxng) | OSS meta search engine capable of searching across many providers like Reddit, Google, Brave, Arxiv, Genius, IMDB, Rotten Tomatoes, Wikidata, Wolfram Alpha, YouTube, GitHub, [etc](https://docs.searxng.org/user/configured_engines.html#configured-engines). |
| [SerpAPI](https://serpapi.com/search-api) | `@agentic/serpapi` | [docs](https://agentic.so/tools/serpapi) | Lightweight wrapper around SerpAPI for Google search. |