@oh-my-pi/perplexity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @oh-my-pi/perplexity
2
+
3
+ Perplexity AI search tools for [pi](https://github.com/badlogic/pi-mono).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ omp install @oh-my-pi/perplexity
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Set your Perplexity API key:
14
+
15
+ ```bash
16
+ # Option 1: Environment variable
17
+ export PERPLEXITY_API_KEY=pplx-xxx
18
+
19
+ # Option 2: Add to ~/.env
20
+ echo "PERPLEXITY_API_KEY=pplx-xxx" >> ~/.env
21
+ ```
22
+
23
+ Get your API key from [Perplexity API Settings](https://www.perplexity.ai/settings/api).
24
+
25
+ ## Tools
26
+
27
+ ### `perplexity_search`
28
+
29
+ Fast web search using Perplexity Sonar. Returns real-time answers with citations.
30
+
31
+ **Best for:** Quick facts, current events, straightforward questions
32
+
33
+ **Parameters:**
34
+
35
+ - `query` (required): The search query or question
36
+ - `search_recency_filter`: Filter by recency (`day`, `week`, `month`, `year`)
37
+ - `search_domain_filter`: Limit to specific domains (e.g., `["nature.com", "arxiv.org"]`)
38
+ - `search_context_size`: Amount of search context (`low`, `medium`, `high`)
39
+ - `return_related_questions`: Include follow-up question suggestions
40
+
41
+ ### `perplexity_search_pro`
42
+
43
+ Advanced web search using Perplexity Sonar Pro. Returns comprehensive answers with 2x more sources.
44
+
45
+ **Best for:** Complex research, multi-step analysis, detailed comparisons
46
+
47
+ **Additional parameters:**
48
+
49
+ - `system_prompt`: Guide the response style and focus
50
+
51
+ ## Pricing
52
+
53
+ | Model | Input | Output | Per 1K Requests |
54
+ | --------- | ----------- | ------------ | --------------- |
55
+ | Sonar | $1/M tokens | $1/M tokens | $5-12 |
56
+ | Sonar Pro | $3/M tokens | $15/M tokens | $6-14 |
57
+
58
+ Request cost varies by `search_context_size` (low/medium/high).
59
+
60
+ ## License
61
+
62
+ MIT
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@oh-my-pi/perplexity",
3
+ "version": "0.1.0",
4
+ "description": "Perplexity AI search tools for pi",
5
+ "keywords": [
6
+ "omp-plugin",
7
+ "perplexity",
8
+ "web-search",
9
+ "ai-search",
10
+ "sonar"
11
+ ],
12
+ "author": "Can Bölük <me@can.ac>",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/can1357/oh-my-pi.git",
17
+ "directory": "plugins/perplexity"
18
+ },
19
+ "omp": {
20
+ "install": [
21
+ {
22
+ "src": "tools/perplexity/runtime.json",
23
+ "dest": "agent/tools/perplexity/runtime.json",
24
+ "copy": true
25
+ },
26
+ {
27
+ "src": "tools/perplexity/index.ts",
28
+ "dest": "agent/tools/perplexity/index.ts"
29
+ },
30
+ {
31
+ "src": "tools/perplexity/shared.ts",
32
+ "dest": "agent/tools/perplexity/shared.ts"
33
+ },
34
+ {
35
+ "src": "tools/perplexity/search.ts",
36
+ "dest": "agent/tools/perplexity/search.ts"
37
+ }
38
+ ],
39
+ "variables": {
40
+ "apiKey": {
41
+ "type": "string",
42
+ "env": "PERPLEXITY_API_KEY",
43
+ "description": "Perplexity API key for authentication",
44
+ "required": true
45
+ }
46
+ },
47
+ "features": {
48
+ "search": {
49
+ "description": "Web search with Sonar models (fast and pro)",
50
+ "default": true
51
+ }
52
+ }
53
+ },
54
+ "files": [
55
+ "tools"
56
+ ]
57
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Perplexity Tools - Dynamic loader for feature modules
3
+ *
4
+ * Reads runtime.json to determine which features are enabled,
5
+ * then loads and initializes those feature modules.
6
+ *
7
+ * Available features:
8
+ * - search: Web search with Sonar models (fast and pro)
9
+ */
10
+
11
+ import type { TSchema } from "@sinclair/typebox";
12
+ import type {
13
+ CustomAgentTool,
14
+ CustomToolFactory,
15
+ ToolAPI,
16
+ } from "@mariozechner/pi-coding-agent";
17
+ import runtime from "./runtime.json";
18
+
19
+ // Map feature names to their module imports
20
+ const FEATURE_LOADERS: Record<
21
+ string,
22
+ () => Promise<{ default: CustomToolFactory }>
23
+ > = {
24
+ search: () => import("./search"),
25
+ };
26
+
27
+ /**
28
+ * Factory function that loads enabled features from runtime.json
29
+ */
30
+ const factory: CustomToolFactory = async (
31
+ toolApi: ToolAPI,
32
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
33
+ const allTools: CustomAgentTool<TSchema, unknown>[] = [];
34
+ const enabledFeatures = runtime.features ?? [];
35
+
36
+ for (const feature of enabledFeatures) {
37
+ const loader = FEATURE_LOADERS[feature];
38
+ if (!loader) {
39
+ console.error(`Unknown perplexity feature: "${feature}"`);
40
+ continue;
41
+ }
42
+
43
+ try {
44
+ const module = await loader();
45
+ const featureFactory = module.default;
46
+
47
+ if (typeof featureFactory === "function") {
48
+ const result = await featureFactory(toolApi);
49
+ // Handle both single tool and array of tools
50
+ if (result) {
51
+ const tools = Array.isArray(result) ? result : [result];
52
+ for (const tool of tools) {
53
+ if (tool && typeof tool === "object" && "name" in tool) {
54
+ allTools.push(tool);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ } catch (error) {
60
+ console.error(`Failed to load perplexity feature "${feature}":`, error);
61
+ }
62
+ }
63
+
64
+ return allTools.length > 0 ? allTools : null;
65
+ };
66
+
67
+ export default factory;
@@ -0,0 +1,4 @@
1
+ {
2
+ "features": ["search"],
3
+ "options": {}
4
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Perplexity Search Tools - Web search with Sonar models
3
+ *
4
+ * Tools:
5
+ * - perplexity_search: Fast web search with Sonar (quick answers)
6
+ * - perplexity_search_pro: Advanced search with Sonar Pro (deeper research)
7
+ */
8
+
9
+ import { Type, type TSchema } from "@sinclair/typebox";
10
+ import type {
11
+ CustomAgentTool,
12
+ CustomToolFactory,
13
+ ToolAPI,
14
+ } from "@mariozechner/pi-coding-agent";
15
+ import {
16
+ callPerplexity,
17
+ findApiKey,
18
+ formatResponse,
19
+ type PerplexityRequest,
20
+ } from "./shared";
21
+
22
+ const RecencyFilter = Type.Optional(
23
+ Type.Union(
24
+ [
25
+ Type.Literal("day"),
26
+ Type.Literal("week"),
27
+ Type.Literal("month"),
28
+ Type.Literal("year"),
29
+ ],
30
+ { description: "Filter results by recency" },
31
+ ),
32
+ );
33
+
34
+ const SearchContextSize = Type.Optional(
35
+ Type.Union(
36
+ [
37
+ Type.Literal("low"),
38
+ Type.Literal("medium"),
39
+ Type.Literal("high"),
40
+ ],
41
+ { description: "Amount of search context to use (affects cost). Default: low" },
42
+ ),
43
+ );
44
+
45
+ // Schema for fast search
46
+ const FastSearchSchema = Type.Object({
47
+ query: Type.String({
48
+ description: "The search query or question to answer",
49
+ }),
50
+ search_recency_filter: RecencyFilter,
51
+ search_domain_filter: Type.Optional(
52
+ Type.Array(Type.String(), {
53
+ description: "Limit search to specific domains (e.g., ['nature.com', 'arxiv.org']). Prefix with '-' to exclude.",
54
+ }),
55
+ ),
56
+ search_context_size: SearchContextSize,
57
+ return_related_questions: Type.Optional(
58
+ Type.Boolean({
59
+ description: "Include related follow-up questions in response",
60
+ }),
61
+ ),
62
+ });
63
+
64
+ // Schema for pro search
65
+ const ProSearchSchema = Type.Object({
66
+ query: Type.String({
67
+ description: "The search query or research question",
68
+ }),
69
+ system_prompt: Type.Optional(
70
+ Type.String({
71
+ description: "System prompt to guide the response style and focus",
72
+ }),
73
+ ),
74
+ search_recency_filter: RecencyFilter,
75
+ search_domain_filter: Type.Optional(
76
+ Type.Array(Type.String(), {
77
+ description: "Limit search to specific domains (e.g., ['nature.com', 'arxiv.org']). Prefix with '-' to exclude.",
78
+ }),
79
+ ),
80
+ search_context_size: SearchContextSize,
81
+ return_related_questions: Type.Optional(
82
+ Type.Boolean({
83
+ description: "Include related follow-up questions in response",
84
+ }),
85
+ ),
86
+ });
87
+
88
+ type FastSearchParams = {
89
+ query: string;
90
+ search_recency_filter?: "day" | "week" | "month" | "year";
91
+ search_domain_filter?: string[];
92
+ search_context_size?: "low" | "medium" | "high";
93
+ return_related_questions?: boolean;
94
+ };
95
+
96
+ type ProSearchParams = FastSearchParams & {
97
+ system_prompt?: string;
98
+ };
99
+
100
+ function createSearchTool(
101
+ apiKey: string,
102
+ name: string,
103
+ description: string,
104
+ model: string,
105
+ schema: TSchema,
106
+ ): CustomAgentTool<TSchema, unknown> {
107
+ return {
108
+ name,
109
+ label: name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
110
+ description,
111
+ parameters: schema,
112
+ async execute(_toolCallId, params) {
113
+ try {
114
+ const p = (params ?? {}) as ProSearchParams;
115
+
116
+ const request: PerplexityRequest = {
117
+ model,
118
+ messages: [],
119
+ };
120
+
121
+ // Add system prompt if provided
122
+ if (p.system_prompt) {
123
+ request.messages.push({
124
+ role: "system",
125
+ content: p.system_prompt,
126
+ });
127
+ }
128
+
129
+ request.messages.push({
130
+ role: "user",
131
+ content: p.query,
132
+ });
133
+
134
+ // Add optional parameters
135
+ if (p.search_recency_filter) {
136
+ request.search_recency_filter = p.search_recency_filter;
137
+ }
138
+ if (p.search_domain_filter && p.search_domain_filter.length > 0) {
139
+ request.search_domain_filter = p.search_domain_filter;
140
+ }
141
+ if (p.search_context_size) {
142
+ request.search_context_size = p.search_context_size;
143
+ }
144
+ if (p.return_related_questions) {
145
+ request.return_related_questions = p.return_related_questions;
146
+ }
147
+
148
+ const response = await callPerplexity(apiKey, request);
149
+ const text = formatResponse(response);
150
+
151
+ return {
152
+ content: [{ type: "text" as const, text }],
153
+ details: {
154
+ model: response.model,
155
+ usage: response.usage,
156
+ citations: response.citations,
157
+ search_results: response.search_results,
158
+ related_questions: response.related_questions,
159
+ },
160
+ };
161
+ } catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ return {
164
+ content: [{ type: "text" as const, text: `Error: ${message}` }],
165
+ details: { error: message },
166
+ };
167
+ }
168
+ },
169
+ };
170
+ }
171
+
172
+ const factory: CustomToolFactory = async (
173
+ _toolApi: ToolAPI,
174
+ ): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
175
+ const apiKey = findApiKey();
176
+ if (!apiKey) return null;
177
+
178
+ return [
179
+ createSearchTool(
180
+ apiKey,
181
+ "perplexity_search",
182
+ "Fast web search using Perplexity Sonar. Returns real-time answers with citations. Best for quick facts, current events, and straightforward questions. Cost-effective for high-volume queries.",
183
+ "sonar",
184
+ FastSearchSchema,
185
+ ),
186
+ createSearchTool(
187
+ apiKey,
188
+ "perplexity_search_pro",
189
+ "Advanced web search using Perplexity Sonar Pro. Returns comprehensive, well-researched answers with 2x more sources. Best for complex research questions, multi-step analysis, and detailed comparisons. Higher cost but deeper results.",
190
+ "sonar-pro",
191
+ ProSearchSchema,
192
+ ),
193
+ ];
194
+ };
195
+
196
+ export default factory;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Shared utilities for Perplexity API
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+
9
+ export const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
10
+
11
+ export interface PerplexityMessage {
12
+ role: "system" | "user" | "assistant";
13
+ content: string;
14
+ }
15
+
16
+ export interface PerplexityRequest {
17
+ model: string;
18
+ messages: PerplexityMessage[];
19
+ temperature?: number;
20
+ max_tokens?: number;
21
+ search_domain_filter?: string[];
22
+ search_recency_filter?: "day" | "week" | "month" | "year";
23
+ return_images?: boolean;
24
+ return_related_questions?: boolean;
25
+ search_context_size?: "low" | "medium" | "high";
26
+ }
27
+
28
+ export interface SearchResult {
29
+ title: string;
30
+ url: string;
31
+ date?: string;
32
+ snippet?: string;
33
+ }
34
+
35
+ export interface PerplexityResponse {
36
+ id: string;
37
+ model: string;
38
+ created: number;
39
+ usage: {
40
+ prompt_tokens: number;
41
+ completion_tokens: number;
42
+ total_tokens: number;
43
+ search_context_size?: string;
44
+ };
45
+ citations?: string[];
46
+ search_results?: SearchResult[];
47
+ related_questions?: string[];
48
+ choices: Array<{
49
+ index: number;
50
+ finish_reason: string;
51
+ message: {
52
+ role: string;
53
+ content: string;
54
+ };
55
+ }>;
56
+ }
57
+
58
+ /**
59
+ * Parse a .env file and return key-value pairs
60
+ */
61
+ function parseEnvFile(filePath: string): Record<string, string> {
62
+ const result: Record<string, string> = {};
63
+ if (!fs.existsSync(filePath)) return result;
64
+
65
+ try {
66
+ const content = fs.readFileSync(filePath, "utf-8");
67
+ for (const line of content.split("\n")) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed || trimmed.startsWith("#")) continue;
70
+
71
+ const eqIndex = trimmed.indexOf("=");
72
+ if (eqIndex === -1) continue;
73
+
74
+ const key = trimmed.slice(0, eqIndex).trim();
75
+ let value = trimmed.slice(eqIndex + 1).trim();
76
+
77
+ // Remove surrounding quotes
78
+ if (
79
+ (value.startsWith('"') && value.endsWith('"')) ||
80
+ (value.startsWith("'") && value.endsWith("'"))
81
+ ) {
82
+ value = value.slice(1, -1);
83
+ }
84
+
85
+ result[key] = value;
86
+ }
87
+ } catch {
88
+ // Ignore read errors
89
+ }
90
+
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * Find PERPLEXITY_API_KEY from environment or .env files
96
+ */
97
+ export function findApiKey(): string | null {
98
+ // 1. Check environment variable
99
+ if (process.env.PERPLEXITY_API_KEY) {
100
+ return process.env.PERPLEXITY_API_KEY;
101
+ }
102
+
103
+ // 2. Check .env in current directory
104
+ const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
105
+ if (localEnv.PERPLEXITY_API_KEY) {
106
+ return localEnv.PERPLEXITY_API_KEY;
107
+ }
108
+
109
+ // 3. Check ~/.env
110
+ const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
111
+ if (homeEnv.PERPLEXITY_API_KEY) {
112
+ return homeEnv.PERPLEXITY_API_KEY;
113
+ }
114
+
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Call Perplexity API
120
+ */
121
+ export async function callPerplexity(
122
+ apiKey: string,
123
+ request: PerplexityRequest,
124
+ ): Promise<PerplexityResponse> {
125
+ const response = await fetch(PERPLEXITY_API_URL, {
126
+ method: "POST",
127
+ headers: {
128
+ Authorization: `Bearer ${apiKey}`,
129
+ "Content-Type": "application/json",
130
+ },
131
+ body: JSON.stringify(request),
132
+ });
133
+
134
+ if (!response.ok) {
135
+ const errorText = await response.text();
136
+ throw new Error(`Perplexity API error (${response.status}): ${errorText}`);
137
+ }
138
+
139
+ return response.json() as Promise<PerplexityResponse>;
140
+ }
141
+
142
+ /**
143
+ * Format Perplexity response for display
144
+ */
145
+ export function formatResponse(response: PerplexityResponse): string {
146
+ const content = response.choices[0]?.message?.content ?? "";
147
+ const parts: string[] = [content];
148
+
149
+ // Add citations if available
150
+ if (response.citations && response.citations.length > 0) {
151
+ parts.push("\n\n## Sources");
152
+ for (const [i, url] of response.citations.entries()) {
153
+ parts.push(`[${i + 1}] ${url}`);
154
+ }
155
+ }
156
+
157
+ // Add related questions if available
158
+ if (response.related_questions && response.related_questions.length > 0) {
159
+ parts.push("\n\n## Related Questions");
160
+ for (const question of response.related_questions) {
161
+ parts.push(`- ${question}`);
162
+ }
163
+ }
164
+
165
+ return parts.join("\n");
166
+ }