Skip to main content
The Search API returns up to five pages for a question. Your user wants the one sentence that answers it, in the page’s own words, with a link. Put the Decisions API in between. Send it each result’s sentences as numbered options, and it returns a probability for each one, plus the probability that any of them answers. Your code ranks by those probabilities and prints the top sentence with its URL, or says that no result answers.

A live run of this recipe on two questions: one with an answer and one without. The step captions and API panels were added for the recording, and request IDs are shortened. The script prints the terminal lines and writes out/run.json.

Why the Decisions API

  • Quickly evaluate options. A choice question takes up to 255 options. Each sentence is an option, keyed by its number, so the answer is a sentence number your code looks up. There is no reply to parse and nothing to match back to the page.
  • A way to say “none of these”. A choice answer always has a top option, even when no option answers. The noul (yes/no) question in the same request gives the probability that any sentence answers, and your code compares it to a cutoff before it trusts the ranking.
  • One consistent scale across results. choice probabilities sum to about 1 inside one request, so 0.9 in one result and 0.7 in another are not comparable. A second request puts the top sentence from each result side by side and asks the same yes/no question of each, so those probabilities are comparable.
  • One key, multiple APIs. The Search API and the Decisions API use the same Perplexity API key.

What you will build

A command-line tool. You give it a question. It prints one sentence from a live search result with the title, URL, and date of the page, or a line saying that no result answers. The project is seven files in one folder:
Every run takes five steps:
  1. search_client.py sends your question to the Search API and gets up to five results. Each result has a title, URL, date, and a snippet: text extracted from the page.
  2. sentences.py splits each snippet into sentences.
  3. quote_finder.py sends one Decisions API request per result. The request numbers the result’s sentences and asks two questions: which numbered sentence most directly answers the question (choice), and whether any of them answers it (noul). Your code keeps the top-probability sentence as that result’s candidate.
  4. Candidates from results whose “any of them answers” probability is at least 0.3 go into one more request, which asks the same yes/no question of each candidate: does it state the answer?
  5. If the highest of those probabilities is at least 0.6, your code prints that sentence as the answer. Otherwise it prints that no result answers, followed by the closest candidate marked “below cutoff”.
Only quote_finder.py chooses and prints a sentence. The Decisions API returns a probability for every sentence number it is shown and a probability that any of them answers. Every character of the quote came back from the Search API. One run makes one Search API request and at most six Decisions API requests: one per result and one to compare. Retries can add attempts.

What you need

  • Python 3.10 or newer on macOS or Linux. Check with python3 --version; on macOS the system python3 can be older.
  • A Perplexity API key from the API Console. One key works for both APIs.

Set up

requirements.txt pins the two packages to the tested versions. httpx sends the requests. pytest runs the tests.
requirements.txt
pyproject.toml holds settings for pytest, plus ruff and mypy if you use them. The script runs without it.
pyproject.toml
Run these commands. Save requirements.txt and pyproject.toml in the new folder when the comment says to.
In a new terminal, run the source and export lines again.

The Search API client: search_client.py

search_client.py sends one POST to https://api.perplexity.ai/search with your question, max_results: 5, and max_tokens_per_page: 2048. It keeps four fields from each result: title, url, snippet, and date. date can be missing or null, so it is optional. A 429 or 5xx gets two retries, waiting for the Retry-After header when there is one. The snippet is not the whole page. It is the part of the page the Search API extracted for your question, with sections separated by .... For the Mariana Trench question, snippets were 438 to 2,441 characters long, and raising max_tokens_per_page to 4096 returned the same text. The quote can only come from the snippet, because this recipe does not fetch pages.
search_client.py

Sentences: sentences.py

split turns a snippet into a list of sentences, in page order:
  1. Replace each ... section break with a new line, then split on new lines.
  2. Remove Markdown line markers at the start of each line: # headings, * and - bullets, and > quotes. A marker counts only when a space follows it, so **bold** at the start of a line stays intact.
  3. Split each line where ., !, or ? is followed by a space and a capital letter, digit, quote mark, or opening parenthesis. A footnote marker such as ^[1]^ right after the punctuation stays with its sentence.
  4. Drop pieces shorter than 25 characters (stray headings and labels) or longer than 400, and drop exact repeats.
  5. Keep at most 255, the choice option limit.
The splitter only trims whitespace and line markers at the edges. Everything else, including Markdown emphasis and the source’s own typos, stays exactly as the Search API returned it.
sentences.py

The Decisions API client: decisions_client.py

decisions_client.py sends one request to https://api.perplexity.ai/v1/decisions with the model pplx-decider-v1.1-27b, your state, and your questions. It returns the response’s answers object untouched, so callers can read choice, probabilities, confidence, and noul. It also keeps the x-request-id header, the input token count, and the time the request took. Like the Search client, it retries twice on 429, 500, 502, and 503, waiting for Retry-After. The timeout is 30 seconds, which covers any request under the input limit.
decisions_client.py

The quote finder: quote_finder.py

quote_finder.py is shown in four parts. Paste them into one file in order, with two blank lines between parts.

Imports and cutoffs

The three cutoffs are starting points, not validated production values. Tune them on your own questions.
  • HAS_ANSWER_CUTOFF = 0.3: a result whose “any of these answers” probability is below this sends no candidate to the compare step.
  • ANSWER_CUTOFF = 0.6: the top candidate needs at least this “states the answer” probability to be printed as the answer.
  • MIN_SENTENCES = 1: a result whose snippet has no usable sentence is skipped.
Candidate holds the top-probability sentence of one result and the numbers behind it. answers stays empty until the compare step fills it.
quote_finder.py (part 1 of 4)

The requests

rank_request builds the request for one result. The state holds your question, the page title and URL, and the numbered sentences. It asks two questions:
  • best, a choice whose options are the sentence numbers. Each option’s description is the sentence itself.
  • has_answer, a noul with criteria that spell out what counts as yes (“a reader could quote that sentence as the answer”) and no (“the sentences discuss the topic but none states the answer”).
The sentences appear twice on purpose: in state, so has_answer can read them, and as the choice descriptions, so each option carries its own text. In our tests the repeat added 7 to 36 percent to a request’s input tokens, compared with null descriptions. One of these requests used 237 to 2,554 input tokens. compare_request puts every candidate in one state and asks one noul per candidate, named answers_1, answers_2, and so on. Each one asks whether that candidate states the answer to the question. Two requests, because a choice says which sentence in one result is most likely the answer relative to its neighbors, and its probabilities sum to 1 inside that request. Two results’ top probabilities are not on the same scale until the compare request asks the same yes/no question of each candidate in one state.
quote_finder.py (part 2 of 4)

The pipeline

find_quote runs the five steps on one question.
  • It splits every snippet. A result with no usable sentence is printed as skipped: no sentences.
  • It sends one rank request per result and reads answers["best"]["choice"] (a string such as "2"), that option’s probability, the confidence, and answers["has_answer"]["noul"].
  • Candidates with has_answer at or above 0.3 go into the compare request. If none qualify, there is no compare request.
  • It sorts candidates by their compare probability. Ties go to the higher has_answer, then to the earlier result. The top one is the answer if its probability is at least 0.6.
  • If nothing passes, it prints the no-answer line and the closest candidate, marked “below cutoff”. When there was no compare request, the closest candidate is the one with the highest has_answer.
The finally block writes out/run.json even if a request fails partway, so an interrupted run still leaves a record. It holds every result with its sentences, every probability, and every Decisions x-request-id. answer is null when nothing passes the cutoff, and compare is null when the compare step was skipped.
quote_finder.py (part 3 of 4)

Printing and command line

The results table prints one line per result: sentence count, has_answer, the top sentence number, its probability, and confidence. A result whose has_answer falls below the cutoff shows dashes and no candidate. Probabilities print with three decimals. The quote prints exactly as the Search API returned it, in straight double quotes, followed by the page title, URL, and date. The script takes one question and an optional --out folder.
quote_finder.py (part 4 of 4)

Test it

Save all seven files in your folder, then run the tests before your first live run. They need no API key or network. They cover the splitter, the shape of both requests, the two cutoffs, a run where no result sends a candidate, the run record after a failed request, and parsing a Search API response, with fake API responses and made-up probabilities.
test_quote_finder.py

Run it

Each run searches the live web, so the results and the probabilities can change from run to run. Each run overwrites out/run.json, which holds your question, the page text, and the request IDs. Add out/ to .gitignore if the folder is in a repository.
Recorded on 2026-10-09 at 18:19 UTC with pplx-decider-v1.1-27b. Your output will differ.
Recorded on 2026-10-09 at 18:19 UTC.
Recorded on 2026-10-09 at 18:19 UTC. No page in the top five gives a count for one floor’s railing.

Reading the numbers

The bones question. Every result had a sentence with 206 in it, and every has_answer was 0.999 or higher. The compare request scored four of the five candidates between 0.996 and 0.998. “Adults have between 206 and 213 bones” came in lower, at 0.915. Britannica’s sentence printed as the answer at 0.998. The Hubble question. Results 1 and 2 both show 0.997 at three decimals. Your code compares the unrounded values, and result 2 was higher (0.99742 against 0.99716), so the NASA sentence printed. The rivets question. Every page gives the tower’s total of about 2.5 million rivets, and none gives a count for the second floor. Four results had has_answer below 0.3 and sent no candidate. Result 1 cleared that cutoff at 0.320, and its candidate scored 0.536 in the compare request, under the 0.6 cutoff. Your code printed the no-answer line and showed that sentence as the closest. Ten questions. We ran the recipe on 2026-10-09 between 21:14 and 21:15 UTC on the ten questions below: six with a known numeric or dated answer, and four built to have no sentence-level answer in the top results. We read every printed quote. A correct no-answer means none of the five snippets answers the question. It does not mean no page on the web does.
The wrong quote came from “What color was the ink used to sign the Treaty of Rome in 1957?” No page names an ink color. The top result was a Romanian article whose headline says the treaty was signed on blank pages. That headline got a has_answer of 0.906 and a compare probability of 0.973, so your code printed it. A high probability is an estimate that the sentence states the answer to your question. It does not mean the sentence is about what you asked. A quote that answers is not a quote that is true. Treat the printed sentence as the best candidate from these results, and check it before you rely on it for anything that matters. Repeats. We ran each of the three questions above five times in a row. All fifteen runs got the same five results and printed the same sentence with the same probabilities: 0.998 for bones, 0.997 for Hubble, and 0.536 for rivets. Search results change over days and weeks, and the Decisions quickstart notes that identical requests can occasionally differ in the second decimal place. Time and tokens. In the ten-question run, a rank request used 237 to 2,554 input tokens (median 678) and took 0.24 to 0.49 seconds (median 0.26). A full question, including the search, took 1.6 to 2.6 seconds (median 2.0). Token counts repeat closely from run to run; timings vary, and these come from that one run. What to watch for.
  • The date field is whatever the Search API returns for the page. For the Wikipedia results in our runs, it was a date from 2001, years before the facts on the page.
  • A quote can be a real published figure and still not the current one. In two runs of the Mariana Trench question, Wikipedia results gave 10,935 ± 6 meters and 10,984 ± 25 metres, both with a date of 2001-05-14. The first matches NOAA’s revised 2021 measurement. They scored 0.986 and 0.985 in the compare request. The probability covers whether a sentence states the answer, not whether the figure is current.
  • A snippet can end mid-sentence. In one run, the quote for a Statue of Liberty question stopped at “has 64” because that was where the snippet stopped.
  • The splitter breaks after abbreviations such as “Aug.” when a number follows, so “Voyager 1 entered interstellar space on Aug.” showed up as a candidate. It scored 0.141 in the compare request and was not printed.
  • Results can be in other languages. The Treaty of Rome question returned Romanian, Japanese, French, and Spanish pages.

Adapt it

  • Return the top three quotes. Print the three highest compare probabilities with their URLs for a research notes tool.
  • Quote only trusted sources. Add search_domain_filter to the Search API request, or search_recency_filter for recent facts. Add search_language_filter to keep quotes in one language.
  • Add one sentence of framing. Send the question and the quote to the Agent API and ask for a one-sentence lead-in. Print the quote unchanged after it.
  • Keep a citation record. Store the quote, URL, date, and request IDs from out/run.json with your answer.
  • Quote your own documents. Replace search_client.py with a function that returns Result objects from your own text. Nothing else changes.

Troubleshooting

  • Set PERPLEXITY_API_KEY first: run the export line in this terminal.
  • 401 from either API: the key is wrong or inactive. A Decisions API 401 carries no request ID.
  • No results: the search returned nothing. The script exits normally.
  • Every result is skipped: no sentences: the snippets had no piece between 25 and 400 characters. Rephrase the question.
  • The quote is on topic but does not answer: open out/run.json and read the probabilities map for that result. A close runner-up shows up as a low confidence. Raise ANSWER_CUTOFF and expect more no-answer results.
  • 400 with Each decision needs between 1 and 255 options: a choice got more than 255 sentences. Check the MAX_SENTENCES cap in sentences.py.
  • 429: one question sends its Decisions requests one at a time, well under the 10 requests per second limit. If you run many questions in parallel, the client retries twice and then fails; slow down.
  • A different sentence on a rerun: the search results changed, or a probability moved. Each run overwrites out/run.json, so save the second run elsewhere with --out out/run-2 and compare out/run.json with out/run-2/run.json.

The complete files

Each file in full, for copying.
requirements.txt
pyproject.toml
search_client.py
sentences.py
decisions_client.py
quote_finder.py
test_quote_finder.py