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
choicequestion 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
choiceanswer always has a top option, even when no option answers. Thenoul(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.
choiceprobabilities 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:search_client.pysends 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.sentences.pysplits each snippet into sentences.quote_finder.pysends 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.- 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?
- 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”.
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 systempython3can 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
requirements.txt
pyproject.toml holds settings for pytest, plus ruff and mypy if you use them. The script runs without it.
pyproject.toml
pyproject.toml
requirements.txt and pyproject.toml in the new folder when the comment says to.
Install and set your key
Install and set your key
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
search_client.py
Sentences: sentences.py
split turns a snippet into a list of sentences, in page order:
- Replace each
...section break with a new line, then split on new lines. - 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. - 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. - Drop pieces shorter than 25 characters (stray headings and labels) or longer than 400, and drop exact repeats.
- Keep at most 255, the
choiceoption limit.
sentences.py
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
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.
Imports and cutoffs
Imports and cutoffs
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, achoicewhose options are the sentence numbers. Each option’s description is the sentence itself.has_answer, anoulwithcriteriathat 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”).
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.
The requests
The requests
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, theconfidence, andanswers["has_answer"]["noul"]. - Candidates with
has_answerat 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.
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.
The pipeline
The pipeline
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.
Printing and command line
Printing and command line
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
test_quote_finder.py
Run the tests
Run the tests
Run it
Each run searches the live web, so the results and the probabilities can change from run to run. Each run overwritesout/run.json, which holds your question, the page text, and the request IDs. Add out/ to .gitignore if the folder is in a repository.
Run the quote finder
Run the quote finder
pplx-decider-v1.1-27b. Your output will differ.
Observed output: a numeric fact
Observed output: a numeric fact
Observed output: a date
Observed output: a date
Observed output: no answer
Observed output: no answer
Reading the numbers
The bones question. Every result had a sentence with 206 in it, and everyhas_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.
The ten questions
The ten questions
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
datefield 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
dateof 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_filterto the Search API request, orsearch_recency_filterfor recent facts. Addsearch_language_filterto 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.jsonwith your answer. - Quote your own documents. Replace
search_client.pywith a function that returnsResultobjects from your own text. Nothing else changes.
Troubleshooting
Set PERPLEXITY_API_KEY first: run theexportline in this terminal.401from either API: the key is wrong or inactive. A Decisions API401carries 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.jsonand read theprobabilitiesmap for that result. A close runner-up shows up as a lowconfidence. RaiseANSWER_CUTOFFand expect more no-answer results. 400withEach decision needs between 1 and 255 options: achoicegot more than 255 sentences. Check theMAX_SENTENCEScap insentences.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-2and compareout/run.jsonwithout/run-2/run.json.
The complete files
Each file in full, for copying.requirements.txt
requirements.txt
pyproject.toml
pyproject.toml
search_client.py
search_client.py
sentences.py
sentences.py
decisions_client.py
decisions_client.py
quote_finder.py
quote_finder.py
test_quote_finder.py
test_quote_finder.py