import z from 'zod'; import type { ResearchAction } from './types.js'; import type { Chunk, SearchResultsResearchBlock } from '../types.js'; import { searchSearxng } from '../searxng.js'; const schema = z.object({ queries: z.array(z.string()).max(6).describe('List of social search queries'), }); const socialSearchAction: ResearchAction = { name: 'social_search', schema, getToolDescription: () => 'Use this tool to perform social media searches for posts, discussions, and trends. Provide up to 6 queries in the user\'s language.', getDescription: () => 'Use this tool to perform social media searches for posts, discussions, and trends. Up to 6 queries in the user\'s language.', enabled: (config) => config.sources.includes('discussions') && config.classification.classification.skipSearch === false && config.classification.classification.discussionSearch === true, execute: async (input, additionalConfig) => { input.queries = input.queries.slice(0, 6); const researchBlock = additionalConfig.session.getBlock(additionalConfig.researchBlockId); if (researchBlock && researchBlock.type === 'research') { researchBlock.data.subSteps.push({ id: crypto.randomUUID(), type: 'searching', searching: input.queries, }); additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ { op: 'replace', path: '/data/subSteps', value: researchBlock.data.subSteps }, ]); } const searchResultsBlockId = crypto.randomUUID(); let searchResultsEmitted = false; const results: Chunk[] = []; const search = async (q: string) => { const [page1, page2] = await Promise.all([ searchSearxng(q, { categories: ['social_media'], pageno: 1, }), searchSearxng(q, { categories: ['social_media'], pageno: 2, }), ]); const seenUrls = new Set(); const allResults = [...(page1.results ?? []), ...(page2.results ?? [])].filter((r) => { if (!r.url || seenUrls.has(r.url)) return false; seenUrls.add(r.url); return true; }); const res = { results: allResults }; const resultChunks: Chunk[] = res.results.map((r) => ({ content: r.content || r.title, metadata: { title: r.title, url: r.url }, })); results.push(...resultChunks); if (!searchResultsEmitted && researchBlock && researchBlock.type === 'research') { searchResultsEmitted = true; researchBlock.data.subSteps.push({ id: searchResultsBlockId, type: 'search_results', reading: resultChunks, }); additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ { op: 'replace', path: '/data/subSteps', value: researchBlock.data.subSteps }, ]); } else if (searchResultsEmitted && researchBlock && researchBlock.type === 'research') { const subStepIndex = researchBlock.data.subSteps.findIndex((s) => s.id === searchResultsBlockId); const subStep = researchBlock.data.subSteps[subStepIndex] as SearchResultsResearchBlock | undefined; if (subStep) { subStep.reading.push(...resultChunks); additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [ { op: 'replace', path: '/data/subSteps', value: researchBlock.data.subSteps }, ]); } } }; await Promise.all(input.queries.map(search)); return { type: 'search_results', results }; }, }; export default socialSearchAction;