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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Ready-to-run applications demonstrating real-world use cases:
- **[Disease Information App](docs/examples/disease-qa/)** - Interactive medical information lookup
- **[Financial News Tracker](docs/examples/financial-news-tracker/)** - Real-time market analysis
- **[Equity Research Brief](docs/examples/equity-research-brief/)** - Agent API + `finance_search` for ticker-level research briefs
- **[Finance Chart (Sandbox)](docs/examples/finance-chart-sandbox/)** - Agent API + `finance_search` + `sandbox` to chart a stock's price history
- **[Academic Research Finder](docs/examples/research-finder/)** - Literature discovery and summarization
- **[Discord Bot](docs/examples/discord-py-bot/)** - Discord integration example

Expand Down
234 changes: 234 additions & 0 deletions docs/examples/finance-chart-sandbox/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
---
title: Finance Chart (Sandbox)
description: Chart a stock's closing-price history with the Perplexity Agent API sandbox tool — fetched and built inside an isolated container as a background task, then rendered locally.
sidebar_position: 8
keywords: [agent-api, sandbox, code-execution, stock-chart, matplotlib, csv, background]
---

# Finance Chart (Sandbox)

A command-line tool that charts a stock's closing-price history using Perplexity's [Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart) [`sandbox`](https://docs.perplexity.ai/docs/agent-api/tools/sandbox) tool.

The whole agent loop runs inside **one background Agent API request**:

1. The model is given the `sandbox` tool — a full agentic Python environment that includes the Perplexity SDK (web search + URL fetch).
2. Inside the sandbox it finds and fetches the ticker's historical daily closing prices, parses them, and prints a clean `date,close` CSV to stdout between sentinel fences.

The script polls the request to completion, pulls the CSV out of the sandbox's stdout, saves it, and renders the line chart **locally** with matplotlib.

![AAPL closing price chart, last month](../../static/img/finance-chart-sandbox-aapl.png)

## How it differs from the docs (important)

This recipe was built and verified against the live Agent API. A few realities shape the design:

- **The `sandbox` tool requires a background task.** On the synchronous/streaming path the request is rejected with `streaming failed: ... unknown tool "sandbox"`. You must submit with `background: true` and poll the response by id. This script always does that.
- **`stdout` is nested.** The execution output lives at `sandbox_results.results[].stdout`, not at the top level of the `sandbox_results` item.
- **`finance_search` has no history (current deployment).** The top-level `finance_search` tool returns only the latest quote, so the price *series* is gathered from inside the sandbox using its in-container Perplexity SDK.
- **Sandbox data fetching is best-effort.** Because the sandbox pulls from third-party web sources, requests can be rate-limited. The script retries the whole call a few times (`--attempts`) until it gets a usable CSV.
- **No PNG comes back.** `sandbox_results` carries only text, so the chart is rendered client-side — and you keep a reusable `.csv`.

The Agent API is called over **raw HTTP** (stdlib `urllib`, no SDK) so the exact request body is visible and the endpoint is configurable.

## Features

- One background request orchestrates the sandbox; the script polls it to completion (resilient to transient 5xx)
- Sandbox fetches the price history itself and emits a fenced `date,close` CSV; the script extracts it from `sandbox_results.results[].stdout` (with message-text fallbacks)
- Automatic retries until a usable CSV is parsed
- Renders a clean closing-price line chart with matplotlib (headless `Agg` backend)
- Saves both a reusable CSV and a PNG
- Configurable `--base-url` / `PERPLEXITY_BASE_URL`; reports sandbox invocation count and request cost

## Prerequisites

- Python 3.9+
- A Perplexity API key with Agent API access. The `sandbox` tool is in **preview** — see the [sandbox docs](https://docs.perplexity.ai/docs/agent-api/tools/sandbox) for availability.

## Installation

```bash
cd docs/examples/finance-chart-sandbox
pip install -r requirements.txt # matplotlib only — the API is called over raw HTTP
chmod +x finance_chart_sandbox.py
```

## API Key Setup

```bash
export PERPLEXITY_API_KEY="your-api-key-here"
```

You can also pass `--api-key`, place the key in a `.pplx_api_key` file, or add a `PERPLEXITY_API_KEY=` / `PPLX_API_KEY=` line to a local `.env`.

## Quick Start

Chart Apple's last 6 months of closing prices:

```bash
./finance_chart_sandbox.py AAPL
```

This writes `AAPL_6mo.csv` and `AAPL_6mo.png` to the current directory.

## Usage

```bash
./finance_chart_sandbox.py TICKER [--period 6mo] [--start YYYY-MM-DD --end YYYY-MM-DD] \
[--model MODEL] [--attempts 3] [--max-steps 25] [--poll-timeout 300] \
[--out-dir DIR] [--base-url URL] [--api-key KEY] [--keep-json]
```

### A one-month chart

```bash
./finance_chart_sandbox.py AAPL --period 1mo
```

### An explicit date range, more retries

```bash
./finance_chart_sandbox.py MSFT --start 2025-01-01 --end 2025-06-30 --attempts 5
```

### Point at a different endpoint

```bash
PERPLEXITY_BASE_URL=https://api.perplexity.ai ./finance_chart_sandbox.py NVDA
```

## Example Output

```
[attempt 1/3] Asking the sandbox to fetch AAPL closing prices over the past 1 month...

Data points: 21 (2026-04-30 → 2026-05-29)
CSV: AAPL_1mo.csv
Chart: AAPL_1mo.png
Sandbox invocations: 8
Cost: 0.3509 USD
```

The CSV (`AAPL_1mo.csv`):

```csv
date,close
2026-04-30,271.35
2026-05-01,280.14
2026-05-04,276.83
...
```

…and `AAPL_1mo.png` is a line chart of `close` over `date`.

## Web UI (FastAPI + JS)

A small web app in [`webapp/`](webapp/) puts a **natural-language** front door
on the agent loop: ask *"What was Apple's stock price over the last 6 months?"*
and the model resolves the ticker and period itself, fetches the prices in the
sandbox, and the page charts the result.

Unlike the CLI (which calls the API over raw HTTP), the web backend uses the
**Perplexity Python SDK** and reuses the CLI module's CSV-extraction and parsing
helpers. It runs a **two-phase** flow, because the `sandbox` tool only runs as a
(non-streamable) background task:

1. **Data** — a background call (`client.responses.create(..., background=True)`
then `client.responses.retrieve(id)`) where the sandbox resolves the ticker +
period, fetches the prices, and prints a `META`/`CSV` block.
2. **Answer** — a separate **streaming** call (`stream=True`) that writes a short
natural-language analysis of the series, token by token.

| Endpoint | Purpose |
| --- | --- |
| `POST /api/charts` | Submit a question (`{query, attempts?}`) → returns a `job_id` |
| `GET /api/charts/{job_id}/events` | **Server-Sent Events**: `progress` → `chart` → streamed `token`s → `done` |
| `GET /api/charts/{job_id}/response.json` | The **raw Agent API response** from phase 1 (sandbox code, stdout, usage) |
| `GET /api/charts/{job_id}/csv` | Download the `date,close` CSV |

The job runs in a worker thread and writes incremental state onto the job; the
SSE endpoint merely *tails* that state, so reconnects never re-run the work. The
frontend (vanilla JS + [Chart.js](https://www.chartjs.org/) from a CDN — no
build step) renders the chart on the `chart` event, appends the streamed
analysis live, and links to the raw JSON for inspection.

![Finance Chart sandbox web UI](../../static/img/finance-chart-sandbox-ui.png)

### Run it

```bash
cd docs/examples/finance-chart-sandbox/webapp
pip install -r requirements.txt # perplexityai + fastapi + uvicorn
export PERPLEXITY_API_KEY="your-api-key-here" # or a .env in this dir
python app.py # serves http://127.0.0.1:8000
# PORT=8060 python app.py # if 8000 is taken
```

Open the page, type a question (or click an example), and hit **Ask**. The
status line updates per attempt while the background sandbox runs (~30–60s).

## Code Walkthrough

**1. Submit a background request with the sandbox tool (raw HTTP).**

```python
payload = {
"model": "openai/gpt-5.5",
"instructions": SYSTEM_PROMPT, # "print the CSV between fences"
"input": "Produce the daily closing-price CSV for AAPL over the past 6 months. ...",
"tools": [{"type": "sandbox"}],
"background": True, # required for the sandbox tool
"max_steps": 25,
}
# POST https://api.perplexity.ai/v1/responses (Authorization: Bearer <key>)
```

**2. Poll the response by id until it completes.**

```python
# GET https://api.perplexity.ai/v1/responses/{id}
while body["status"] in ("queued", "in_progress"):
time.sleep(3)
body = get(f"/v1/responses/{body['id']}") # tolerate transient 5xx
```

**3. Pull the CSV out of the nested sandbox stdout.**

```python
for item in body["output"]:
if item["type"] == "sandbox_results":
for res in item["results"]:
stdout = res["stdout"] # contains the fenced CSV
```

The script searches each `sandbox_results.results[].stdout` for text between the `===CSV_START===` / `===CSV_END===` fences (falling back to the message text and a ```` ```csv ```` block), validates it parses into ≥2 `date,close` rows, and retries the whole call if not.

**4. Render the chart locally.** The CSV is parsed with the stdlib `csv` module and plotted with matplotlib's headless `Agg` backend. Because the sandbox returns only text, rendering lives on the client side and you keep a tidy `.csv`.

## Prompting Guidance

- **Fence the machine-readable output.** Asking the sandbox to wrap the CSV in unique sentinel lines makes extraction robust even when the model adds commentary or debug prints.
- **Tell it to retry sources.** Public price endpoints rate-limit (e.g. Yahoo `429`) or gate behind captchas; instructing the model to try another source on failure improves the hit rate.
- **Forbid fabrication.** The system prompt instructs the model to use only prices it actually retrieved — never to interpolate or estimate.

## Pricing

- **`sandbox`**: `$0.03` per container session
- **In-sandbox SDK search queries**: `$0.005` per request (the sandbox issues these to gather the data)
- **Model tokens**: billed separately per Agent API token pricing

Sandbox invocations are counted under `usage.tool_calls_details.sandbox.invocation`. A typical run here is a few sandbox calls plus a handful of in-sandbox searches. See [Perplexity Pricing](https://docs.perplexity.ai/docs/getting-started/pricing) for current rates.

## Limitations

- `sandbox` is in **preview** and must be run as a background task
- Price history is fetched from third-party web sources inside the sandbox, so **data accuracy and availability depend on those sources** — values should be sanity-checked, and obscure/non-US tickers may fail
- Fetching is **best-effort**: rate limits can cause an attempt to return no CSV; the script retries, but a run may still fail (raise `--attempts`)
- Each attempt is a separate billed sandbox session
- This is not investment advice

## Resources

- [Sandbox Tool](https://docs.perplexity.ai/docs/agent-api/tools/sandbox)
- [Agent API Quickstart](https://docs.perplexity.ai/docs/agent-api/quickstart)
- [Finance Search Tool](https://docs.perplexity.ai/docs/agent-api/tools/finance-search)
- [Perplexity Python SDK](https://github.com/ppl-ai/perplexity-python)
Loading
Loading