The ecosystem

Code language

View this guide and its examples in your language.

Prism Perplexity

Prism Perplexity provides access to Perplexity embeddings, web search and the long-running Agent API. Packages are available for PHP, TypeScript and Python.

Installation#

composer require particle-academy/prism-perplexity
npm install @particle-academy/prism-perplexity
pip install prism-ai-perplexity

In PHP, the package extends Prism's existing perplexity provider registration: text, structured output and streaming keep working through Prism core, and embeddings, search and the Agent API are added to the same provider. TypeScript and Python expose functions that take an HTTP transport; they do not register a Perplexity provider in core.

The PHP package requires PHP 8.2+, Laravel 12.61+ or 13.12+, and particle-academy/prism 0.113 or newer. Configure PERPLEXITY_API_KEY in your server environment.

Transport (TypeScript and Python)#

Every TypeScript and Python function takes an HttpClient as its first argument. The package ships one built on the runtime's own HTTP client:

import { fetchTransport } from '@particle-academy/prism-perplexity';

const http = fetchTransport({
  apiKey: process.env.PERPLEXITY_API_KEY!,
  baseUrl: 'https://api.perplexity.ai', // the default
});
import os
from prism_perplexity import urllib_transport

http = urllib_transport(
    os.environ["PERPLEXITY_API_KEY"],
    base_url="https://api.perplexity.ai",  # the default
    timeout=30.0,  # seconds, per request; the default
)

fetchTransport has no timeout option. To set one, pass your own fetch; an expired timeout rejects with the fetch abort error, not a PerplexityError:

const http = fetchTransport({
  apiKey: process.env.PERPLEXITY_API_KEY!,
  fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(30_000) }),
});

To use another HTTP library, write your own transport: a function that takes an HttpRequest (method, path relative to the base URL, optional JSON body) and returns an HttpResponse (status and the decoded json, or null when the body was not JSON). In TypeScript it returns a promise; in Python it is a plain function.

In PHP, the provider uses Laravel's HTTP client and the perplexity entry in config/prism.php.

Embeddings#

Generate embeddings through the PHP provider extension or the port's embeddings function:

use Prism\Prism\Facades\Prism;

$response = Prism::embeddings()
    ->using('perplexity', 'text-embedding-v1')
    ->fromInput('The quick brown fox')
    ->asEmbeddings();

$response->embeddings[0]->embedding;   // float[]
import { embeddings, fetchTransport } from '@particle-academy/prism-perplexity';

const http = fetchTransport({ apiKey: process.env.PERPLEXITY_API_KEY! });
const response = await embeddings(http, {
  model: 'text-embedding-v1',
  inputs: ['The quick brown fox'],
});

response.embeddings[0]; // number[]
import os
from prism_perplexity import embeddings, urllib_transport

http = urllib_transport(os.environ["PERPLEXITY_API_KEY"])
response = embeddings(http, "text-embedding-v1", ["The quick brown fox"])

response.embeddings[0]  # list[float]

TypeScript and Python return embeddings, the provider's usage object and the model that answered.

Order. PHP sorts the vectors by the index Perplexity returns with each one, so embeddings[i] belongs to input i. TypeScript and Python return the vectors in the order the response lists them and do not expose the index.

Contextualized embeddings#

Contextualized embeddings represent chunks from one document using the surrounding document context. This calls a separate endpoint (/contextualized-embeddings). Enable it when the supplied chunks belong to the same document; document_context / documentContext optionally sends the document context with them:

Prism::embeddings()
    ->using('perplexity', 'text-embedding-v1')
    ->fromArray($chunks)
    ->withProviderOptions([
        'contextualized' => true,
        'document_context' => $documentText, // optional
    ])
    ->asEmbeddings();
await embeddings(http, {
  model: 'text-embedding-v1',
  inputs: chunks,
  contextualized: true,
  documentContext: documentText, // optional
});
embeddings(
    http,
    "text-embedding-v1",
    chunks,
    contextualized=True,
    document_context=document_text,  # optional
)

Retrieve web search results without requesting a generated answer.

use Prism\Prism\PrismManager;

$provider = app(PrismManager::class)->resolve('perplexity');

$results = $provider->search('what is prism php', [
    'max_results' => 5,
    'search_domain_filter' => ['github.com'],
]);
import { search } from '@particle-academy/prism-perplexity';

const results = await search(http, 'what is prism php', {
  max_results: 5,
  search_domain_filter: ['github.com'],
});
from prism_perplexity import search

results = search(
    http,
    "what is prism php",
    {"max_results": 5, "search_domain_filter": ["github.com"]},
)

PHP exposes search as a provider method. TypeScript and Python expose a standalone function.

search() also takes an array of queries to run together. The options are sent to Perplexity's search endpoint as given; options set to null or to an empty array are left out of the request. Results are plain arrays (objects in TypeScript, dicts in Python), so every field Perplexity sends reaches you.

An empty result list is an answer. It comes back as [].

Agent API#

The Agent API runs long research tasks in the background. create() starts a run and returns at once; retrieve() reads its current state, wait() polls until it finishes, and cancel() stops it.

use Prism\Perplexity\Agent\AgentStatus;
use Prism\Prism\PrismManager;

$agent = app(PrismManager::class)->resolve('perplexity')->agent();

$queued = $agent->create('Research the Prism ecosystem', [
    'preset' => 'deep-research',
    'max_steps' => 8,
]);

$snapshot = $agent->retrieve($queued->id);
$finished = $agent->wait($queued->id, maxAttempts: 300, intervalMilliseconds: 1000);

if ($finished->isSuccessful()) {
    echo $finished->text();
} elseif ($finished->status === AgentStatus::Failed) {
    report($finished->error?->message);
}

$agent->cancel($queued->id);
import { AgentClient } from '@particle-academy/prism-perplexity';

const agent = new AgentClient(http);

const queued = await agent.create('Research the Prism ecosystem', {
  preset: 'deep-research',
  max_steps: 8,
});

const snapshot = await agent.retrieve(queued.id);
const finished = await agent.wait(queued.id, 300, 1000); // maxAttempts, intervalMs

if (finished.isSuccessful()) {
  console.log(finished.text());
} else if (finished.status === 'failed') {
  console.error(finished.error?.message);
}

await agent.cancel(queued.id);
from prism_perplexity import AgentClient, AgentStatus

agent = AgentClient(http)

queued = agent.create(
    "Research the Prism ecosystem",
    {"preset": "deep-research", "max_steps": 8},
)

snapshot = agent.retrieve(queued.id)
finished = agent.wait(queued.id, max_attempts=300, interval_seconds=1.0)

if finished.is_successful():
    print(finished.text())
elif finished.status is AgentStatus.FAILED and finished.error is not None:
    print(finished.error.message)

agent.cancel(queued.id)

create() takes the input (a string, or a list of input items) and an options array that is sent to Perplexity as the request body. background defaults to true; pass 'background' => false to hold the request open until the run completes.

Responses#

Every call returns an AgentResponse:

Field Contains
id the response id — keep it to retrieve or cancel the run later
status queued, in_progress, completed, failed, incomplete or cancelled
model the model that ran, or null
output the output items
annotations every annotation (citations) from the output's content parts, flattened
usage the provider's usage and cost object, unmodified
error an AgentError (message, code, type) when the run reported one
createdAt / created_at Unix timestamp, or null
raw the unmodified response body

isTerminal() / is_terminal() is true for completed, failed, incomplete and cancelled; isSuccessful() / is_successful() only for completed. text() joins every text part of the output. PHP's status is the Prism\Perplexity\Agent\AgentStatus enum (AgentStatus::Completed, …), and Python's is the AgentStatus enum (AgentStatus.COMPLETED, …); TypeScript uses the string values. PHP responses also carry protocol (AgentProtocol::Agent).

A run that ends failed, incomplete or cancelled is returned as a response, not thrown. Check status before reading text().

Waiting#

wait() retrieves the run up to maxAttempts times, sleeping the interval between attempts, and returns as soon as the run is terminal. If it is still running after the last attempt, wait() throws — it never returns an unfinished run.

Defaults Throws on timeout
PHP maxAttempts: 60, intervalMilliseconds: 1000 Prism\Perplexity\Agent\AgentWaitTimedOut ($responseId, $attempts)
TypeScript maxAttempts = 60, intervalMs = 1000 PerplexityError with code agent_wait_timed_out
Python max_attempts=300, interval_seconds=1.0 PerplexityError with code agent_wait_timed_out

A deep-research run routinely takes about a minute, so the PHP and TypeScript defaults of 60 one-second attempts can time out on a run that is working. Pass a larger maxAttempts for those. In a queue worker, store the response id and call retrieve() from a later job instead of waiting.

A maxAttempts below 1 or a negative interval is rejected (InvalidArgumentException in PHP, invalid_argument in TypeScript and Python).

The sleep between attempts can be replaced, for example in tests: the first argument to agent() in PHP (a closure receiving milliseconds), and the second constructor argument to AgentClient in TypeScript (returns a promise, receives milliseconds) and Python (receives seconds).

Perplexity does not document an endpoint that lists Agent responses, so there is no list().

Client options (PHP)#

search() and agent() accept Laravel HTTP client options and retry settings; embeddings use Prism's own withClientOptions() and withClientRetry():

use Prism\Prism\Facades\Prism;
use Prism\Prism\PrismManager;

$provider = app(PrismManager::class)->resolve('perplexity');

// search(query, options, clientOptions, clientRetry)
$results = $provider->search('prism php', ['max_results' => 5], ['timeout' => 10], [3, 500]);

// agent(sleeper, clientOptions, clientRetry)
$agent = $provider->agent(clientOptions: ['timeout' => 600], clientRetry: [2, 1000]);

$response = Prism::embeddings()
    ->using('perplexity', 'text-embedding-v1')
    ->fromInput('The quick brown fox')
    ->withClientOptions(['timeout' => 20])
    ->withClientRetry(3, 500)
    ->asEmbeddings();

$clientRetry is the argument list for Laravel's retry(): times, sleep in milliseconds, an optional when callback and throw. The Agent API client defaults to a 300-second timeout; a timeout in $clientOptions replaces it.

Errors#

Perplexity can report a failure inside an HTTP 200 response. Every call checks the response body for an error object and throws when it finds one, so a failure is never returned as an empty vector set or an empty result list. A response that is not a JSON object also throws.

In PHP these are Prism\Prism\Exceptions\PrismException, with the message Perplexity Error [<status>]: <type> - <message> and the HTTP status as the exception code. The type is Perplexity's error type when it sent one, or unreadable_response, invalid_response, search_error, embeddings_error or agent_request_error.

In TypeScript and Python they are PerplexityError, with a stable code, the provider's error type (providerType / provider_type) and the HTTP status when known:

Code Meaning
unreadable_response the endpoint did not return a JSON object
provider_error the response carried an error, or the Agent API answered with a non-2xx status
invalid_response an Agent API response is missing its id or a recognised status
agent_wait_timed_out wait() ran out of attempts
invalid_argument wait() was called with invalid arguments

Python also exposes the codes as the ErrorCode enum.

use Prism\Perplexity\Agent\AgentWaitTimedOut;
use Prism\Prism\Exceptions\PrismException;

try {
    $finished = $agent->wait($queued->id, maxAttempts: 5);
} catch (AgentWaitTimedOut $e) {
    // Still running: keep $e->responseId and retrieve it later.
} catch (PrismException $e) {
    report($e);
}
import { PerplexityError } from '@particle-academy/prism-perplexity';

try {
  await agent.wait(queued.id, 5, 1000);
} catch (error) {
  if (error instanceof PerplexityError && error.code === 'agent_wait_timed_out') {
    // Still running: keep queued.id and retrieve it later.
  } else if (error instanceof PerplexityError && error.code === 'provider_error') {
    console.error(error.status, error.providerType, error.message);
  } else {
    throw error;
  }
}
from prism_perplexity import ErrorCode, PerplexityError

try:
    agent.wait(queued.id, max_attempts=5)
except PerplexityError as error:
    if error.code == ErrorCode.AGENT_WAIT_TIMED_OUT.value:
        pass  # Still running: keep queued.id and retrieve it later.
    elif error.code == ErrorCode.PROVIDER_ERROR.value:
        print(error.status, error.provider_type, error.message)
    else:
        raise

Source: Particle-Academy/prism-perplexity.