feat: add @agentic/youtube package

pull/706/head
Travis Fischer 2025-04-09 20:28:07 +07:00
rodzic 2fcfa6a2ad
commit 5d0d30d45a
8 zmienionych plików z 362 dodań i 0 usunięć

Wyświetl plik

@ -76,6 +76,7 @@
"@agentic/wikidata": "workspace:*",
"@agentic/wikipedia": "workspace:*",
"@agentic/wolfram-alpha": "workspace:*",
"@agentic/youtube": "workspace:*",
"@agentic/zoominfo": "workspace:*",
"@e2b/code-interpreter": "catalog:"
},

Wyświetl plik

@ -41,4 +41,5 @@ export * from '@agentic/weather'
export * from '@agentic/wikidata'
export * from '@agentic/wikipedia'
export * from '@agentic/wolfram-alpha'
export * from '@agentic/youtube'
export * from '@agentic/zoominfo'

Wyświetl plik

@ -0,0 +1,45 @@
{
"name": "@agentic/youtube",
"version": "7.6.3",
"description": "Agentic SDK for the YouTube data API.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/transitive-bullshit/agentic.git",
"directory": "packages/youtube"
},
"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 './youtube-client'

Wyświetl plik

@ -0,0 +1,267 @@
import {
aiFunction,
AIFunctionsProvider,
assert,
getEnv,
sanitizeSearchParams
} from '@agentic/core'
import defaultKy, { type KyInstance } from 'ky'
import { z } from 'zod'
export namespace youtube {
export const API_BASE_URL = 'https://www.googleapis.com/youtube/v3'
export interface SearchOptions {
query: string
maxResults?: number
pageToken?: string
channelId?: string
channelType?: 'any' | 'show'
eventType?: 'live' | 'completed' | 'upcoming'
location?: string
locationRadius?: string
order?:
| 'relevance'
| 'date'
| 'rating'
| 'title'
| 'videoCount'
| 'viewCount'
// The value is an RFC 3339 formatted date-time value (1970-01-01T00:00:00Z).
publishedAfter?: string
publishedBefore?: string
// The regionCode parameter instructs the API to return search results for videos that can be viewed in the specified country. The parameter value is an ISO 3166-1 alpha-2 country code.
regionCode?: string
relevanceLanguage?: string
safeSearch?: 'moderate' | 'none' | 'strict'
topicId?: string
videoCaption?: 'any' | 'closedCaption' | 'none'
videoCategoryId?: string
videoDefinition?: 'any' | 'high' | 'standard'
videoDimension?: '2d' | '3d' | 'any'
videoDuration?: 'any' | 'long' | 'medium' | 'short'
videoEmbeddable?: 'any' | 'true'
videoLicense?: 'any' | 'creativeCommon' | 'youtube'
videoPaidProductPlacement?: 'any' | 'true'
videoSyndicated?: 'any' | 'true'
videoType?: 'any' | 'episode' | 'movie'
}
export type SearchType = 'video' | 'channel' | 'playlist'
export interface SearchVideosResult {
videoId: string
title: string
description: string
thumbnail: string
channelId: string
channelTitle: string
publishedAt: string
url: string
}
export interface SearchChannelsResult {
channelId: string
title: string
description: string
thumbnail: string
publishedAt: string
url: string
}
export type SearchResponse<T extends SearchType> = {
results: T extends 'video'
? SearchVideosResult[]
: T extends 'channel'
? SearchChannelsResult[]
: never
totalResults: number
prevPageToken?: string
nextPageToken?: string
}
export type SearchVideosResponse = SearchResponse<'video'>
export type SearchChannelsResponse = SearchResponse<'channel'>
}
/**
* YouTube data API v3 client.
*
* @see https://developers.google.com/youtube/v3
*/
export class YouTubeClient extends AIFunctionsProvider {
protected readonly ky: KyInstance
protected readonly apiKey: string
protected readonly apiBaseUrl: string
constructor({
apiKey = getEnv('YOUTUBE_API_KEY'),
apiBaseUrl = youtube.API_BASE_URL,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
ky?: KyInstance
} = {}) {
assert(
apiKey,
'YouTubeClient missing required "apiKey" (defaults to "YOUTUBE_API_KEY")'
)
super()
this.apiKey = apiKey
this.apiBaseUrl = apiBaseUrl
this.ky = ky.extend({
prefixUrl: this.apiBaseUrl
})
}
/**
* Searches for videos on YouTube.
*
* @see https://developers.google.com/youtube/v3/docs/search/list
*/
@aiFunction({
name: 'youtube_search_videos',
description: 'Searches for videos on YouTube.',
inputSchema: z.object({
query: z.string().describe(`The query to search for.
Your request can optionally use the Boolean NOT (-) and OR (|) operators to exclude videos or to find videos that are associated with one of several search terms. For example, to search for videos matching either "boating" or "sailing", set the query parameter value to boating|sailing. Similarly, to search for videos matching either "boating" or "sailing" but not "fishing", set the query parameter value to boating|sailing -fishing.`),
maxResults: z
.number()
.int()
.optional()
.describe('The maximum number of results to return (defaults to 5).')
})
})
async searchVideos(
queryOrOpts: string | youtube.SearchOptions
): Promise<youtube.SearchVideosResponse> {
const opts =
typeof queryOrOpts === 'string' ? { query: queryOrOpts } : queryOrOpts
const data = await this._search({
...opts,
type: 'video'
})
const results = (data.items || [])
.map((item: any) => {
const snippet = item.snippet
if (!snippet) return null
const videoId = item.id?.videoId
if (!videoId) return null
const thumbnails = snippet.thumbnails
if (!thumbnails) return null
return {
videoId,
title: snippet.title,
description: snippet.description,
// https://i.ytimg.com/vi/MRtg6A1f2Ko/maxresdefault.jpg
thumbnail:
thumbnails.high?.url ||
thumbnails.medium?.url ||
thumbnails.default?.url ||
`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`,
channelId: snippet.channelId,
channelTitle: snippet.channelTitle,
publishedAt: snippet.publishedAt,
url: `https://www.youtube.com/watch?v=${videoId}`
}
})
.filter(Boolean)
return {
results,
totalResults: data.pageInfo?.totalResults || 0,
prevPageToken: data.prevPageToken,
nextPageToken: data.nextPageToken
}
}
/**
* Searches for channels on YouTube.
*
* @see https://developers.google.com/youtube/v3/docs/search/list
*/
@aiFunction({
name: 'youtube_search_channels',
description: 'Searches for channels on YouTube.',
inputSchema: z.object({
query: z.string().describe('The query to search for.'),
maxResults: z
.number()
.int()
.optional()
.describe('The maximum number of results to return (defaults to 5).')
})
})
async searchChannels(
queryOrOpts: string | youtube.SearchOptions
): Promise<youtube.SearchChannelsResponse> {
const opts =
typeof queryOrOpts === 'string' ? { query: queryOrOpts } : queryOrOpts
const data = await this._search({
...opts,
type: 'channel'
})
const results = (data.items || [])
.map((item: any) => {
const snippet = item.snippet
if (!snippet) return null
const channelId = item.id?.channelId
if (!channelId) return null
const thumbnails = snippet.thumbnails
if (!thumbnails) return null
return {
channelId,
title: snippet.title,
description: snippet.description,
thumbnail:
thumbnails.high?.url ||
thumbnails.medium?.url ||
thumbnails.default?.url,
publishedAt: snippet.publishedAt,
url: `https://www.youtube.com/channel/${channelId}`
}
})
.filter(Boolean)
return {
results,
totalResults: data.pageInfo?.totalResults || 0,
prevPageToken: data.prevPageToken,
nextPageToken: data.nextPageToken
}
}
protected async _search(
opts: youtube.SearchOptions & {
type: youtube.SearchType
}
) {
const { query, ...params } = opts
return this.ky
.get('search', {
searchParams: sanitizeSearchParams({
q: query,
part: 'snippet',
maxResults: 5,
...params,
key: this.apiKey
})
})
.json<any>()
}
}

Wyświetl plik

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

Wyświetl plik

@ -1203,6 +1203,9 @@ importers:
'@agentic/wolfram-alpha':
specifier: workspace:*
version: link:../wolfram-alpha
'@agentic/youtube':
specifier: workspace:*
version: link:../youtube
'@agentic/zoominfo':
specifier: workspace:*
version: link:../zoominfo
@ -1331,6 +1334,21 @@ importers:
specifier: 'catalog:'
version: 0.2.0-beta.3(zod-to-json-schema@3.24.5(zod@3.24.2))
packages/youtube:
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/zoominfo:
dependencies:
'@agentic/core':