Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions typescript-recipes/parallel-vercel-template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ View the demo at:
│ localStorage │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ SearchDemo │ │ /api/search │ → client.beta.search()
│ ExtractDemo │ │ /api/extract │ → client.beta.extract()
│ SearchDemo │ │ /api/search │ → client.search()
│ ExtractDemo │ │ /api/extract │ → client.extract()
│ TasksDemo │ │ /api/tasks │ → client.taskRun.create()
└─────────────────┘ │ /api/tasks/[id]/status │ → client.taskRun.retrieve()
│ /api/tasks/[id]/events │ → client.beta.taskRun.events()
│ /api/tasks/[id]/events │ → client.taskRun.events()
└─────────────────┘
```

### How It Works

1. **Search**: User enters a search objective and optional queries → API calls `client.beta.search()` → Returns ranked results with excerpts
2. **Extract**: User enters URLs and optional objective → API calls `client.beta.extract()` → Returns extracted content
1. **Search**: User enters a search objective and optional queries → API calls `client.search()` → Returns ranked results with excerpts. The route uses the objective as a query when queries are omitted, with a choice of `basic` or `advanced` search
2. **Extract**: User enters URLs and optional objective → API calls `client.extract()` → Returns extracted content
3. **Tasks**: User enters a research task → API calls `client.taskRun.create()` → SSE stream delivers real-time progress → Final output displayed on completion

## Quick Start
Expand Down Expand Up @@ -126,8 +126,8 @@ Go to your [Vercel Integration page](https://vercel.com/marketplace/parallel), s
## Resources

- [Parallel Documentation](https://docs.parallel.ai)
- [Search API Reference](https://docs.parallel.ai/api-reference/search-beta/search)
- [Extract API Reference](https://docs.parallel.ai/api-reference/extract-beta/extract)
- [Search API Reference](https://docs.parallel.ai/api-reference/search/search)
- [Extract API Reference](https://docs.parallel.ai/api-reference/extract/extract)
- [Tasks API Reference](https://docs.parallel.ai/api-reference/tasks-v1/create-task-run)
- [SSE Streaming Guide](https://docs.parallel.ai/task-api/task-sse)
- [Pricing](https://docs.parallel.ai/resources/pricing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ export async function POST(request: NextRequest) {

const client = getParallelClient();

const extractResult = await client.beta.extract({
const extractResult = await client.extract({
urls,
objective: objective?.trim() || undefined,
excerpts: true,
full_content: false,
// Excerpts are enabled by default in v1.
advanced_settings: { full_content: false },
});

return NextResponse.json(extractResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,38 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const { objective, searchQueries, mode, maxResults } = body;

if (!objective) {
if (typeof objective !== "string" || !objective.trim()) {
return errorResponse("objective is required", 400);
}

if (
searchQueries !== undefined &&
(!Array.isArray(searchQueries) ||
searchQueries.some((query: unknown) => typeof query !== "string"))
) {
return errorResponse("searchQueries must be an array of strings", 400);
}

const searchMode = mode || "basic";
if (!["turbo", "fast", "basic", "advanced"].includes(searchMode)) {
return errorResponse("Unsupported search mode", 400);
}
const queries = (searchQueries ?? [])
.map((query: string) => query.trim())
.filter(Boolean);
const client = getParallelClient();

const searchResult = await client.beta.search({
const searchResult = await client.search({
objective,
search_queries: searchQueries || undefined,
mode: mode || "one-shot",
max_results: maxResults || SEARCH_DEFAULTS.MAX_RESULTS,
max_chars_per_result: SEARCH_DEFAULTS.MAX_CHARS_PER_RESULT,
// Keep objective-only submissions working with v1's required queries.
search_queries: queries.length ? queries : [objective.trim()],
mode: searchMode,
advanced_settings: {
max_results: maxResults || SEARCH_DEFAULTS.MAX_RESULTS,
excerpt_settings: {
max_chars_per_result: SEARCH_DEFAULTS.MAX_CHARS_PER_RESULT,
},
},
});

return NextResponse.json(searchResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ export async function GET(
try {
const client = getParallelClient();

// Use the SDK's beta.taskRun.events() method to get the event stream
const eventStream = await client.beta.taskRun.events(runId);
const eventStream = await client.taskRun.events(runId);

// Create a ReadableStream that converts SDK events to SSE format
const stream = new ReadableStream({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default function ExtractDemo() {
URLs to Extract (one per line)
</label>
<a
href="https://docs.parallel.ai/api-reference/extract-beta/extract"
href="https://docs.parallel.ai/api-reference/extract/extract"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-green-600 dark:text-green-400 hover:underline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface SearchResponse {
error?: string;
}

type SearchMode = "one-shot" | "agentic";
type SearchMode = "basic" | "advanced";

interface StoredSearchState {
objective: string;
Expand All @@ -27,14 +27,14 @@ interface StoredSearchState {
const INITIAL_STATE: StoredSearchState = {
objective: "",
searchQueries: "",
mode: "one-shot",
mode: "basic",
results: [],
error: null,
};

export default function SearchDemo() {
const [storedState, setStoredState, clearStoredState, isHydrated] =
useSessionStorage<StoredSearchState>("parallel-search-demo", INITIAL_STATE);
useSessionStorage<StoredSearchState>("parallel-search-demo-v1", INITIAL_STATE);

const [loading, setLoading] = useState(false);

Expand Down Expand Up @@ -108,7 +108,7 @@ export default function SearchDemo() {
Search Objective
</label>
<a
href="https://docs.parallel.ai/api-reference/search-beta/search"
href="https://docs.parallel.ai/api-reference/search/search"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
Expand Down Expand Up @@ -150,31 +150,31 @@ export default function SearchDemo() {
<div className="flex gap-2">
<button
type="button"
onClick={() => setMode("one-shot")}
onClick={() => setMode("basic")}
className={`flex-1 py-2 px-3 text-sm font-medium rounded-lg border transition-colors ${
mode === "one-shot"
mode === "basic"
? "bg-blue-600 text-white border-blue-600"
: "bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700"
}`}
>
One-shot
Basic
</button>
<button
type="button"
onClick={() => setMode("agentic")}
onClick={() => setMode("advanced")}
className={`flex-1 py-2 px-3 text-sm font-medium rounded-lg border transition-colors ${
mode === "agentic"
mode === "advanced"
? "bg-blue-600 text-white border-blue-600"
: "bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700"
}`}
>
Agentic
Advanced
</button>
</div>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
{mode === "one-shot"
? "Comprehensive results with longer excerpts for single-query answers"
: "Concise, token-efficient results for use in agentic loops"}
{mode === "basic"
? "Low-latency search, best with 2-3 focused queries"
: "Higher-quality search with more advanced retrieval and compression"}
</p>
</div>

Expand Down
8 changes: 4 additions & 4 deletions typescript-recipes/parallel-vercel-template/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion typescript-recipes/parallel-vercel-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"next": "16.2.3",
"parallel-web": "^0.2.4",
"parallel-web": "^1.3.3",
"react": "19.2.3",
"react-dom": "19.2.3"
},
Expand Down
10 changes: 5 additions & 5 deletions typescript-recipes/parallel-vercel-template/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import Module from "node:module";
import { fileURLToPath } from "node:url";
import ts from "typescript";

const dirname = path.dirname(fileURLToPath(import.meta.url));

// Load route handlers without a Next server, keeping the real SDK and serializer.
function load(relativePath) {
const filename = path.resolve(dirname, "..", relativePath);
const compiled = ts.transpileModule(fs.readFileSync(filename, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
}).outputText;
const mod = new Module(filename);
mod.filename = filename;
mod.paths = Module._nodeModulePaths(path.dirname(filename));
const originalRequire = mod.require.bind(mod);
mod.require = (name) => name === "@/lib/parallel" ? load("lib/parallel.ts") : originalRequire(name);
mod._compile(compiled, filename);
return mod.exports;
}

const post = (body) => new Request("http://localhost/api", {
method: "POST", body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
});

test("routes serialize v1 requests and preserve response and Task contracts", async () => {
const previousFetch = global.fetch;
const previousKey = process.env.PARALLEL_API_KEY;
process.env.PARALLEL_API_KEY = "test-key";
const calls = [];
let responseBody = { results: [] };
global.fetch = async (url, options) => {
calls.push({ path: new URL(url).pathname, body: options.body && JSON.parse(options.body) });
if (String(url).endsWith("/events")) {
return new Response('event: task_run.status\ndata: {"type":"task_run.status","status":"completed"}\n\n', {
headers: { "Content-Type": "text/event-stream" },
});
}
return Response.json(responseBody);
};
try {
const search = load("app/api/search/route.ts");
assert.deepEqual(await (await search.POST(post({ objective: "AI safety" }))).json(), responseBody);
assert.deepEqual(calls.pop(), { path: "/v1/search", body: {
objective: "AI safety", search_queries: ["AI safety"], mode: "basic",
advanced_settings: { max_results: 10, excerpt_settings: { max_chars_per_result: 2500 } },
} });
await search.POST(post({ objective: "AI safety", searchQueries: [" alignment ", " "], mode: "advanced", maxResults: 3 }));
assert.deepEqual(calls.at(-1).body.search_queries, ["alignment"]);
assert.equal(calls.at(-1).body.mode, "advanced");
assert.equal(calls.at(-1).body.advanced_settings.max_results, 3);
await search.POST(post({ objective: "AI safety", searchQueries: [" "] }));
assert.deepEqual(calls.at(-1).body.search_queries, ["AI safety"]);
const count = calls.length;
for (const body of [{ objective: " " }, { objective: "x", searchQueries: "x" }, { objective: "x", mode: "invalid" }, { objective: "x", mode: "agentic" }]) {
assert.equal((await search.POST(post(body))).status, 400);
}
assert.equal(calls.length, count);
const extract = load("app/api/extract/route.ts");
assert.deepEqual(await (await extract.POST(post({ urls: ["https://example.com"], objective: " facts " }))).json(), responseBody);
assert.deepEqual(calls.pop(), { path: "/v1/extract", body: {
urls: ["https://example.com"], objective: "facts", advanced_settings: { full_content: false },
} });
const tasks = load("app/api/tasks/route.ts");
responseBody = { run_id: "run_test", status: "queued" };
for (const [processor, type] of [["lite", "text"], ["pro", "auto"]]) {
assert.deepEqual(await (await tasks.POST(post({ input: "Research", processor }))).json(), responseBody);
assert.deepEqual(calls.pop(), { path: "/v1/tasks/runs", body: { input: "Research", processor, task_spec: { output_schema: { type } } } });
}
const params = { params: Promise.resolve({ runId: "run_test" }) };
const status = load("app/api/tasks/[runId]/status/route.ts");
responseBody = { status: "completed", output: { content: "Done" } };
assert.deepEqual(await (await status.GET(null, params)).json(), responseBody);
assert.deepEqual(calls.slice(-2).map((call) => call.path), ["/v1/tasks/runs/run_test", "/v1/tasks/runs/run_test/result"]);
const events = load("app/api/tasks/[runId]/events/route.ts");
const stream = await events.GET(null, params);
assert.equal(stream.headers.get("Content-Type"), "text/event-stream");
assert.equal(await stream.text(), 'data: {"type":"task_run.status","status":"completed"}\n\n');
assert.equal(calls.at(-1).path, "/v1/tasks/runs/run_test/events");
} finally {
global.fetch = previousFetch;
if (previousKey === undefined) delete process.env.PARALLEL_API_KEY;
else process.env.PARALLEL_API_KEY = previousKey;
}
});
Loading