Skip to content

Commit a0f58e9

Browse files
authored
add perplexity
Co-authored-by: Chris Tate <ctate@users.noreply.github.com>
1 parent b33c27a commit a0f58e9

File tree

9 files changed

+648
-1
lines changed

9 files changed

+648
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
8484
- **Firecrawl**: Scrape URL, Search Web
8585
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
8686
- **Linear**: Create Ticket, Find Issues
87+
- **Perplexity**: Search Web, Ask Question, Research Topic
8788
- **Resend**: Send Email
8889
- **Slack**: Send Slack Message
8990
- **Stripe**: Create Customer, Get Customer, Create Invoice

plugins/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
* 1. Delete the plugin directory
1414
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
1515
*
16-
* Discovered plugins: ai-gateway, blob, firecrawl, github, linear, resend, slack, stripe, superagent, v0
16+
* Discovered plugins: ai-gateway, blob, firecrawl, github, linear, perplexity, resend, slack, superagent, v0
1717
*/
1818

1919
import "./ai-gateway";
2020
import "./blob";
2121
import "./firecrawl";
2222
import "./github";
2323
import "./linear";
24+
import "./perplexity";
2425
import "./resend";
2526
import "./slack";
2627
import "./stripe";

plugins/perplexity/credentials.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type PerplexityCredentials = {
2+
PERPLEXITY_API_KEY?: string;
3+
};

plugins/perplexity/icon.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function PerplexityIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
aria-label="Perplexity logo"
5+
className={className}
6+
fill="currentColor"
7+
viewBox="0 0 24 24"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<title>Perplexity</title>
11+
<path d="M22.2819 9.821V8.402h-3.306V2.202l-.606.417-3.157 2.173-.36.248h-5.719l-.36-.248-3.156-2.173-.606-.417v6.2H1.703v1.42h3.306v6.135l-.646.998-.154.238.124.255 2.49 5.12.336.69h9.665l.336-.69 2.49-5.12.124-.255-.154-.238-.646-.998V9.82h3.307zm-12.6-3.783 2.319 1.596 2.318-1.596v1.964H9.682V6.038zm-3.656 2.363h3.256v1.42H6.026V8.401zm-.6 12.462L3.66 17.069l.479-.74h2.543l.844 1.304zm6.094 1.015H9.08v-2.22L7.35 17.157l-.955-1.475V9.821h2.856v.964l-2.125 1.34.452.717 2.073-1.306v2.132l-2.125 1.34.452.717 2.073-1.306v.964l-2.125 1.34.452.717 2.073-1.306v5.343h1.168v-5.26l2.073 1.224.452-.717-2.125-1.258v-.964l2.073 1.224.452-.717-2.125-1.258v-2.132l2.073 1.224.452-.717-2.125-1.258v-.964h2.855v5.861l-.954 1.475-1.732 2.502v2.22h-3.238zm6.895-3.794.844-1.304h2.543l.48.74-1.768 3.794H15.37zm2.302-5.76h-4.037v-1.42h3.256v-1.42h-3.257V6.038l2.319 1.596 2.318-1.596v5.286h-.6z" />
12+
</svg>
13+
);
14+
}

plugins/perplexity/index.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import type { IntegrationPlugin } from "../registry";
2+
import { registerIntegration } from "../registry";
3+
import { PerplexityIcon } from "./icon";
4+
5+
const perplexityPlugin: IntegrationPlugin = {
6+
type: "perplexity",
7+
label: "Perplexity",
8+
description: "AI-powered search and research with real-time web access",
9+
10+
icon: PerplexityIcon,
11+
12+
formFields: [
13+
{
14+
id: "perplexityApiKey",
15+
label: "API Key",
16+
type: "password",
17+
placeholder: "pplx-...",
18+
configKey: "apiKey",
19+
envVar: "PERPLEXITY_API_KEY",
20+
helpText: "Get your API key from ",
21+
helpLink: {
22+
text: "perplexity.ai/settings/api",
23+
url: "https://www.perplexity.ai/settings/api",
24+
},
25+
},
26+
],
27+
28+
testConfig: {
29+
getTestFunction: async () => {
30+
const { testPerplexity } = await import("./test");
31+
return testPerplexity;
32+
},
33+
},
34+
35+
actions: [
36+
{
37+
slug: "search",
38+
label: "Search Web",
39+
description:
40+
"Search the web with AI-powered answers and citations from Perplexity",
41+
category: "Perplexity",
42+
stepFunction: "perplexitySearchStep",
43+
stepImportPath: "search",
44+
outputFields: [
45+
{ field: "answer", description: "AI-generated answer to the query" },
46+
{ field: "citations", description: "Array of source URLs" },
47+
{ field: "model", description: "Model used for the response" },
48+
],
49+
configFields: [
50+
{
51+
key: "query",
52+
label: "Search Query",
53+
type: "template-input",
54+
placeholder: "Enter your search query or use {{NodeName.field}}",
55+
example: "What are the latest developments in AI?",
56+
required: true,
57+
},
58+
{
59+
key: "searchFocus",
60+
label: "Search Focus",
61+
type: "select",
62+
defaultValue: "internet",
63+
options: [
64+
{ value: "internet", label: "General Web" },
65+
{ value: "academic", label: "Academic Sources" },
66+
{ value: "news", label: "News Articles" },
67+
{ value: "youtube", label: "YouTube" },
68+
{ value: "reddit", label: "Reddit" },
69+
],
70+
},
71+
],
72+
},
73+
{
74+
slug: "ask",
75+
label: "Ask Question",
76+
description:
77+
"Ask a question and get an AI-powered response with web sources",
78+
category: "Perplexity",
79+
stepFunction: "perplexityAskStep",
80+
stepImportPath: "ask",
81+
outputFields: [
82+
{ field: "answer", description: "AI-generated answer" },
83+
{ field: "citations", description: "Array of source URLs" },
84+
{ field: "model", description: "Model used for the response" },
85+
],
86+
configFields: [
87+
{
88+
key: "question",
89+
label: "Question",
90+
type: "template-textarea",
91+
placeholder: "Enter your question or use {{NodeName.field}}",
92+
example: "Explain quantum computing in simple terms",
93+
rows: 3,
94+
required: true,
95+
},
96+
{
97+
key: "systemPrompt",
98+
label: "System Prompt",
99+
type: "template-textarea",
100+
placeholder: "Optional: customize how Perplexity responds",
101+
rows: 2,
102+
},
103+
{
104+
key: "model",
105+
label: "Model",
106+
type: "select",
107+
defaultValue: "sonar",
108+
options: [
109+
{ value: "sonar", label: "Sonar (Fast)" },
110+
{ value: "sonar-pro", label: "Sonar Pro (Advanced)" },
111+
{ value: "sonar-reasoning", label: "Sonar Reasoning (Complex)" },
112+
],
113+
},
114+
],
115+
},
116+
{
117+
slug: "research",
118+
label: "Research Topic",
119+
description:
120+
"Conduct deep research on a topic with comprehensive analysis and citations",
121+
category: "Perplexity",
122+
stepFunction: "perplexityResearchStep",
123+
stepImportPath: "research",
124+
outputFields: [
125+
{ field: "report", description: "Comprehensive research report" },
126+
{ field: "citations", description: "Array of source URLs" },
127+
{ field: "model", description: "Model used for the response" },
128+
],
129+
configFields: [
130+
{
131+
key: "topic",
132+
label: "Research Topic",
133+
type: "template-textarea",
134+
placeholder:
135+
"Enter the topic to research or use {{NodeName.field}}",
136+
example: "The impact of artificial intelligence on healthcare",
137+
rows: 3,
138+
required: true,
139+
},
140+
{
141+
key: "depth",
142+
label: "Research Depth",
143+
type: "select",
144+
defaultValue: "detailed",
145+
options: [
146+
{ value: "brief", label: "Brief Overview" },
147+
{ value: "detailed", label: "Detailed Analysis" },
148+
{ value: "comprehensive", label: "Comprehensive Report" },
149+
],
150+
},
151+
],
152+
},
153+
],
154+
};
155+
156+
// Auto-register on import
157+
registerIntegration(perplexityPlugin);
158+
159+
export default perplexityPlugin;

plugins/perplexity/steps/ask.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import "server-only";
2+
3+
import { fetchCredentials } from "@/lib/credential-fetcher";
4+
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
5+
import { getErrorMessage } from "@/lib/utils";
6+
import type { PerplexityCredentials } from "../credentials";
7+
8+
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
9+
10+
type PerplexityMessage = {
11+
role: "system" | "user" | "assistant";
12+
content: string;
13+
};
14+
15+
type PerplexityCitation = {
16+
url: string;
17+
text?: string;
18+
};
19+
20+
type PerplexityResponse = {
21+
id: string;
22+
model: string;
23+
choices: Array<{
24+
index: number;
25+
message: PerplexityMessage;
26+
finish_reason: string;
27+
}>;
28+
citations?: PerplexityCitation[] | string[];
29+
usage: {
30+
prompt_tokens: number;
31+
completion_tokens: number;
32+
total_tokens: number;
33+
};
34+
};
35+
36+
type AskResult = {
37+
answer: string;
38+
citations: string[];
39+
model: string;
40+
};
41+
42+
export type PerplexityAskCoreInput = {
43+
question: string;
44+
systemPrompt?: string;
45+
model?: string;
46+
};
47+
48+
export type PerplexityAskInput = StepInput &
49+
PerplexityAskCoreInput & {
50+
integrationId?: string;
51+
};
52+
53+
/**
54+
* Core logic - ask a question with Perplexity AI
55+
*/
56+
async function stepHandler(
57+
input: PerplexityAskCoreInput,
58+
credentials: PerplexityCredentials
59+
): Promise<AskResult> {
60+
const apiKey = credentials.PERPLEXITY_API_KEY;
61+
62+
if (!apiKey) {
63+
throw new Error("Perplexity API Key is not configured.");
64+
}
65+
66+
try {
67+
const messages: PerplexityMessage[] = [];
68+
69+
if (input.systemPrompt) {
70+
messages.push({
71+
role: "system",
72+
content: input.systemPrompt,
73+
});
74+
} else {
75+
messages.push({
76+
role: "system",
77+
content:
78+
"You are a helpful AI assistant. Provide accurate, well-researched answers with citations when available.",
79+
});
80+
}
81+
82+
messages.push({
83+
role: "user",
84+
content: input.question,
85+
});
86+
87+
const response = await fetch(PERPLEXITY_API_URL, {
88+
method: "POST",
89+
headers: {
90+
"Content-Type": "application/json",
91+
Authorization: `Bearer ${apiKey}`,
92+
},
93+
body: JSON.stringify({
94+
model: input.model || "sonar",
95+
messages,
96+
return_citations: true,
97+
}),
98+
});
99+
100+
if (!response.ok) {
101+
const errorText = await response.text();
102+
throw new Error(`HTTP ${response.status}: ${errorText}`);
103+
}
104+
105+
const result = (await response.json()) as PerplexityResponse;
106+
107+
const answer = result.choices[0]?.message?.content || "";
108+
const citations = (result.citations || []).map((c) =>
109+
typeof c === "string" ? c : c.url
110+
);
111+
112+
return {
113+
answer,
114+
citations,
115+
model: result.model,
116+
};
117+
} catch (error) {
118+
throw new Error(`Failed to ask: ${getErrorMessage(error)}`);
119+
}
120+
}
121+
122+
/**
123+
* App entry point - fetches credentials and wraps with logging
124+
*/
125+
export async function perplexityAskStep(
126+
input: PerplexityAskInput
127+
): Promise<AskResult> {
128+
"use step";
129+
130+
const credentials = input.integrationId
131+
? await fetchCredentials(input.integrationId)
132+
: {};
133+
134+
return withStepLogging(input, () => stepHandler(input, credentials));
135+
}
136+
137+
export const _integrationType = "perplexity";

0 commit comments

Comments
 (0)