Skip to content

Commit 82f951a

Browse files
Add Perplexity web search
1 parent 8f6bcae commit 82f951a

4 files changed

Lines changed: 230 additions & 6 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"@opentelemetry/sdk-node": "^0.57.1",
117117
"@opentelemetry/sdk-trace-base": "^1.30.1",
118118
"@opentelemetry/semantic-conventions": "^1.29.0",
119+
"@perplexity-ai/perplexity_ai": "^0.10.0",
119120
"@sinclair/typebox": "^0.34.41",
120121
"@slack/bolt": "^4.4.0",
121122
"@slack/web-api": "^7.9.3",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { expect } from 'chai';
2+
import { agentContextStorage } from '#agent/agentContextLocalStorage';
3+
import type { AgentContext } from '#shared/agent/agent.model';
4+
import { setupConditionalLoggerOutput } from '#test/testUtils';
5+
import { Perplexity } from './perplexity';
6+
7+
describe('Perplexity Integration Tests', () => {
8+
setupConditionalLoggerOutput();
9+
10+
let perplexity: Perplexity;
11+
let mockAgentContext: AgentContext;
12+
13+
beforeEach(() => {
14+
perplexity = new Perplexity();
15+
mockAgentContext = {
16+
agentId: 'test-agent-id',
17+
memory: {},
18+
} as AgentContext;
19+
});
20+
21+
describe('research', () => {
22+
it('should perform research and return results when saveToMemory is false', async function () {
23+
this.timeout(30000);
24+
25+
const researchTask = 'What are the latest TypeScript 5.9 features?';
26+
27+
const result = await agentContextStorage.run(mockAgentContext, async () => {
28+
return await perplexity.research(researchTask, false);
29+
});
30+
31+
expect(result).to.be.a('string');
32+
expect(result.length).to.be.greaterThan(0);
33+
expect(result).to.include('TypeScript');
34+
});
35+
36+
it('should save research to memory and return memory key when saveToMemory is true', async function () {
37+
this.timeout(30000);
38+
39+
const researchTask = 'What is the capital of France?';
40+
41+
const memoryKey = await agentContextStorage.run(mockAgentContext, async () => {
42+
return await perplexity.research(researchTask, true);
43+
});
44+
45+
expect(memoryKey).to.be.a('string');
46+
expect(memoryKey).to.match(/^Perplexity-/);
47+
expect(mockAgentContext.memory[memoryKey]).to.be.a('string');
48+
expect(mockAgentContext.memory[memoryKey].length).to.be.greaterThan(0);
49+
});
50+
51+
it('should handle complex research queries', async function () {
52+
this.timeout(30000);
53+
54+
const researchTask = 'Compare the performance characteristics of Node.js async/await versus callbacks in 2025';
55+
56+
const result = await agentContextStorage.run(mockAgentContext, async () => {
57+
return await perplexity.research(researchTask, false);
58+
});
59+
60+
expect(result).to.be.a('string');
61+
expect(result.length).to.be.greaterThan(100);
62+
});
63+
64+
it('should handle short queries', async function () {
65+
this.timeout(30000);
66+
67+
const researchTask = 'TypeScript';
68+
69+
const result = await agentContextStorage.run(mockAgentContext, async () => {
70+
return await perplexity.research(researchTask, false);
71+
});
72+
73+
expect(result).to.be.a('string');
74+
expect(result.length).to.be.greaterThan(0);
75+
});
76+
});
77+
78+
describe('search', () => {
79+
it('should perform basic web search and return results', async function () {
80+
this.timeout(30000);
81+
82+
const result = await agentContextStorage.run(mockAgentContext, async () => {
83+
return await perplexity.search('TypeScript 5.9 features', {
84+
max_results: 5,
85+
});
86+
});
87+
88+
expect(result).to.have.property('results');
89+
expect(result.results).to.be.an('array');
90+
expect(result.results.length).to.be.greaterThan(0);
91+
expect(result.results.length).to.be.at.most(5);
92+
93+
const firstResult = result.results[0];
94+
expect(firstResult).to.have.property('title');
95+
expect(firstResult).to.have.property('url');
96+
expect(firstResult.title).to.be.a('string');
97+
expect(firstResult.url).to.be.a('string');
98+
});
99+
100+
it('should support multi-query search', async function () {
101+
this.timeout(30000);
102+
103+
const result = await agentContextStorage.run(mockAgentContext, async () => {
104+
return await perplexity.search(['Node.js performance', 'JavaScript async patterns'], {
105+
max_results: 10,
106+
});
107+
});
108+
109+
expect(result.results).to.be.an('array');
110+
expect(result.results.length).to.be.greaterThan(0);
111+
});
112+
113+
it('should return snippets when requested', async function () {
114+
this.timeout(30000);
115+
116+
const result = await agentContextStorage.run(mockAgentContext, async () => {
117+
return await perplexity.search('React best practices 2025', {
118+
max_results: 3,
119+
return_snippets: true,
120+
});
121+
});
122+
123+
expect(result.results).to.be.an('array');
124+
expect(result.results.length).to.be.greaterThan(0);
125+
126+
const resultsWithSnippets = result.results.filter((r) => r.snippet);
127+
expect(resultsWithSnippets.length).to.be.greaterThan(0);
128+
});
129+
130+
it('should support country filtering', async function () {
131+
this.timeout(30000);
132+
133+
const result = await agentContextStorage.run(mockAgentContext, async () => {
134+
return await perplexity.search('local tech events', {
135+
max_results: 5,
136+
country: 'US',
137+
});
138+
});
139+
140+
expect(result.results).to.be.an('array');
141+
expect(result.results.length).to.be.greaterThan(0);
142+
});
143+
144+
it('should handle errors gracefully', async function () {
145+
this.timeout(30000);
146+
147+
await agentContextStorage.run(mockAgentContext, async () => {
148+
try {
149+
await perplexity.search('', {
150+
max_results: 5,
151+
});
152+
expect.fail('Should have thrown an error');
153+
} catch (error) {
154+
expect(error).to.exist;
155+
}
156+
});
157+
});
158+
});
159+
});

src/functions/web/perplexity.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import PerplexitySDK from '@perplexity-ai/perplexity_ai';
12
import { agentContext, llms } from '#agent/agentContextLocalStorage';
23
import { func, funcClass } from '#functionSchema/functionDecorators';
34
import { perplexityReasoningProLLM } from '#llm/services/perplexity-llm';
@@ -10,23 +11,50 @@ export interface PerplexityConfig {
1011
key: string;
1112
}
1213

14+
export interface PerplexitySearchOptions {
15+
query: string | string[];
16+
max_results?: number;
17+
country?: string;
18+
return_images?: boolean;
19+
return_snippets?: boolean;
20+
}
21+
22+
export interface PerplexitySearchResult {
23+
title: string;
24+
url: string;
25+
date?: string;
26+
snippet?: string;
27+
images?: string[];
28+
}
29+
30+
export interface PerplexitySearchResponse {
31+
results: PerplexitySearchResult[];
32+
}
33+
1334
@funcClass(__filename)
1435
export class Perplexity {
36+
private sdkClient: PerplexitySDK;
37+
38+
constructor() {
39+
// SDK automatically uses PERPLEXITY_API_KEY from environment
40+
this.sdkClient = new PerplexitySDK();
41+
}
42+
1543
/**
16-
* Calls Perplexity to perform online research.
17-
* @param researchQuery the natural language query to research
44+
* Calls Perplexity.ai agent to perform online research.
45+
* @param researchTask the comprehensive, detailed task to for the AI agent with online research capabilities to answer.
1846
* @param saveToMemory if the response should be saved to the agent memory.
1947
* @returns {string} if saveToMemory is true then returns the memory key. If saveToMemory is false then returns the research contents.
2048
*/
2149
@cacheRetry()
2250
@func()
23-
async research(researchQuery: string, saveToMemory: boolean): Promise<string> {
51+
async research(researchTask: string, saveToMemory: boolean): Promise<string> {
2452
try {
25-
const report: string = await perplexityReasoningProLLM().generateText(researchQuery, { id: 'Perplexity' });
53+
const report: string = await perplexityReasoningProLLM().generateText(researchTask, { id: 'Perplexity' });
2654

2755
if (saveToMemory) {
2856
const summary = await llms().easy.generateText(
29-
`<query>${researchQuery}</query>\nGenerate a summarised version of the research key in one short sentence at most, with only alphanumeric with underscores for spaces. Answer concisely with only the summary.`,
57+
`<query>${researchTask}</query>\nGenerate a summarised version of the research key in one short sentence at most, with only alphanumeric with underscores for spaces. Answer concisely with only the summary.`,
3058
{ id: 'Perplexity memory key' },
3159
);
3260
const key = `Perplexity-${summary}`;
@@ -35,7 +63,35 @@ export class Perplexity {
3563
}
3664
return report;
3765
} catch (e) {
38-
log.error(e, `Perplexity error. Query: ${researchQuery}`);
66+
log.error(e, `Perplexity error. Query: ${researchTask}`);
67+
throw e;
68+
}
69+
}
70+
71+
/**
72+
* Performs a web search using Perplexity's search API.
73+
* @param query - The search query.
74+
* @returns {PerplexitySearchResponse} The search results with titles, URLs, snippets.
75+
*/
76+
@cacheRetry()
77+
@func()
78+
async webSearch(query: string | string[]): Promise<PerplexitySearchResponse> {
79+
const searchParams: PerplexitySearchOptions = { query, max_results: 15, return_snippets: true };
80+
try {
81+
const response = await this.sdkClient.search.create(searchParams);
82+
83+
// Transform SDK response to our interface
84+
return {
85+
results: response.results.map((r: any) => ({
86+
title: r.title,
87+
url: r.url,
88+
date: r.date,
89+
snippet: r.snippet,
90+
// images: r.images,
91+
})),
92+
};
93+
} catch (e) {
94+
log.error(e, `Perplexity web search error. query: ${JSON.stringify(query)}`);
3995
throw e;
4096
}
4197
}

0 commit comments

Comments
 (0)