diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2b4c5a8..c4401e1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,7 +3,6 @@ Brief description of your contribution ## Type of Contribution - [ ] Example Tutorial -- [ ] Showcase Project - [ ] Article/Integration Guide - [ ] Documentation Update - [ ] Bug Fix @@ -22,7 +21,7 @@ Brief description of your contribution **What makes this contribution valuable to other developers?** - + **External Links (if applicable):** - GitHub Repository: @@ -36,4 +35,4 @@ Brief description of your contribution ## Additional Notes - \ No newline at end of file + diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index f38c33c..6630651 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -12,30 +12,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v6 - + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: '18' + node-version: '20' cache: 'npm' - + - name: MDX validation dependencies - run: npm install --save-dev @mdx-js/mdx @mdx-js/loader glob - + run: npm ci + - name: Validate MDX files run: node scripts/validate-mdx.js - + - name: Check for broken links run: | - # Simple check for common broken link patterns - echo "Checking for potential broken links..." - if grep -r "http://localhost\|http://127.0.0.1" docs/ --exclude-dir=showcase; then - echo "❌ Found localhost links that should be removed" + echo "Checking for localhost Markdown links..." + if grep -rE '\]\(http://(localhost|127\.0\.0\.1)' docs/; then + echo "❌ Found localhost Markdown links that should be removed" exit 1 fi - echo "✅ No obvious broken links found" - + echo "✅ No localhost Markdown links found" + - name: Validate frontmatter run: | # Check that all MDX files have required frontmatter @@ -45,4 +44,4 @@ jobs: exit 1 fi echo "✅ $file - Has frontmatter" - done \ No newline at end of file + done diff --git a/.github/workflows/sync-to-docs.yml b/.github/workflows/sync-to-docs.yml index 8c0ed5b..d45042c 100644 --- a/.github/workflows/sync-to-docs.yml +++ b/.github/workflows/sync-to-docs.yml @@ -10,72 +10,83 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout cookbook repository - uses: actions/checkout@v6 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: path: cookbook-repo - name: Checkout docs repository - uses: actions/checkout@v6 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: repository: ${{ secrets.DOCS_REPO_NAME || 'ppl-ai/api-docs' }} token: ${{ secrets.DOCS_REPO_TOKEN }} path: docs-repo - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: '18' - cache: 'npm' - cache-dependency-path: docs-repo/package.json + node-version: '20' - name: Install docs dependencies run: | + corepack enable cd docs-repo - npm install + pnpm install --frozen-lockfile - name: Clear existing cookbook content run: | - rm -rf docs-repo/cookbook/* || true + rm -rf docs-repo/docs/cookbook - name: Copy cookbook content to docs repository run: | - # Create cookbook directory if it doesn't exist - mkdir -p docs-repo/cookbook - - # Copy docs content from cookbook to docs repo (already in MDX format) - cp -r cookbook-repo/docs/* docs-repo/cookbook/ - - # Copy static assets if they exist + mkdir -p docs-repo/docs/cookbook + + cp -a cookbook-repo/docs/. docs-repo/docs/cookbook/ + if [ -d "cookbook-repo/static" ]; then - mkdir -p docs-repo/cookbook/static - cp -r cookbook-repo/static/* docs-repo/cookbook/static/ + mkdir -p docs-repo/docs/cookbook/static + cp -a cookbook-repo/static/. docs-repo/docs/cookbook/static/ fi - - name: Generate cookbook navigation - run: | - cd docs-repo - # Run the navigation generation script - node scripts/generate-cookbook-nav.js - - name: Configure git run: | cd docs-repo git config --local user.email "cookbook-sync@perplexity.ai" git config --local user.name "Cookbook Sync Bot" - - - name: Commit and push changes + + - name: Commit cookbook content + id: cookbook-content run: | cd docs-repo - git add . + git add docs/cookbook if git diff --staged --quiet; then - echo "No changes to commit" - echo "CHANGES_MADE=false" >> $GITHUB_ENV + echo "changed=false" >> "$GITHUB_OUTPUT" else git commit -m "📚 Sync cookbook from ${{ github.repository }}@${{ github.sha }} - Updated cookbook content and navigation from community contributions. - + Updated cookbook content from the canonical repository. + Source: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}" + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Generate cookbook navigation and gallery data + run: | + cd docs-repo + pnpm sync-cookbook + + - name: Commit and push changes + run: | + cd docs-repo + git add config/navigation.json snippets/cookbookData.jsx + if [ "${{ steps.cookbook-content.outputs.changed }}" = "true" ]; then + git commit --amend --no-edit + git push + echo "CHANGES_MADE=true" >> $GITHUB_ENV + elif git diff --staged --quiet; then + echo "No changes to commit" + echo "CHANGES_MADE=false" >> $GITHUB_ENV + else + git commit -m "📚 Regenerate cookbook navigation and gallery data" git push echo "CHANGES_MADE=true" >> $GITHUB_ENV fi @@ -83,7 +94,7 @@ jobs: - name: Create deployment comment if: env.CHANGES_MADE == 'true' continue-on-error: true - uses: actions/github-script@v8 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v8.0.0 with: script: | try { @@ -100,7 +111,7 @@ jobs: 📈 Changes will be live on docs.perplexity.ai within a few minutes. - 🔗 [View docs site](https://docs.perplexity.ai/cookbook)` + 🔗 [View docs site](https://docs.perplexity.ai/docs/cookbook)` }); console.log('✅ Success comment posted successfully'); } catch (error) { @@ -111,7 +122,7 @@ jobs: - name: Report sync failure if: failure() continue-on-error: true - uses: actions/github-script@v8 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v8.0.0 with: script: | try { @@ -145,4 +156,4 @@ jobs: else echo "ℹ️ No changes to sync" echo "📄 Cookbook content is already up to date" - fi \ No newline at end of file + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d62c0c..50f90f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,22 @@ # Contributing to Perplexity API Cookbook -Thank you for your interest in contributing to our API Cookbook! We welcome high-quality examples that showcase the capabilities of Perplexity's Sonar API. +Thank you for your interest in contributing to our API Cookbook! We welcome high-quality examples that teach developers how to use the Perplexity API Platform. ## Structure -This cookbook contains three main sections: +This cookbook contains two main sections: ### 1. **Examples** (`/docs/examples/`) Step-by-step tutorials and example implementations that teach specific concepts or solve common use cases. -### 2. **Showcase** (`/docs/showcase/`) -Community-built projects that demonstrate real-world applications of the Sonar API. - -### 3. **Articles** (`/docs/articles/`) +### 2. **Articles** (`/docs/articles/`) In-depth integration guides and advanced implementation tutorials for complex use cases and integrations with other tools. ## Contributing Guidelines ### What We're Looking For -- **Clear, educational content** that helps developers understand how to use the Sonar API effectively +- **Clear, educational content** that helps developers understand how to use the Agent API, Search API, or Embeddings API effectively - **Real-world use cases** that solve actual problems - **Well-documented code** with clear explanations - **Novel applications** that showcase unique ways to leverage the API @@ -86,14 +83,6 @@ Any known limitations or considerations users should be aware of. 4. Include any necessary code snippets in your MDX file 5. Submit a pull request -### For Showcase Projects - -1. Build your project in a separate public repository -2. Fork this repository -3. Create a new MDX file under `/docs/showcase/your-project-name.mdx` -4. Include screenshots or demos if applicable -5. Submit a pull request - ### For Articles 1. Fork this repository @@ -113,7 +102,6 @@ Brief description of your contribution ## Type of Contribution - [ ] Example Tutorial -- [ ] Showcase Project - [ ] Article/Integration Guide ## Checklist @@ -157,4 +145,4 @@ If you have questions about contributing, please: 2. Open an issue for discussion before starting major work 3. Contact us at api@perplexity.ai for specific questions -We look forward to seeing your creative applications of the Perplexity Sonar API! +We look forward to seeing your creative applications of the Perplexity API Platform! diff --git a/README.md b/README.md index 59d92e6..282a39c 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,27 @@ -A comprehensive collection of practical examples, integration guides, and community showcases for building with [Perplexity's Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart) — the new primary API for building hosted agents with native web access, citations, code execution, subagents, and durable, long-running work. +A comprehensive collection of practical examples and integration guides for building with [Perplexity's Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart), Search API, and Embeddings API. > **Agent API is now the primary Perplexity API.** New projects should build on the Agent API. The Sonar API (`/chat/completions`) is deprecated — if you're on Sonar, see the [migrate from Sonar guide](https://docs.perplexity.ai/docs/agent-api/migrate-from-sonar) to move to the Agent API. -📖 **[View the full cookbook →](https://docs.perplexity.ai/cookbook)** +📖 **[View the full cookbook →](https://docs.perplexity.ai/docs/cookbook)** ## What's Inside ### 🛠️ [Examples](docs/examples/) -Ready-to-run applications demonstrating real-world use cases: - -- **[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 -- **[Fact Checker CLI](docs/examples/fact-checker-cli/)** - Verify claims and articles for accuracy -- **[Daily Knowledge Bot](docs/examples/daily-knowledge-bot/)** - Automated daily fact delivery system -- **[Disease Information App](docs/examples/disease-qa/)** - Interactive medical information lookup -- **[Financial News Tracker](docs/examples/financial-news-tracker/)** - Real-time market analysis -- **[Academic Research Finder](docs/examples/research-finder/)** - Literature discovery and summarization -- **[Discord Bot](docs/examples/discord-py-bot/)** - Discord integration example - -### 🌟 [Community Showcase](docs/showcase/) -Community-built applications including: -- News and finance apps -- AI-powered search tools -- Browser extensions -- Educational platforms -- And many more innovative projects +Ready-to-run applications demonstrating research, finance, sandbox, MCP, multimodal, Search API, and embeddings workflows. ### 📚 [Integration Guides](docs/articles/) In-depth tutorials for advanced implementations: -- Migrating from Sonar to the Agent API -- Memory management patterns -- OpenAI agents integration -- Multi-modal implementations +- Agent API orchestration and tool use +- Search filtering and academic research +- Structured outputs and streaming citations +- Embeddings and retrieval-augmented generation ## Quick Start -1. **Browse the [documentation](https://docs.perplexity.ai/cookbook)** to find examples that match your needs +1. **Browse the [documentation](https://docs.perplexity.ai/docs/cookbook)** to find examples that match your needs 2. **Clone this repository** and navigate to any example directory 3. **Follow the setup instructions** in each example's README -4. **Get your API key** from [Perplexity](https://docs.perplexity.ai/guides/getting-started) +4. **Get your API key** from [Perplexity](https://console.perplexity.ai) 5. **Build and customize** for your specific use case ## API Key Setup @@ -49,14 +32,14 @@ All examples require a Perplexity API key: export PPLX_API_KEY="your-api-key-here" ``` -Get your API key at [docs.perplexity.ai](https://docs.perplexity.ai/guides/getting-started). +Get your API key in the [API Portal](https://console.perplexity.ai). ## Contributing -Have a project built with the Agent API? We'd love to feature it! +Have an example built with the Perplexity API Platform? We'd love to include it. - **[Submit an Example Tutorial](CONTRIBUTING.md#for-examples)** -- **[Submit a Showcase Project](CONTRIBUTING.md#for-showcase-projects)** +- **[Submit an Integration Guide](CONTRIBUTING.md#for-articles)** - **[View Full Contributing Guidelines](CONTRIBUTING.md)** ## Resources @@ -68,4 +51,4 @@ Have a project built with the Agent API? We'd love to feature it! --- -*This repository syncs to [docs.perplexity.ai/cookbook](https://docs.perplexity.ai/cookbook) on every commit.* +*This repository syncs to [docs.perplexity.ai/docs/cookbook](https://docs.perplexity.ai/docs/cookbook) on every commit to `main`.* diff --git a/SONAR_MIGRATION_INVENTORY.md b/SONAR_MIGRATION_INVENTORY.md new file mode 100644 index 0000000..96ef3ec --- /dev/null +++ b/SONAR_MIGRATION_INVENTORY.md @@ -0,0 +1,119 @@ +# Sonar cookbook migration inventory + +This inventory records the 26 Community Showcase pages, 9 examples, and 6 guides reviewed before the September 27, 2026 end of public Sonar Chat Completions support. It distinguishes content that remains live from Sonar content removed now and ranks the concepts worth rebuilding on the Agent API. + +The complete pre-removal source is preserved in [`api-cookbook` at `543c229`](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs). A prior docs commit, [`514a467a`](https://github.com/ppl-ai/api-docs/commit/514a467adff2b418d254ce3ecb22948b00baaab9), contains live-tested Agent API ports of the ten removed first-party pages. Treat those ports as recovery material, not publication-ready content: review them against the current Agent API contract before reuse. The later revert, [`b264aca7`](https://github.com/ppl-ai/api-docs/commit/b264aca7), preserves their history. + +## Decision summary + +- **Keep unchanged:** `equity-research-brief`, `model-comparison`, `search-news-monitor`, `langchain-vc-memo-agent`, and `multi-provider-orchestration`. These already use the Agent API or Search API, even where an Agent API example selects a `perplexity/sonar` model. +- **Rebuild first:** `fact-checker-cli`, `openai-agents-integration`, `discord-py-bot`, `financial-news-tracker`, and `daily-knowledge-bot`. Consolidate the three memory-management pages into one Agent API memory guide. +- **Rebuild later:** `perplexigrid`, `daily-news-briefing`, `perplexity-flutter`, `citypulse-ai-search`, and `greenify`. +- **Archive only:** the other 21 Community Showcase pages plus `disease-qa` and `research-finder`. +- **Remove now:** all 26 Community Showcase pages, all 6 Sonar examples, and all 4 Sonar guides. Git history and the links below preserve the deleted source. + +## Content that remains live + +These five pages are part of the 41-item audit but are not Sonar Chat Completions content. + +| Item | Type | API surface | Decision | +| --- | --- | --- | --- | +| [Equity Research Brief](docs/examples/equity-research-brief/README.mdx) | Example | Agent API with `finance_search` | Keep unchanged; review its `perplexity/sonar` model choice only if that Agent API model ID is withdrawn. | +| [Model Comparison](docs/examples/model-comparison/README.mdx) | Example | Agent API model routing and fallback | Keep unchanged; it is the canonical comparison recipe. | +| [Search News Monitor](docs/examples/search-news-monitor/README.mdx) | Example | Search API | Keep unchanged; its only Sonar reference was the shared deprecation banner. | +| [LangChain VC Memo Agent](docs/articles/langchain-vc-memo-agent/README.mdx) | Guide | Agent API with `web_search` and `finance_search` | Keep unchanged; use it as the quality bar for framework integrations. | +| [Multi-provider Orchestration](docs/articles/multi-provider-orchestration/README.mdx) | Guide | Agent API model routing, fallback, comparison, and model discovery | Keep unchanged; it already covers the routing lesson attempted by several showcases. | + +## Rebuild first + +The order below follows the source-grounded audit. Rebuild the archived behavior as maintained, first-party Agent API content rather than mechanically replacing an endpoint or republishing community code. + +| Order | Source material | Agent API capability to teach | Key migration concern | +| --- | --- | --- | --- | +| 1 | [Fact Checker CLI](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/examples/fact-checker-cli) | Strict structured verdict/evidence output plus `web_search` source attribution. | Replace the Sonar model allowlist and JSON/text branching; keep the framing factual and avoid verdict-style claims about people. | +| 2 | [OpenAI Agents integration](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/articles/openai-agents-integration) | OpenAI-compatible framework interop, function calling, and custom tools on the Agent API. | Revalidate against the current OpenAI Agents SDK and current Agent API model IDs. | +| 3 | [Discord bot](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/examples/discord-py-bot) | Long-running chat integration, streaming or chunked delivery, and per-user request handling. | Rewrite both Chat Completions call sites and preserve Discord's 2,000-character response handling. | +| 4 | [Memory Management hub](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/articles/memory-management/README.mdx) | Agent API conversation state as the default answer to context-window loss. | Merge the hub and both child recipes into one guide instead of restoring a three-page tree. | +| 5 | [Chat Summary Memory Buffer](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/articles/memory-management/chat-summary-memory-buffer) | Token-budgeted summarize-and-truncate memory as a manual fallback. | Remove Sonar-specific message conversion and avoid making LlamaIndex the only path. | +| 6 | [Persistent Chat Memory](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/articles/memory-management/chat-with-persistence) | Durable cross-session memory with vector retrieval of prior turns. | Distinguish conversation memory from the existing embeddings/RAG guide and revalidate LanceDB/LlamaIndex APIs. | +| 7 | [Financial News Tracker](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/examples/financial-news-tracker) | `finance_search`, structured outputs, and recency windows for topic-level market monitoring. | Keep it distinct from per-ticker briefs and SEC search; frame output as analysis, not investment advice. | +| 8 | [Daily Knowledge Bot](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/examples/daily-knowledge-bot) | Scheduled automation that writes dated file artifacts with retries. | Preserve the scheduling shell while replacing the Sonar request and response parsing. | + +Items 1–3 and 7–8 already have Perplexity-owned runnable code. Items 4–6 are one rebuild project: a consolidated Agent API memory guide with native conversation state first, token-budget summarization second, and durable retrieval when cross-session recall is required. + +## Rebuild later + +| Source concept | Agent API lesson | Why it is later | +| --- | --- | --- | +| [PerplexiGrid](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/perplexigrid.mdx) | Generate a validated visualization specification that an application renders. | Strong structured-output story, but it needs a Perplexity-owned, Supabase-free rewrite and overlaps the existing Competitor Buzz Tracker. | +| [Daily News Briefing](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/daily-news-briefing.mdx) | Deliver a scheduled digest into an external knowledge tool such as Obsidian. | The community project is well maintained, but its sample uses the retired `sonar-medium-online` ID and the digest concept overlaps News Dedupe Digest. | +| [Perplexity Dart and Flutter SDKs](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/perplexity-flutter.mdx) | Type-safe streaming and multimodal Agent API integration for mobile developers. | The third-party SDKs do not support Agent API and would need sponsorship or replacement. | +| [CityPulse](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/citypulse-ai-search.mdx) | Two-stage fast retrieval then reasoning with structured output. | It needs first-party code and a safe, reproducible location-data substitute; routing is already covered by retained recipes. | +| [Greenify](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/greenify.mdx) | Multimodal image input to schema-validated structured output. | Prefer extending the existing Image Analysis example with schema validation if that teaches the same capability. | + +## Archive only + +These pages do not merit a standalone Agent API rebuild. Their historical source remains available for reference. + +### Community Showcase + +| Item | Why it should stay archived | +| --- | --- | +| [4Point Hoops](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/4point-Hoops.mdx) | The reusable API portion is a thin prompt wrapper around scraped sports data in an unlicensed SaaS stack. | +| [Ellipsis](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/Ellipsis.mdx) | Most of its value and complexity is in third-party TTS and podcast distribution, not the Perplexity request. | +| [BazaarAISaathi](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/bazaar-ai-saathi.mdx) | Personalized investment recommendations create avoidable risk, and retained recipes already cover finance research and model routing. | +| [Briefo](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/briefo.mdx) | It is a consumer mobile product rather than a reproducible API pattern; research and memory are covered elsewhere. | +| [CycleSyncAI](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/cycle-sync-ai.mdx) | Personalized health guidance is risky, and the distinctive implementation is iOS HealthKit plumbing. | +| [Executive Intelligence](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/executive-intelligence.mdx) | The research and memory patterns duplicate maintained Agent API examples. | +| [Fact Dynamics](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/fact-dynamics.mdx) | Its fact-checking concept is better represented by the first-party CLI; the distinct part is third-party Flutter speech-to-text. | +| [FirstPrinciples](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/first-principle.mdx) | Its arbitrary two-provider split is superseded by native Agent API provider routing. | +| [FlameGuardAI](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/flameguardai.mdx) | Property fire-safety claims are risky, and the iterative research loop is now native Agent API behavior. | +| [Flow & Focus](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/flow-and-focus.mdx) | This is primarily a feed UI; fast-feed/deep-dive behavior overlaps the async deep-research guide. | +| [Monday](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/monday.mdx) | The immersive UI has no distinct API lesson, requires several external services, and its linked source repository is unavailable. | +| [MVP LifeLine](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/mvp-lifeline-ai-app.mdx) | Dual-persona prompting is a thin pattern, while the mental-health companion framing is risky. | +| [PerplexiCart](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/perplexicart.mdx) | Prompted product research plus JSON duplicates retained structured-output content and relies on fragile shopping claims. | +| [Perplexity Client](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/perplexity-client.mdx) | Its premise is a desktop GUI over Sonar model and parameter controls, so the product would need a wholesale redesign. | +| [Perplexity Lens](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/perplexity-lens.mdx) | The browser knowledge-graph UI is the product; the API usage is not a distinct reusable pattern. | +| [PosterLens](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/posterlens.mdx) | Medical interpretation and generated follow-up questions create risk; image analysis and academic search already survive as separate recipes. | +| [Sonar Chromium Browser](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/sonar-chromium-browser.mdx) | Maintaining a Chromium fork is disproportionate to the simple search and summarization API pattern. | +| [StarPlex](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/starplex.mdx) | Startup validation is prompt assembly over common research flows already covered by retained Agent API recipes. | +| [TruthTracer](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/truth-tracer.mdx) | It duplicates the first-party fact-checker and makes high-risk misinformation scoring claims. | +| [UnCovered](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/uncovered.mdx) | It duplicates fact-checking and browser-surface concepts without a distinct API capability. | +| [Valetudo AI](https://github.com/perplexityai/api-cookbook/blob/543c229320acf9204d47a3ff91f13349b57047be/docs/showcase/valetudo-ai.mdx) | Medical answer generation is a claim class that should not be restored as a showcase. | + +### Examples + +| Item | Why it should stay archived | +| --- | --- | +| [Disease Information App](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/examples/disease-qa) | Medical claims require a new safety review, and the old implementation exposes an API key in generated client-side HTML. | +| [Academic Research Finder](https://github.com/perplexityai/api-cookbook/tree/543c229320acf9204d47a3ff91f13349b57047be/docs/examples/research-finder) | The retained Academic and Scholarly Search guide supersedes it; fold useful CLI ergonomics into that guide instead. | + +## Audit coverage + +This ledger accounts for all 41 reviewed pages. + +| Type | Keep | Rebuild first | Rebuild later | Archive only | Total | +| --- | ---: | ---: | ---: | ---: | ---: | +| Community Showcase | 0 | 0 | 5 | 21 | 26 | +| Examples | 3 | 4 | 0 | 2 | 9 | +| Guides | 2 | 4 | 0 | 0 | 6 | +| **Unique pages** | **5** | **8** | **5** | **23** | **41** | + +## Consolidation rules + +- Keep one claim-verification recipe: rebuild `fact-checker-cli`; do not restore Fact Dynamics, TruthTracer, or UnCovered. +- Merge the memory hub, summary buffer, and persistent-chat pages into one guide. +- Let News Dedupe Digest own the digest concept; rebuild Daily News Briefing only for third-party-tool delivery. +- Keep finance recipes separated by job: per-ticker brief, VC memo, and topic-level news monitoring. +- Let Multi-provider Orchestration and Model Comparison own model routing and fallback. +- Fold Research Finder into Academic and Scholarly Search rather than restoring a duplicate page. +- Extend Image Analysis with schema validation instead of rebuilding Greenify if that covers the same lesson. + +## Republish checklist + +- Start from the current Agent API migration guide and public API contract; do not publish the archived Sonar implementation unchanged. +- Use current `responses.create` or `POST /v1/agent` conventions, typed output items, presets, and tools. +- Preserve the reusable prompts, schemas, integration shell, and assets from the historical source while rewriting API-specific code. +- Add `products: [agent-api]` and controlled `categories` frontmatter. +- Test all runnable code against the live Agent API and retain redacted proof before publication. +- Review dependency activity, licensing, privacy, and safety-sensitive claims before adapting community material into a maintained first-party recipe. diff --git a/docs/articles/academic-search/README.mdx b/docs/articles/academic-search/README.mdx new file mode 100644 index 0000000..f5a3636 --- /dev/null +++ b/docs/articles/academic-search/README.mdx @@ -0,0 +1,528 @@ +--- +title: Academic and Scholarly Search +description: Use the Agent API's domain filtering to restrict search to academic sources, extract DOIs and paper metadata, build citation chains, and create research summaries with proper attribution +sidebar_position: 7 +keywords: [academic-search, scholarly, doi, citations, research, papers, agent-api, domain-filter] +products: [agent-api] +categories: [structured-outputs, search-filtering] +--- + +This guide shows how to use the Agent API's `search_domain_filter` to restrict search results to academic and scholarly sources. You will learn how to extract paper metadata (DOIs, authors, publication dates), build citation chains across related papers, and produce properly attributed research summaries. + + +The `search_domain_filter` parameter on the Agent API's `web_search` tool controls which domains the search draws from. By filtering to academic domains like `arxiv.org`, `nature.com`, and `.edu`, you restrict results to peer-reviewed journals, preprint servers, and academic databases. For more on filtering, see the [Agent API Filters](/docs/agent-api/tools/web-search#filters) docs. + + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## Basic Academic Search + +Use `search_domain_filter` to restrict the Agent API's `web_search` tool to academic sources only. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +ACADEMIC_DOMAINS = [ + "arxiv.org", + "pubmed.ncbi.nlm.nih.gov", + "nature.com", + "science.org", + ".edu", + "scholar.google.com", + "semanticscholar.org", +] + +response = client.responses.create( + model="openai/gpt-5.4", + input="What are the latest findings on the relationship between gut microbiome and mental health?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": ACADEMIC_DOMAINS, + }, + }], + instructions="Focus on peer-reviewed academic sources. Cite papers with authors and publication years when possible.", +) + +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const ACADEMIC_DOMAINS = [ + "arxiv.org", + "pubmed.ncbi.nlm.nih.gov", + "nature.com", + "science.org", + ".edu", + "scholar.google.com", + "semanticscholar.org", +]; + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are the latest findings on the relationship between gut microbiome and mental health?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: ACADEMIC_DOMAINS, + }, + }], + instructions: "Focus on peer-reviewed academic sources. Cite papers with authors and publication years when possible.", +}); + +console.log(response.output_text); +``` + +```bash curl +curl "https://api.perplexity.ai/v1/agent" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "openai/gpt-5.4", + "input": "What are the latest findings on the relationship between gut microbiome and mental health?", + "tools": [{"type": "web_search", "filters": {"search_domain_filter": ["arxiv.org", "pubmed.ncbi.nlm.nih.gov", "nature.com", "science.org", ".edu"]}}], + "instructions": "Focus on peer-reviewed academic sources. Cite papers with authors and publication years when possible." + }' +``` + + + +Academic domain filtering targets papers from PubMed, arXiv, Google Scholar, Semantic Scholar, and major journal publishers. Combine `search_domain_filter` with clear `instructions` to ensure the model focuses on peer-reviewed or pre-print academic content. + + +## Extracting Paper Metadata + +Use structured outputs to extract detailed paper metadata from academic search results. + + +```python Python +import json +from perplexity import Perplexity + +client = Perplexity() + +# Use Agent API with web_search for structured extraction +response = client.responses.create( + model="openai/gpt-5.4", + input="Find the 5 most cited recent papers on transformer architectures in computer vision (Vision Transformers).", + tools=[{"type": "web_search"}], + instructions=( + "Search for academic papers only. For each paper, extract the title, authors, " + "publication year, journal or venue, DOI if available, and a one-sentence summary of the key contribution." + ), + response_format={ + "type": "json_schema", + "json_schema": { + "name": "academic_papers", + "schema": { + "type": "object", + "properties": { + "query": {"type": "string"}, + "papers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "authors": {"type": "string"}, + "year": {"type": "integer"}, + "venue": {"type": "string"}, + "doi": {"type": "string"}, + "key_contribution": {"type": "string"}, + }, + "required": ["title", "authors", "year", "venue", "doi", "key_contribution"], + "additionalProperties": false, + }, + }, + }, + "required": ["query", "papers"], + "additionalProperties": false, + }, + }, + }, +) + +data = json.loads(response.output_text) +print(f"Query: {data['query']}\n") + +for paper in data["papers"]: + print(f" {paper['title']}") + print(f" Authors: {paper['authors']}") + print(f" Venue: {paper['venue']} ({paper['year']})") + if paper["doi"]: + print(f" DOI: {paper['doi']}") + print(f" Contribution: {paper['key_contribution']}") + print() +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "Find the 5 most cited recent papers on transformer architectures in computer vision (Vision Transformers).", + tools: [{ type: "web_search" }], + instructions: "Search for academic papers only. For each paper, extract the title, authors, publication year, journal or venue, DOI if available, and a one-sentence summary of the key contribution.", + response_format: { + type: "json_schema", + json_schema: { + name: "academic_papers", + schema: { + type: "object", + properties: { + query: { type: "string" }, + papers: { + type: "array", + items: { + type: "object", + properties: { + title: { type: "string" }, + authors: { type: "string" }, + year: { type: "integer" }, + venue: { type: "string" }, + doi: { type: "string" }, + key_contribution: { type: "string" }, + }, + required: ["title", "authors", "year", "venue", "doi", "key_contribution"], + }, + }, + }, + required: ["query", "papers"], + }, + }, + }, +}); + +const data = JSON.parse(response.output_text); +console.log(`Query: ${data.query}\n`); + +for (const paper of data.papers) { + console.log(` ${paper.title}`); + console.log(` Authors: ${paper.authors}`); + console.log(` Venue: ${paper.venue} (${paper.year})`); + if (paper.doi) console.log(` DOI: ${paper.doi}`); + console.log(` Contribution: ${paper.key_contribution}`); + console.log(); +} +``` + + +## Building Citation Chains + +Trace how papers cite each other to understand the evolution of an idea across the literature. + + +```python Python +import json +from perplexity import Perplexity + +client = Perplexity() + + +def find_citing_papers(paper_title: str, depth: int = 0, max_depth: int = 2) -> dict: + """Recursively find papers that cite a given paper.""" + indent = " " * depth + print(f"{indent}Searching citations for: {paper_title}...") + + response = client.responses.create( + model="openai/gpt-5.4", + input=f"What are the 3 most important papers that directly cite or build upon '{paper_title}'?", + tools=[{"type": "web_search"}], + instructions="Focus on academic papers only. Return papers that explicitly reference or extend the given work.", + response_format={ + "type": "json_schema", + "json_schema": { + "name": "citing_papers", + "schema": { + "type": "object", + "properties": { + "source_paper": {"type": "string"}, + "citing_papers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "authors": {"type": "string"}, + "year": {"type": "integer"}, + "relationship": {"type": "string"}, + }, + "required": ["title", "authors", "year", "relationship"], + "additionalProperties": false, + }, + }, + }, + "required": ["source_paper", "citing_papers"], + "additionalProperties": false, + }, + }, + }, + ) + + data = json.loads(response.output_text) + result = { + "paper": paper_title, + "cited_by": [], + } + + for citing in data["citing_papers"]: + entry = { + "title": citing["title"], + "authors": citing["authors"], + "year": citing["year"], + "relationship": citing["relationship"], + } + + # Recurse for deeper citation chains + if depth < max_depth: + entry["cited_by"] = find_citing_papers(citing["title"], depth + 1, max_depth).get("cited_by", []) + + result["cited_by"].append(entry) + + return result + + +# Start with a foundational paper +chain = find_citing_papers("Attention Is All You Need", max_depth=1) +print(json.dumps(chain, indent=2)) +``` + + + +Citation chain depth grows exponentially. Keep `max_depth` low (1-2) to avoid excessive API calls. For comprehensive citation graphs, use dedicated tools like Semantic Scholar's API alongside Perplexity for summaries. + + +## Research Summary with Attribution + +Generate a research summary that properly attributes each claim to its source paper. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +ACADEMIC_DOMAINS = [ + "arxiv.org", "pubmed.ncbi.nlm.nih.gov", "nature.com", + "science.org", ".edu", "scholar.google.com", +] + + +def academic_research_summary(topic: str) -> str: + """Generate an academic research summary with proper citations.""" + response = client.responses.create( + model="openai/gpt-5.4", + input=( + f"Provide a comprehensive academic literature review on: {topic}. " + "Include specific findings, methodologies, and conclusions from recent papers. " + "Cite each claim with its source." + ), + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": ACADEMIC_DOMAINS, + }, + }], + instructions=( + "Search for peer-reviewed academic sources only. For each claim, " + "attribute it to the specific paper with author names and year. " + "Format the output as a structured literature review with a references section." + ), + ) + + return f"# Literature Review: {topic}\n\n{response.output_text}" + + +report = academic_research_summary( + "the effectiveness of large language models for automated code review" +) +print(report) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const ACADEMIC_DOMAINS = [ + "arxiv.org", "pubmed.ncbi.nlm.nih.gov", "nature.com", + "science.org", ".edu", "scholar.google.com", +]; + +async function academicResearchSummary(topic: string): Promise { + const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: `Provide a comprehensive academic literature review on: ${topic}. Include specific findings, methodologies, and conclusions from recent papers. Cite each claim with its source.`, + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: ACADEMIC_DOMAINS, + }, + }], + instructions: "Search for peer-reviewed academic sources only. For each claim, attribute it to the specific paper with author names and year. Format the output as a structured literature review with a references section.", + }); + + return `# Literature Review: ${topic}\n\n${response.output_text}`; +} + +const report = await academicResearchSummary( + "the effectiveness of large language models for automated code review" +); +console.log(report); +``` + + +## Multi-Field Academic Search + +Use field-specific domain filters to search across different academic disciplines. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +ACADEMIC_DOMAINS = { + "biomedical": ["pubmed.ncbi.nlm.nih.gov", "nih.gov", "thelancet.com", "nejm.org"], + "computer_science": ["arxiv.org", "dl.acm.org", "ieee.org", "openreview.net"], + "social_science": ["jstor.org", "ssrn.com", "journals.sagepub.com"], +} + + +def field_specific_search(query: str, field: str) -> dict: + """Search academic literature within a specific field.""" + domains = ACADEMIC_DOMAINS.get(field, []) + + response = client.responses.create( + model="openai/gpt-5.4", + input=query, + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": domains, + }, + }] if domains else [{"type": "web_search"}], + instructions=f"Search for peer-reviewed academic sources in the {field.replace('_', ' ')} field. Cite papers with authors and years.", + ) + + return { + "field": field, + "content": response.output_text, + } + + +# Search across multiple fields +query = "What are the ethical implications of AI-generated content?" +fields = ["computer_science", "social_science"] + +for field in fields: + result = field_specific_search(query, field) + print(f"\n{'='*60}") + print(f"Field: {result['field']}") + print(f"{'='*60}") + print(result["content"][:500]) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const ACADEMIC_DOMAINS: Record = { + biomedical: ["pubmed.ncbi.nlm.nih.gov", "nih.gov", "thelancet.com", "nejm.org"], + computer_science: ["arxiv.org", "dl.acm.org", "ieee.org", "openreview.net"], + social_science: ["jstor.org", "ssrn.com", "journals.sagepub.com"], +}; + +async function fieldSpecificSearch(query: string, field: string) { + const domains = ACADEMIC_DOMAINS[field] ?? []; + + const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: query, + tools: domains.length > 0 + ? [{ type: "web_search" as const, filters: { search_domain_filter: domains } }] + : [{ type: "web_search" as const }], + instructions: `Search for peer-reviewed academic sources in the ${field.replace("_", " ")} field. Cite papers with authors and years.`, + }); + + return { + field, + content: response.output_text, + }; +} + +const query = "What are the ethical implications of AI-generated content?"; +const fields = ["computer_science", "social_science"]; + +for (const field of fields) { + const result = await fieldSpecificSearch(query, field); + console.log(`\n${"=".repeat(60)}`); + console.log(`Field: ${result.field}`); + console.log("=".repeat(60)); + console.log(result.content.slice(0, 500)); +} +``` + + +## Tips and Best Practices + +1. **Use `search_domain_filter` with academic domains** to restrict results to peer-reviewed sources. Target domains like `arxiv.org`, `nature.com`, `pubmed.ncbi.nlm.nih.gov`, and `.edu`. + +2. **Use `instructions` to guide academic focus.** Tell the model to prioritize peer-reviewed papers, cite authors and years, and focus on specific fields. + +3. **Use field-specific domain lists** to narrow results to specific publishers or databases (e.g., PubMed for biomedical, arXiv for CS). + +4. **Use structured outputs** for metadata extraction. JSON schemas ensure consistent paper metadata across queries. + +5. **Request specific details in your prompt.** Ask for "authors, year, journal, and key findings" to get more complete metadata in the response. + +6. **Combine `search_domain_filter` with `search_recency_filter`** for time-sensitive research. Use `"week"`, `"month"`, or `"year"` to find recent publications. + +## Next Steps + + + +Full reference for domain, recency, and location filters on the Agent API. + + + +Extract typed JSON for paper metadata and research findings. + + + +Control which domains the search includes or excludes. + + diff --git a/docs/articles/agent-skills/README.mdx b/docs/articles/agent-skills/README.mdx new file mode 100644 index 0000000..eae1e83 --- /dev/null +++ b/docs/articles/agent-skills/README.mdx @@ -0,0 +1,304 @@ +--- +title: Daily AI Stock News PDF with Skills +description: Generate a daily one-page AI-industry stock news PDF with the Agent API using the built-in office skill and an inline design skill. +sidebar_position: 20 +keywords: [skills, office, pdf, inline-skills, design-system, agent-api, daily-report] +products: [agent-api] +categories: [sandbox, orchestration] +--- + +Build a repeatable end-of-day report for the AI-industry stocks you track. Each run produces a one-page PDF with today's prices, the day's moves, and the news that moved them — all rendered in your own house style. + +The recipe uses two skills: + +- **Built-in `office`** — the umbrella that lets the model pick the right document format (PDF, in this case). +- **Inline `design-system`** — a request-scoped skill that carries your colors, fonts, and layout rules. + +See [Skills](/docs/agent-api/skills) for the full request schema and runtime behavior. + +## Prerequisites + +Install one SDK: + +- Python: `pip install perplexityai` +- TypeScript: `npm install @perplexity-ai/perplexity_ai` + +If you do not have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Export your API key: + +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## Define the design skill + +The inline `design-system` skill tells the model how the report should look. Its `description` is the routing trigger — one line telling the model when to load it. The `instructions` carry the full design book. + +Keep the design book short and directive. The model reads it once, per request, and applies it while generating the document. + +```text +Load when creating documents that must follow the house design book. + +Model: a 1970s letterpress broadsheet financial page. One ink, gray paper. + +Colors +- Paper #EDE9DE; tinted boxes and alternating table rows #E3DFD2. +- Body ink #232220 — soft, never hard black (ink spread on newsprint). +- Headlines and rules may deepen to #141311; faded ink #5C5850 for captions and secondary text. +- No second color anywhere. Up moves: bold with a ▲. Down moves: parentheses with a ▼. + +Typography +- Body: low-contrast newspaper serif (Georgia, PT Serif, or Times), 9-10pt, justified and hyphenated. +- Headlines: bold condensed serif with a smaller deck beneath. +- Kickers and table headers: condensed grotesque caps (Franklin Gothic or Oswald), letterspaced. +- Tables: agate style — 7-8pt condensed, tabular figures. + +Layout +- One page, ~18mm margins. +- Nameplate in blackletter or heavy serif, with a folio line (date, edition, price) set between an Oxford rule (thick over hairline). +- Ticker summary as a boxed agate strip below the nameplate. +- News timeline in 3-4 narrow justified columns divided by hairline column rules; each item opens with a bold caps dateline ('LONDON, JULY 17 —'). +- Data table ruled with hairlines only. +- Pack the page — separate blocks with cutoff rules, not white space. + +Imagery +- Grayscale halftone only, with a hairline keyline and an italic caption. + +Avoid +- Second colors, gradients, shadows, rounded corners, sans-serif body text, and generous white space. +``` + +## Generate today's report + +Combine the `office` umbrella with the inline `design-system` skill. The prompt names the tickers and asks for today's move plus dated news tagged to the ticker each item moved. + +Skills run on the durable backend, so submit with `background: true` and poll `GET /v1/agent/{id}` until the status is terminal. + + +```python Python +import time +from datetime import date +from perplexity import Perplexity + +client = Perplexity() + +design_book = """ +Load when creating documents that must follow the house design book. + +Model: a 1970s letterpress broadsheet financial page. One ink, gray paper. + +Colors +- Paper #EDE9DE; tinted boxes and alternating table rows #E3DFD2. +- Body ink #232220 — soft, never hard black (ink spread on newsprint). +- Headlines and rules may deepen to #141311; faded ink #5C5850 for captions and secondary text. +- No second color anywhere. Up moves: bold with a ▲. Down moves: parentheses with a ▼. + +Typography +- Body: low-contrast newspaper serif (Georgia, PT Serif, or Times), 9-10pt, justified and hyphenated. +- Headlines: bold condensed serif with a smaller deck beneath. +- Kickers and table headers: condensed grotesque caps (Franklin Gothic or Oswald), letterspaced. +- Tables: agate style — 7-8pt condensed, tabular figures. + +Layout +- One page, ~18mm margins. +- Nameplate in blackletter or heavy serif, with a folio line (date, edition, price) set between an Oxford rule (thick over hairline). +- Ticker summary as a boxed agate strip below the nameplate. +- News timeline in 3-4 narrow justified columns divided by hairline column rules; each item opens with a bold caps dateline ('LONDON, JULY 17 —'). +- Data table ruled with hairlines only. +- Pack the page — separate blocks with cutoff rules, not white space. + +Imagery +- Grayscale halftone only, with a hairline keyline and an italic caption. + +Avoid +- Second colors, gradients, shadows, rounded corners, sans-serif body text, and generous white space. +""" + +response = client.responses.create( + preset="xhigh", + background=True, + skills=[ + { + "type": "inline", + "name": "design-system", + "description": "Load when creating documents that must follow the house design book.", + "instructions": design_book, + }, + {"type": "builtin", "name": "office"}, + ], + input=( + f"Create a one-page PDF titled 'AI Stocks Daily' for {date.today():%Y-%m-%d}. " + "Cover NVDA, MSFT, GOOGL, AMD, AVGO, META, and TSM. For each ticker " + "include latest price and today's % change. Add a dated timeline of " + "today's key AI news, tag each item to the ticker it moved, and " + "finish with a data table (ticker, price, day change, week change). " + "Follow the design-system skill." + ), +) + +while response.status not in ("completed", "failed", "cancelled", "incomplete"): + time.sleep(2) + response = client.responses.retrieve(response.id) + +print(f"Final status: {response.status}") + +if response.status == "completed": + files = client.responses.files.list(response.id) + for file in files.data: + content = client.responses.files.content( + file_id=file.id, + response_id=response.id, + ) + content.write_to_file(file.filename) + print(f"Downloaded {file.filename} ({file.bytes} bytes)") +``` + +```typescript Typescript +import Perplexity from '@perplexity-ai/perplexity_ai'; +import { writeFile } from 'node:fs/promises'; + +const client = new Perplexity(); + +const designBook = ` +Load when creating documents that must follow the house design book. + +Model: a 1970s letterpress broadsheet financial page. One ink, gray paper. + +Colors +- Paper #EDE9DE; tinted boxes and alternating table rows #E3DFD2. +- Body ink #232220 — soft, never hard black (ink spread on newsprint). +- Headlines and rules may deepen to #141311; faded ink #5C5850 for captions and secondary text. +- No second color anywhere. Up moves: bold with a ▲. Down moves: parentheses with a ▼. + +Typography +- Body: low-contrast newspaper serif (Georgia, PT Serif, or Times), 9-10pt, justified and hyphenated. +- Headlines: bold condensed serif with a smaller deck beneath. +- Kickers and table headers: condensed grotesque caps (Franklin Gothic or Oswald), letterspaced. +- Tables: agate style — 7-8pt condensed, tabular figures. + +Layout +- One page, ~18mm margins. +- Nameplate in blackletter or heavy serif, with a folio line (date, edition, price) set between an Oxford rule (thick over hairline). +- Ticker summary as a boxed agate strip below the nameplate. +- News timeline in 3-4 narrow justified columns divided by hairline column rules; each item opens with a bold caps dateline ('LONDON, JULY 17 —'). +- Data table ruled with hairlines only. +- Pack the page — separate blocks with cutoff rules, not white space. + +Imagery +- Grayscale halftone only, with a hairline keyline and an italic caption. + +Avoid +- Second colors, gradients, shadows, rounded corners, sans-serif body text, and generous white space. +`; + +const today = new Date().toISOString().slice(0, 10); + +let response = await client.responses.create({ + preset: 'xhigh', + background: true, + skills: [ + { + type: 'inline', + name: 'design-system', + description: 'Load when creating documents that must follow the house design book.', + instructions: designBook, + }, + { type: 'builtin', name: 'office' }, + ], + input: + `Create a one-page PDF titled 'AI Stocks Daily' for ${today}. ` + + 'Cover NVDA, MSFT, GOOGL, AMD, AVGO, META, and TSM. For each ticker ' + + "include latest price and today's % change. Add a dated timeline of " + + "today's key AI news, tag each item to the ticker it moved, and " + + 'finish with a data table (ticker, price, day change, week change). ' + + 'Follow the design-system skill.', +}); + +while (!['completed', 'failed', 'cancelled', 'incomplete'].includes(response.status)) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + response = await client.responses.retrieve(response.id); +} + +console.log(`Final status: ${response.status}`); + +if (response.status === 'completed') { + const files = await client.responses.files.list(response.id); + for (const file of files.data) { + const content = await client.responses.files.content(file.id, { + response_id: response.id, + }); + await writeFile(file.filename, Buffer.from(await content.arrayBuffer())); + console.log(`Downloaded ${file.filename} (${file.bytes} bytes)`); + } +} +``` + +```bash cURL +TODAY=$(date +%F) + +RESPONSE_ID=$(curl -s https://api.perplexity.ai/v1/agent \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"preset\": \"xhigh\", + \"background\": true, + \"skills\": [ + { + \"type\": \"inline\", + \"name\": \"design-system\", + \"description\": \"Load when creating documents that must follow the house design book.\", + \"instructions\": \"Model: a 1970s letterpress broadsheet financial page. One ink, gray paper.\nColors: paper #EDE9DE (tinted boxes and alternating table rows #E3DFD2); body ink #232220 (soft, never hard black); headlines and rules may deepen to #141311; faded ink #5C5850 for captions. No second color. Up moves: bold + ▲; down moves: parentheses + ▼.\nTypography: body low-contrast newspaper serif (Georgia, PT Serif, or Times), 9-10pt, justified and hyphenated; headlines bold condensed serif with a smaller deck; kickers and table headers condensed grotesque caps (Franklin Gothic or Oswald), letterspaced; tables agate 7-8pt condensed with tabular figures.\nLayout: one page, ~18mm margins; blackletter or heavy-serif nameplate with a folio line (date, edition, price) between an Oxford rule (thick over hairline); boxed agate ticker strip below; news timeline in 3-4 narrow justified columns with hairline column rules, items opening with bold caps datelines ('LONDON, JULY 17 —'); data table ruled with hairlines only. Pack the page — cutoff rules, not padding.\nImagery: grayscale halftone with hairline keyline and italic caption.\nAvoid: second colors, gradients, shadows, rounded corners, sans body text, generous white space.\" + }, + { \"type\": \"builtin\", \"name\": \"office\" } + ], + \"input\": \"Create a one-page PDF titled 'AI Stocks Daily' for $TODAY. Cover NVDA, MSFT, GOOGL, AMD, AVGO, META, and TSM. For each ticker include latest price and today's % change. Add a dated timeline of today's key AI news, tag each item to the ticker it moved, and finish with a data table (ticker, price, day change, week change). Follow the design-system skill.\" + }" | jq -r '.id') + +while true; do + STATUS=$(curl -s "https://api.perplexity.ai/v1/agent/$RESPONSE_ID" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" | jq -r '.status') + echo "Status: $STATUS" + [[ "$STATUS" == "completed" || "$STATUS" == "failed" || "$STATUS" == "cancelled" || "$STATUS" == "incomplete" ]] && break + sleep 2 +done + +FILE_ID=$(curl -s "https://api.perplexity.ai/v1/agent/$RESPONSE_ID/files" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" | jq -r '.data[0].id') + +curl -s "https://api.perplexity.ai/v1/agent/$RESPONSE_ID/files/$FILE_ID/content" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" \ + -o "ai-stocks-daily-$TODAY.pdf" +``` + + +## What the response contains + +The completed response's `output` includes: + +- One `skill_loaded` item for `design-system` and one for the built-in `office/pdf` leaf the model loaded from the umbrella. +- A `share_file` item pointing to the generated PDF. Download it with the response files endpoints, as shown in [Working with files](/docs/agent-api/working-with-files). + +## Run it every trading day + +Turn the snippet into a scheduled job (cron, Airflow, GitHub Actions) that runs after the US market closes. The design skill lives in your code, so the report stays visually consistent every day while the content updates itself. + +## Next steps + + + +Full reference for the skills field, the built-in catalog, and inline skills. + + + +List and download files an Agent API response produced in the sandbox. + + + +Submit, poll, stream, and cancel long-running agent runs. + + diff --git a/docs/articles/async-deep-research/README.mdx b/docs/articles/async-deep-research/README.mdx new file mode 100644 index 0000000..27e325c --- /dev/null +++ b/docs/articles/async-deep-research/README.mdx @@ -0,0 +1,390 @@ +--- +title: Deep Research Workflows +description: Use the Agent API medium preset for comprehensive, multi-step research tasks — synchronous usage, batch concurrency, result processing, and production patterns +sidebar_position: 8 +keywords: [medium, agent-api, preset, long-running, research, multi-step] +products: [agent-api] +categories: [deep-research, search-filtering] +--- + +This guide shows how to use the Agent API's `medium` preset for comprehensive, multi-step research tasks. Deep research performs extended web research, following chains of sources and synthesizing detailed answers. You will learn how to run deep research queries, process results, handle long-running requests, and run batch research workflows. + + +The `medium` preset on the Agent API performs multi-step web research, following chains of sources and synthesizing comprehensive answers. It automatically selects the best model and configures tools for deep research. For more on presets, see the [Agent API Presets](/docs/agent-api/presets) docs. + + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## Basic Deep Research + +Use the `medium` preset for comprehensive research queries. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +response = client.responses.create( + preset="medium", + input=( + "Provide a comprehensive analysis of the current state of nuclear fusion research. " + "Cover the main approaches (tokamak, stellarator, inertial confinement, laser-driven), " + "key milestones achieved in the past 2 years, major private companies involved, " + "and realistic timelines for commercial fusion power." + ), +) + +print(f"Model: {response.model}") +print(f"\n{response.output_text}") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + preset: "medium", + input: "Provide a comprehensive analysis of the current state of nuclear fusion research. Cover the main approaches (tokamak, stellarator, inertial confinement, laser-driven), key milestones achieved in the past 2 years, major private companies involved, and realistic timelines for commercial fusion power.", +}); + +console.log(`Model: ${response.model}`); +console.log(`\n${response.output_text}`); +``` + +```bash curl +curl "https://api.perplexity.ai/v1/agent" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "preset": "medium", + "input": "Provide a comprehensive analysis of the current state of nuclear fusion research." + }' +``` + + + +The `medium` preset automatically selects the best model and configures tools for multi-step research. You don't need to specify a model or tools when using presets. + + +## Processing Deep Research Results + +Extract and format the key parts of a deep research response. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + + +def deep_research(query: str) -> dict: + """Run a deep research query and extract structured results.""" + print(f"Researching: {query[:80]}...") + + response = client.responses.create( + preset="medium", + input=query, + ) + + content = response.output_text + usage = response.usage + + return { + "content": content, + "model": response.model, + "tokens": { + "input": usage.input_tokens if usage else 0, + "output": usage.output_tokens if usage else 0, + }, + "word_count": len(content.split()), + } + + +if __name__ == "__main__": + output = deep_research( + "What is the current state of solid-state battery technology? " + "Cover the leading companies, technical challenges remaining, " + "and expected timeline for mass production in EVs." + ) + print(f"\nModel: {output['model']}") + print(f"Words: {output['word_count']}") + print(f"Tokens: {output['tokens']['input']} in, {output['tokens']['output']} out") + print(f"\n{'='*60}\n") + print(output["content"][:2000]) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +async function deepResearch(query: string) { + console.log(`Researching: ${query.slice(0, 80)}...`); + + const response = await client.responses.create({ + preset: "medium", + input: query, + }); + + const content = response.output_text; + const usage = response.usage; + + return { + content, + model: response.model, + tokens: { + input: usage?.input_tokens ?? 0, + output: usage?.output_tokens ?? 0, + }, + wordCount: content.split(/\s+/).length, + }; +} + +const output = await deepResearch( + "What is the current state of solid-state battery technology? Cover the leading companies, technical challenges remaining, and expected timeline for mass production in EVs." +); + +console.log(`\nModel: ${output.model}`); +console.log(`Words: ${output.wordCount}`); +console.log(`Tokens: ${output.tokens.input} in, ${output.tokens.output} out`); +console.log(`\n${"=".repeat(60)}\n`); +console.log(output.content.slice(0, 2000)); +``` + + +## Deep Research with Domain Filtering + +Combine deep research with domain filters for focused, authoritative research. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Deep research restricted to government and academic sources +response = client.responses.create( + model="openai/gpt-5.2", + input=( + "Analyze the current regulatory landscape for AI in healthcare. " + "Cover FDA guidance, EU AI Act implications, and recent enforcement actions." + ), + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": [".gov", ".europa.eu", "who.int", "nature.com", ".edu"], + }, + }], + instructions=( + "Conduct thorough research using only government and academic sources. " + "Provide specific regulatory references, dates, and policy details." + ), +) + +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.2", + input: "Analyze the current regulatory landscape for AI in healthcare. Cover FDA guidance, EU AI Act implications, and recent enforcement actions.", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: [".gov", ".europa.eu", "who.int", "nature.com", ".edu"], + }, + }], + instructions: "Conduct thorough research using only government and academic sources. Provide specific regulatory references, dates, and policy details.", +}); + +console.log(response.output_text); +``` + + +## Batch Research with Concurrency + +Run multiple deep research queries concurrently using asyncio and the Perplexity SDK. + + +```python Python +import asyncio +import time +from perplexity import AsyncPerplexity + + +async def single_research(client: AsyncPerplexity, query: str) -> dict: + """Run a single deep research query.""" + start = time.time() + try: + response = await client.responses.create( + preset="medium", + input=query, + ) + return { + "query": query, + "content": response.output_text, + "model": response.model, + "elapsed": time.time() - start, + } + except Exception as e: + return {"query": query, "error": str(e), "elapsed": time.time() - start} + + +async def batch_research(queries: list[str], max_concurrent: int = 3) -> list[dict]: + """Run multiple deep research queries with concurrency limits.""" + semaphore = asyncio.Semaphore(max_concurrent) + + async def limited_research(client, query): + async with semaphore: + return await single_research(client, query) + + async with AsyncPerplexity() as client: + tasks = [limited_research(client, q) for q in queries] + return await asyncio.gather(*tasks) + + +if __name__ == "__main__": + queries = [ + "What are the latest advances in room-temperature superconductors?", + "What is the current state of quantum error correction?", + "What are the most promising approaches to carbon capture and storage?", + ] + + print(f"Starting batch research: {len(queries)} queries\n") + results = asyncio.run(batch_research(queries, max_concurrent=3)) + + for r in results: + status = "OK" if "content" in r else f"FAILED ({r.get('error')})" + word_count = len(r.get("content", "").split()) if "content" in r else 0 + print(f" [{r['elapsed']:.0f}s] {r['query'][:60]}... → {status} ({word_count} words)") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +interface ResearchResult { + query: string; + content?: string; + model?: string; + elapsed: number; + error?: string; +} + +const client = new Perplexity(); + +async function singleResearch(query: string): Promise { + const start = Date.now(); + try { + const response = await client.responses.create({ + preset: 'medium', + input: query, + }); + return { + query, + content: response.output_text, + model: response.model, + elapsed: (Date.now() - start) / 1000, + }; + } catch (e) { + return { query, error: String(e), elapsed: (Date.now() - start) / 1000 }; + } +} + +async function batchResearch(queries: string[], maxConcurrent = 3) { + const results: ResearchResult[] = []; + const queue = [...queries]; + async function worker() { + while (queue.length) { + const q = queue.shift()!; + results.push(await singleResearch(q)); + } + } + await Promise.all( + Array.from({ length: maxConcurrent }, () => worker()) + ); + return results; +} + +const queries = [ + 'What are the latest advances in room-temperature superconductors?', + 'What is the current state of quantum error correction?', + 'What are the most promising approaches to carbon capture and storage?', +]; + +console.log(`Starting batch research: ${queries.length} queries\n`); +const results = await batchResearch(queries, 3); + +for (const r of results) { + const status = r.content ? 'OK' : `FAILED (${r.error})`; + const words = r.content ? r.content.split(/\s+/).length : 0; + console.log(` [${r.elapsed.toFixed(0)}s] ${r.query.slice(0, 60)}... → ${status} (${words} words)`); +} +``` + + + +Deep research queries consume significant compute resources. Keep concurrent requests to 3-5 to stay within rate limits and avoid throttling. Check your [rate limits](/docs/admin/rate-limits-usage-tiers) for specific thresholds. + + +## Tips and Best Practices + +1. **Use the `medium` preset** for the simplest integration. It automatically selects the best model and configures tools. + +2. **Combine with domain filters** when you need authoritative sources. Use `search_domain_filter` to restrict to specific domains. + +3. **Use `instructions`** to guide the depth and focus of research. Be specific about what aspects to cover. + +4. **Limit concurrency.** Running too many deep research queries simultaneously may trigger rate limits. Use a semaphore to cap concurrent requests to 3-5. + +5. **Use the async client for batch workflows.** `AsyncPerplexity` enables concurrent requests without blocking. + +6. **Set `max_output_tokens`** for cost control when you need shorter summaries rather than exhaustive reports. + +## Next Steps + + + +Full reference for available presets including medium. + + + +Get started with the Agent API for multi-provider access and tools. + + + +Control which domains the search includes or excludes. + + + +Understand rate limits for research and batch workflows. + + diff --git a/docs/articles/embeddings-rag/README.mdx b/docs/articles/embeddings-rag/README.mdx new file mode 100644 index 0000000..793da5e --- /dev/null +++ b/docs/articles/embeddings-rag/README.mdx @@ -0,0 +1,924 @@ +--- +title: RAG with Perplexity Embeddings +description: Build an end-to-end retrieval-augmented generation pipeline using Perplexity's standard and contextualized embedding models. +sidebar_position: 3 +keywords: [rag, embeddings, retrieval, vector-search, contextualized, chunking, matryoshka] +products: [agent-api, embeddings-api] +categories: [rag] +--- + +This guide walks through building a complete retrieval-augmented generation (RAG) pipeline using Perplexity's Embeddings API and Agent API. + +It covers document chunking, embedding with both standard and contextualized models, building an in-memory vector index, querying for relevant context, and generating grounded answers. + + +This guide focuses on the end-to-end pipeline. For API reference details on individual embedding types, see [Standard Embeddings](/docs/embeddings/standard-embeddings) and [Contextualized Embeddings](/docs/embeddings/contextualized-embeddings). + + +## Pipeline Overview + +A RAG pipeline retrieves relevant information from your own documents before generating an answer, grounding model responses in your data rather than relying solely on parametric knowledge. + +RAG Pipeline Diagram +RAG Pipeline Diagram + +The steps are: + +1. **Chunk** your source documents into manageable pieces with overlap. +2. **Embed** each chunk using a Perplexity embedding model. +3. **Index** the embeddings for similarity search. +4. **Query** by embedding the user question with the same model. +5. **Retrieve** the top-k most similar chunks. +6. **Generate** an answer by passing the retrieved context to the Agent API. + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## Document Chunking + +Split your documents into chunks small enough for the model's context window while preserving semantic coherence. Overlapping chunks ensure that information at chunk boundaries is not lost. + + +```python Python +def chunk_text(text: str, chunk_size: int = 500, overlap: int = 100) -> list[str]: + """Split text into overlapping chunks by character count.""" + chunks = [] + start = 0 + while start < len(text): + end = start + chunk_size + chunk = text[start:end].strip() + if chunk: + chunks.append(chunk) + start += chunk_size - overlap + return chunks + +document = """Retrieval-augmented generation (RAG) is a technique that combines +information retrieval with text generation. Rather than relying solely on a +language model's training data, RAG systems first search a knowledge base for +relevant documents, then use those documents as context when generating a +response. This reduces hallucinations and allows the system to provide answers +grounded in specific, up-to-date sources.""" + +chunks = chunk_text(document, chunk_size=300, overlap=50) +for i, chunk in enumerate(chunks): + print(f"Chunk {i} ({len(chunk)} chars): {chunk[:60]}...") +``` + +```typescript TypeScript +function chunkText(text: string, chunkSize: number = 500, overlap: number = 100): string[] { + const chunks: string[] = []; + let start = 0; + while (start < text.length) { + const end = start + chunkSize; + const chunk = text.slice(start, end).trim(); + if (chunk) chunks.push(chunk); + start += chunkSize - overlap; + } + return chunks; +} + +const document = `Retrieval-augmented generation (RAG) is a technique that combines +information retrieval with text generation. Rather than relying solely on a +language model's training data, RAG systems first search a knowledge base for +relevant documents, then use those documents as context when generating a +response. This reduces hallucinations and allows the system to provide answers +grounded in specific, up-to-date sources.`; + +const chunks = chunkText(document, 300, 50); +chunks.forEach((chunk, i) => { + console.log(`Chunk ${i} (${chunk.length} chars): ${chunk.slice(0, 60)}...`); +}); +``` + + + +A chunk size of 300-500 characters with 50-100 characters of overlap works well for most use cases. For structured documents (markdown, HTML), consider splitting on headings or paragraph boundaries instead of raw character counts. + + +## Embedding with the Standard Model + +Standard embeddings treat each text independently. Use them when chunks are self-contained and don't rely on surrounding context. + + +```python Python +import base64 +import numpy as np +from perplexity import Perplexity + +client = Perplexity() + +def decode_embedding(b64_string: str) -> np.ndarray: + """Decode a base64-encoded int8 embedding to a float32 numpy array.""" + return np.frombuffer(base64.b64decode(b64_string), dtype=np.int8).astype(np.float32) + +chunks = [ + "RAG combines retrieval with generation to ground responses in real data.", + "Document chunking splits text into overlapping segments for embedding.", + "Cosine similarity measures the angle between two embedding vectors.", +] + +response = client.embeddings.create(input=chunks, model="pplx-embed-v1-4b") +embeddings = [decode_embedding(emb.embedding) for emb in response.data] +print(f"Embedded {len(embeddings)} chunks, each with {len(embeddings[0])} dimensions") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +function decodeEmbedding(b64String: string): Int8Array { + const buffer = Buffer.from(b64String, 'base64'); + return new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +const chunks = [ + "RAG combines retrieval with generation to ground responses in real data.", + "Document chunking splits text into overlapping segments for embedding.", + "Cosine similarity measures the angle between two embedding vectors.", +]; + +const response = await client.embeddings.create({ + input: chunks, + model: "pplx-embed-v1-4b" +}); +const embeddings = response.data.map(emb => decodeEmbedding(emb.embedding)); +console.log(`Embedded ${embeddings.length} chunks, each with ${embeddings[0].length} dimensions`); +``` + + +## Embedding with the Contextualized Model + +Contextualized embeddings understand that chunks belong to the same document. The model uses cross-chunk attention so that each chunk's embedding incorporates information from its neighbors. The key API difference is the nested array structure: each inner array contains chunks from a single document. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Two source documents, each split into chunks +doc1_chunks = [ + "RAG combines retrieval with generation to produce grounded answers.", + "The retrieval step searches a vector index for chunks similar to the query.", + "The generation step uses retrieved context to produce a final response." +] +doc2_chunks = [ + "Embedding models convert text into dense vector representations.", + "Cosine similarity is the standard metric for comparing embeddings." +] + +# Pass as nested arrays (one inner array per document) +response = client.contextualized_embeddings.create( + input=[doc1_chunks, doc2_chunks], + model="pplx-embed-context-v1-4b" +) + +# Nested response: response.data[doc_idx].data[chunk_idx] +for doc in response.data: + for chunk in doc.data: + print(f"Doc {doc.index}, Chunk {chunk.index}: {chunk.embedding[:20]}...") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const doc1Chunks = [ + "RAG combines retrieval with generation to produce grounded answers.", + "The retrieval step searches a vector index for chunks similar to the query.", + "The generation step uses retrieved context to produce a final response." +]; +const doc2Chunks = [ + "Embedding models convert text into dense vector representations.", + "Cosine similarity is the standard metric for comparing embeddings." +]; + +// Pass as nested arrays (one inner array per document) +const response = await client.contextualizedEmbeddings.create({ + input: [doc1Chunks, doc2Chunks], + model: "pplx-embed-context-v1-4b" +}); + +// Nested response: response.data[docIdx].data[chunkIdx] +for (const doc of response.data) { + for (const chunk of doc.data) { + console.log(`Doc ${doc.index}, Chunk ${chunk.index}: ${chunk.embedding.slice(0, 20)}...`); + } +} +``` + + + +**Chunk ordering matters.** Chunks within each document must be passed in their original sequential order. The contextualized model uses positional context to relate neighboring chunks, so shuffling them will degrade embedding quality. + + +## Querying a Contextualized Index + +When using contextualized embeddings, wrap each query as a single-element inner list (e.g., `[[query]]`) so the API treats it as a single-chunk document: + + +```python Python +from perplexity import Perplexity +import base64, numpy as np + +client = Perplexity() + +def decode_embedding(b64: str) -> np.ndarray: + return np.frombuffer(base64.b64decode(b64), dtype=np.int8).astype(np.float32) + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) + +# Index with contextualized model (chunks share cross-chunk attention) +doc_chunks = [ + "RAG combines retrieval with generation to produce grounded answers.", + "The retrieval step finds chunks similar to the user query.", + "The generation step uses retrieved context to produce a final response.", +] +ctx_response = client.contextualized_embeddings.create( + input=[doc_chunks], # nested array: one inner list per document + model="pplx-embed-context-v1-4b" +) +index = [ + {"embedding": decode_embedding(chunk.embedding), "text": doc_chunks[chunk.index]} + for chunk in ctx_response.data[0].data +] + +# Query the index +query = "How does retrieval work in RAG?" +q_response = client.contextualized_embeddings.create( + input=[[query]], model="pplx-embed-context-v1-4b" +) +q_emb = decode_embedding(q_response.data[0].data[0].embedding) +results = sorted(index, key=lambda x: cosine_similarity(q_emb, x["embedding"]), reverse=True) +print(f"Top result: {results[0]['text']}") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +function decodeEmbedding(b64: string): Int8Array { + const buffer = Buffer.from(b64, 'base64'); + return new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +function cosineSimilarity(a: Int8Array, b: Int8Array): number { + let dot = 0, normA = 0, normB = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; normA += a[i] ** 2; normB += b[i] ** 2; + } + return dot / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +// Index with contextualized model +const docChunks = [ + "RAG combines retrieval with generation to produce grounded answers.", + "The retrieval step finds chunks similar to the user query.", + "The generation step uses retrieved context to produce a final response.", +]; +const ctxResponse = await client.contextualizedEmbeddings.create({ + input: [docChunks], // nested array: one inner array per document + model: "pplx-embed-context-v1-4b" +}); +const index = ctxResponse.data[0].data.map(chunk => ({ + embedding: decodeEmbedding(chunk.embedding), + text: docChunks[chunk.index], +})); + +// Query the index +const query = "How does retrieval work in RAG?"; +const qResponse = await client.contextualizedEmbeddings.create({ + input: [[query]], model: "pplx-embed-context-v1-4b" +}); +const qEmb = decodeEmbedding(qResponse.data[0].data[0].embedding); +const results = [...index].sort((a, b) => cosineSimilarity(qEmb, b.embedding) - cosineSimilarity(qEmb, a.embedding)); +console.log(`Top result: ${results[0].text}`); +``` + + +## Building a Vector Index + +This example uses numpy for cosine similarity with a simple in-memory index. For production systems with millions of vectors, use a dedicated vector database (Pinecone, Weaviate, Qdrant, etc.). + + +```python Python +import base64 +import numpy as np +from perplexity import Perplexity + +client = Perplexity() + +def decode_embedding(b64_string: str) -> np.ndarray: + return np.frombuffer(base64.b64decode(b64_string), dtype=np.int8).astype(np.float32) + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) + +# Documents to index +documents = { + "RAG Overview": [ + "Retrieval-augmented generation grounds LLM responses in external data.", + "RAG reduces hallucinations by providing factual context to the model.", + "A typical RAG pipeline has three stages: indexing, retrieval, and generation." + ], + "Embedding Models": [ + "Embedding models map text to dense vector representations.", + "Similar texts produce vectors that are close in the embedding space.", + "Perplexity offers both standard and contextualized embedding models." + ] +} + +# Build index: list of (embedding, text, doc_title) tuples +index = [] +for title, chunks in documents.items(): + response = client.embeddings.create(input=chunks, model="pplx-embed-v1-4b") + for emb_obj in response.data: + index.append({ + "embedding": decode_embedding(emb_obj.embedding), + "text": chunks[emb_obj.index], + "doc_title": title + }) + +print(f"Indexed {len(index)} chunks") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +function decodeEmbedding(b64String: string): Int8Array { + const buffer = Buffer.from(b64String, 'base64'); + return new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +function cosineSimilarity(a: Int8Array, b: Int8Array): number { + let dot = 0, normA = 0, normB = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + return dot / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +const documents: Record = { + "RAG Overview": [ + "Retrieval-augmented generation grounds LLM responses in external data.", + "RAG reduces hallucinations by providing factual context to the model.", + "A typical RAG pipeline has three stages: indexing, retrieval, and generation." + ], + "Embedding Models": [ + "Embedding models map text to dense vector representations.", + "Similar texts produce vectors that are close in the embedding space.", + "Perplexity offers both standard and contextualized embedding models." + ] +}; + +// Build index +const index: { embedding: Int8Array; text: string; docTitle: string }[] = []; +for (const [title, chunks] of Object.entries(documents)) { + const response = await client.embeddings.create({ + input: chunks, + model: "pplx-embed-v1-4b" + }); + for (const embObj of response.data) { + index.push({ + embedding: decodeEmbedding(embObj.embedding), + text: chunks[embObj.index], + docTitle: title + }); + } +} + +console.log(`Indexed ${index.length} chunks`); +``` + + +## Query Pipeline + +The full query pipeline embeds the user question, retrieves the top-k most similar chunks, and passes them as context to the Agent API for answer generation. + + +```python Python +def rag_query(question: str, index: list[dict], top_k: int = 3, min_score: float = 0.3) -> str: + """Embed question -> retrieve similar chunks -> generate answer.""" + # Step 1: Embed the question + query_response = client.embeddings.create(input=[question], model="pplx-embed-v1-4b") + query_emb = decode_embedding(query_response.data[0].embedding) + + # Step 2: Retrieve top-k chunks above the minimum similarity threshold + scored = sorted( + [{"score": cosine_similarity(query_emb, item["embedding"]), **item} for item in index], + key=lambda x: x["score"], reverse=True + )[:top_k] + scored = [item for item in scored if item["score"] >= min_score] + + if not scored: + return "No relevant context found for this question." + + # Include source attribution alongside each chunk + context = "\n\n".join( + f"[Source: {item['doc_title']}]\n{item['text']}" for item in scored + ) + + # Step 3: Generate answer via Agent API + response = client.responses.create( + model="openai/gpt-5.4", + input=question, + instructions=( + "Answer based only on the provided context. " + "Cite sources by name when referencing specific information. " + "If the context does not contain enough information, say so.\n\n" + f"Context:\n{context}" + ) + ) + return response.output_text + +answer = rag_query("What are the stages of a RAG pipeline?", index) +print(answer) +``` + +```typescript TypeScript +async function ragQuery(question: string, idx: typeof index, topK: number = 3, minScore: number = 0.3): Promise { + // Step 1: Embed the question + const qResponse = await client.embeddings.create({ + input: [question], model: "pplx-embed-v1-4b" + }); + const qEmb = decodeEmbedding(qResponse.data[0].embedding); + + // Step 2: Retrieve top-k chunks above the minimum similarity threshold + const scored = idx + .map(item => ({ ...item, score: cosineSimilarity(qEmb, item.embedding) })) + .sort((a, b) => b.score - a.score) + .slice(0, topK) + .filter(item => item.score >= minScore); + + if (scored.length === 0) { + return "No relevant context found for this question."; + } + + // Include source attribution alongside each chunk + const context = scored + .map(item => `[Source: ${item.docTitle}]\n${item.text}`) + .join("\n\n"); + + // Step 3: Generate answer via Agent API + const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: question, + instructions: `Answer based only on the provided context. Cite sources by name when referencing specific information. If the context does not contain enough information, say so.\n\nContext:\n${context}` + }); + return response.output_text; +} + +const answer = await ragQuery("What are the stages of a RAG pipeline?", index); +console.log(answer); +``` + + + +Start with `top_k=3` and `min_score=0.3` for most use cases. Raise `top_k` to 5–7 for broad questions or short chunks. Raise `min_score` to 0.5–0.7 if retrieved chunks contain irrelevant information. Lower it toward 0.2 for diverse or ambiguous queries. + + +## Standard vs Contextualized Comparison + +| Aspect | Standard (`pplx-embed-v1-4b`) | Contextualized (`pplx-embed-context-v1-4b`) | +|--------|-------------------------------|---------------------------------------------| +| **Input format** | Flat list of texts | Nested arrays grouped by document | +| **Context awareness** | Each text embedded independently | Chunks share cross-chunk context within each document | +| **Best for** | FAQ entries, standalone texts, short documents | Document paragraphs, article sections | +| **Chunk ordering** | Order does not matter | Must be in original document order | +| **Query embedding** | `client.embeddings.create(input=[query])` | `client.contextualized_embeddings.create(input=[[query]])` | +| **Price (4b model)** | $0.03 / 1M tokens | $0.05 / 1M tokens | + +### When to Use Standard Embeddings + +- Chunks are self-contained and do not rely on surrounding context. +- Your content consists of FAQ pairs, product descriptions, or short independent entries. +- You need the lowest cost per token. + +### When to Use Contextualized Embeddings + +- Chunks come from longer documents where meaning depends on neighboring text. +- A chunk like "This approach improves performance by 20%" only makes sense with its surrounding context. +- You are embedding paragraphs from articles, reports, or technical documentation. +- You want higher retrieval accuracy at a modest cost increase. + +## Matryoshka Dimensions + +Perplexity embedding models support Matryoshka Representation Learning (MRL), which concentrates the most important information in the first N dimensions. You can request reduced dimensions directly via the API for faster search and smaller storage. + + +```python Python +import base64 +import numpy as np +from perplexity import Perplexity + +client = Perplexity() + +texts = ["Matryoshka embeddings allow dimension reduction without re-embedding."] + +def decode_embedding(b64: str) -> np.ndarray: + return np.frombuffer(base64.b64decode(b64), dtype=np.int8) + +# Full dimensions (2560 for 4b model) +full = client.embeddings.create(input=texts, model="pplx-embed-v1-4b") + +# Reduced to 512 dimensions via the API +reduced = client.embeddings.create(input=texts, model="pplx-embed-v1-4b", dimensions=512) + +print(f"Full: {len(decode_embedding(full.data[0].embedding))} dimensions") +print(f"Reduced: {len(decode_embedding(reduced.data[0].embedding))} dimensions") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const texts = ["Matryoshka embeddings allow dimension reduction without re-embedding."]; + +function decodeEmbedding(b64: string): Int8Array { + const buffer = Buffer.from(b64, 'base64'); + return new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +// Full dimensions (2560 for 4b model) +const full = await client.embeddings.create({ input: texts, model: "pplx-embed-v1-4b" }); + +// Reduced to 512 dimensions via the API +const reduced = await client.embeddings.create({ + input: texts, model: "pplx-embed-v1-4b", dimensions: 512 +}); + +console.log(`Full: ${decodeEmbedding(full.data[0].embedding).length} dimensions`); +console.log(`Reduced: ${decodeEmbedding(reduced.data[0].embedding).length} dimensions`); +``` + + +Dimension reduction tradeoffs for the `pplx-embed-v1-4b` model: + +| Dimensions | Storage per Vector | Relative Quality | Use Case | +|:----------:|:-----------------:|:----------------:|----------| +| 2560 (full) | 2.5 KB | Highest | Maximum accuracy, small datasets | +| 1024 | 1 KB | Very high | Good balance for most applications | +| 512 | 512 B | High | Large-scale retrieval, fast search | +| 256 | 256 B | Moderate | Extremely large datasets, coarse filtering | +| 128 | 128 B | Lower | First-pass candidate filtering | + + +Use the `dimensions` parameter in the API call rather than manually truncating vectors. The API applies proper normalization for the requested dimension count. Start with full dimensions and reduce only when storage or latency becomes a bottleneck. + + +## Batch Processing + +When embedding large document collections, process them in batches to stay within API rate limits. The standard API accepts up to 512 texts per request with a combined limit of 120,000 tokens. + + +```python Python +import asyncio +import base64 +import numpy as np +from perplexity import AsyncPerplexity + +def decode_embedding(b64_string: str) -> np.ndarray: + return np.frombuffer(base64.b64decode(b64_string), dtype=np.int8).astype(np.float32) + +async def batch_embed(texts: list[str], batch_size: int = 100) -> list[np.ndarray]: + """Embed texts in batches with rate limiting.""" + async with AsyncPerplexity() as client: + all_embeddings = [] + for i in range(0, len(texts), batch_size): + batch = texts[i:i + batch_size] + response = await client.embeddings.create( + input=batch, model="pplx-embed-v1-4b" + ) + all_embeddings.extend(decode_embedding(e.embedding) for e in response.data) + print(f"Embedded {min(i + batch_size, len(texts))}/{len(texts)}") + if i + batch_size < len(texts): + await asyncio.sleep(0.1) # Brief delay between batches + return all_embeddings + +# Usage +texts = [f"Document chunk number {i} with content." for i in range(500)] +embeddings = asyncio.run(batch_embed(texts, batch_size=100)) +print(f"Total: {len(embeddings)} embeddings") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +function decodeEmbedding(b64String: string): Int8Array { + const buffer = Buffer.from(b64String, 'base64'); + return new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +async function batchEmbed(texts: string[], batchSize: number = 100): Promise { + const allEmbeddings: Int8Array[] = []; + for (let i = 0; i < texts.length; i += batchSize) { + const batch = texts.slice(i, i + batchSize); + const response = await client.embeddings.create({ + input: batch, model: "pplx-embed-v1-4b" + }); + allEmbeddings.push(...response.data.map(e => decodeEmbedding(e.embedding))); + console.log(`Embedded ${Math.min(i + batchSize, texts.length)}/${texts.length}`); + if (i + batchSize < texts.length) { + await new Promise(r => setTimeout(r, 100)); // Brief delay between batches + } + } + return allEmbeddings; +} + +// Usage +const texts = Array.from({ length: 500 }, (_, i) => `Document chunk number ${i} with content.`); +const embeddings = await batchEmbed(texts, 100); +console.log(`Total: ${embeddings.length} embeddings`); +``` + + + +For contextualized embeddings, batch at the document level using `client.contextualized_embeddings.create(input=batch_of_doc_arrays)` with the same pattern. The contextualized API accepts up to 512 documents with 16,000 total chunks per request. + + + +**Rate limits:** Keep batch sizes well within the API limits (512 texts / 120,000 tokens for standard; 512 documents / 16,000 chunks for contextualized) and add small delays between requests to avoid throttling. + + +## Complete Example + +A self-contained pipeline that indexes two documents with contextualized embeddings and answers questions against the indexed content. + + +```python Python +import base64 +import numpy as np +from perplexity import Perplexity + +client = Perplexity() + +# --- Helpers --- + +def chunk_text(text: str, chunk_size: int = 400, overlap: int = 80) -> list[str]: + chunks, start = [], 0 + while start < len(text): + chunk = text[start:start + chunk_size].strip() + if chunk: + chunks.append(chunk) + start += chunk_size - overlap + return chunks + +def decode_embedding(b64: str) -> np.ndarray: + return np.frombuffer(base64.b64decode(b64), dtype=np.int8).astype(np.float32) + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) + +# --- Source documents --- + +DOCUMENTS = { + "Quantum Computing": ( + "Quantum computers use qubits that can exist in superposition, representing " + "0 and 1 simultaneously. Unlike classical bits, qubits leverage quantum " + "interference to perform calculations. Quantum entanglement allows qubits to " + "be correlated, enabling parallel processing at scale. Current quantum computers " + "from IBM, Google, and others have dozens to hundreds of physical qubits." + ), + "Machine Learning": ( + "Machine learning enables computers to learn from data without explicit " + "programming. Supervised learning uses labeled examples to train models for " + "classification and regression. Neural networks with many layers (deep learning) " + "excel at image recognition and language tasks. Training requires large datasets " + "and significant compute, often using GPUs or TPUs." + ), +} + +# --- Step 1: Index with the model --- + +def build_index(documents: dict[str, str]) -> list[dict]: + index = [] + for title, text in documents.items(): + chunks = chunk_text(text) + response = client.contextualized_embeddings.create( + input=[chunks], + model="pplx-embed-context-v1-4b" + ) + for chunk_obj in response.data[0].data: + index.append({ + "embedding": decode_embedding(chunk_obj.embedding), + "text": chunks[chunk_obj.index], + "doc_title": title, + }) + print(f"Indexed {len(index)} chunks from {len(documents)} documents") + return index + +# --- Step 2: Query the index, retrieve, generate --- + +def rag_query(question: str, index: list[dict], top_k: int = 3, min_score: float = 0.3) -> str: + q_resp = client.contextualized_embeddings.create( + input=[[question]], model="pplx-embed-context-v1-4b" + ) + q_emb = decode_embedding(q_resp.data[0].data[0].embedding) + + results = sorted( + [{"score": cosine_similarity(q_emb, item["embedding"]), **item} for item in index], + key=lambda x: x["score"], reverse=True + )[:top_k] + results = [r for r in results if r["score"] >= min_score] + + if not results: + return "No relevant context found for this question." + + context = "\n\n".join(f"[{r['doc_title']}]\n{r['text']}" for r in results) + + response = client.responses.create( + model="openai/gpt-5.4", + input=question, + instructions=( + "Answer based only on the provided context. " + "Cite the source name in brackets when referencing information. " + "If the context is insufficient, say so.\n\n" + f"Context:\n{context}" + ) + ) + return response.output_text + +# --- Run --- + +if __name__ == "__main__": + index = build_index(DOCUMENTS) + + questions = [ + "What makes qubits different from classical bits?", + "What hardware is used to train machine learning models?", + ] + for q in questions: + print(f"\nQ: {q}") + print(f"A: {rag_query(q, index)}") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +// --- Helpers --- + +function chunkText(text: string, chunkSize = 400, overlap = 80): string[] { + const chunks: string[] = []; + let start = 0; + while (start < text.length) { + const chunk = text.slice(start, start + chunkSize).trim(); + if (chunk) chunks.push(chunk); + start += chunkSize - overlap; + } + return chunks; +} + +function decodeEmbedding(b64: string): Int8Array { + const buffer = Buffer.from(b64, 'base64'); + return new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +function cosineSimilarity(a: Int8Array, b: Int8Array): number { + let dot = 0, normA = 0, normB = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; normA += a[i] ** 2; normB += b[i] ** 2; + } + return dot / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +// --- Source documents --- + +const DOCUMENTS: Record = { + "Quantum Computing": "Quantum computers use qubits that can exist in superposition, representing 0 and 1 simultaneously. Unlike classical bits, qubits leverage quantum interference to perform calculations. Quantum entanglement allows qubits to be correlated, enabling parallel processing at scale. Current quantum computers from IBM, Google, and others have dozens to hundreds of physical qubits.", + "Machine Learning": "Machine learning enables computers to learn from data without explicit programming. Supervised learning uses labeled examples to train models for classification and regression. Neural networks with many layers (deep learning) excel at image recognition and language tasks. Training requires large datasets and significant compute, often using GPUs or TPUs.", +}; + +type IndexEntry = { embedding: Int8Array; text: string; docTitle: string }; + +// --- Step 1: Index with the model --- + +async function buildIndex(documents: Record): Promise { + const index: IndexEntry[] = []; + for (const [title, text] of Object.entries(documents)) { + const chunks = chunkText(text); + const response = await client.contextualizedEmbeddings.create({ + input: [chunks], + model: "pplx-embed-context-v1-4b" + }); + for (const chunkObj of response.data[0].data) { + index.push({ + embedding: decodeEmbedding(chunkObj.embedding), + text: chunks[chunkObj.index], + docTitle: title, + }); + } + } + console.log(`Indexed ${index.length} chunks from ${Object.keys(documents).length} documents`); + return index; +} + +// --- Step 2: Query the index, retrieve, generate --- + +async function ragQuery( + question: string, + index: IndexEntry[], + topK = 3, + minScore = 0.3 +): Promise { + const qResp = await client.contextualizedEmbeddings.create({ + input: [[question]], model: "pplx-embed-context-v1-4b" + }); + const qEmb = decodeEmbedding(qResp.data[0].data[0].embedding); + + const results = index + .map(item => ({ ...item, score: cosineSimilarity(qEmb, item.embedding) })) + .sort((a, b) => b.score - a.score) + .slice(0, topK) + .filter(r => r.score >= minScore); + + if (results.length === 0) return "No relevant context found for this question."; + + const context = results.map(r => `[${r.docTitle}]\n${r.text}`).join("\n\n"); + + const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: question, + instructions: `Answer based only on the provided context. Cite the source name in brackets when referencing information. If the context is insufficient, say so.\n\nContext:\n${context}`, + }); + return response.output_text; +} + +// --- Run --- + +const index = await buildIndex(DOCUMENTS); + +const questions = [ + "What makes qubits different from classical bits?", + "What hardware is used to train machine learning models?", +]; +for (const q of questions) { + console.log(`\nQ: ${q}`); + console.log(`A: ${await ragQuery(q, index)}`); +} +``` + + +## Next Steps + + + +API reference for standard embedding parameters and response format. + + + +API reference for contextualized embedding parameters and response format. + + + +Encoding formats, similarity metrics, normalization, and error handling. + + + +Learn more about the Responses API used for answer generation. + + diff --git a/docs/articles/function-calling-e2e/README.mdx b/docs/articles/function-calling-e2e/README.mdx new file mode 100644 index 0000000..8c4bb1d --- /dev/null +++ b/docs/articles/function-calling-e2e/README.mdx @@ -0,0 +1,938 @@ +--- +title: Function Calling End-to-End +description: Complete multi-turn function calling patterns for the Perplexity Agent API, including orchestration, web search integration, error handling, and parallel calls +keywords: [function-calling, tools, agent-api, multi-turn, orchestration, parallel, web-search] +products: [agent-api] +categories: [function-calling, orchestration] +--- + +This guide covers production-ready function calling patterns that go beyond the basics. You will learn the complete multi-turn flow, multi-function orchestration, combining custom functions with built-in tools, robust error handling, and parallel function call processing. + + +This guide assumes familiarity with the Agent API and its tool definitions. For parameter reference and basic usage, see the [Agent API reference](/api-reference/agent-post). + + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + + +For built-in tools, start with [Web Search](/docs/agent-api/tools/web-search), [Fetch URL Content](/docs/agent-api/tools/fetch-url-content), [Finance Search](/docs/agent-api/tools/finance-search), and [People Search](/docs/agent-api/tools/people-search). This guide focuses on custom function orchestration patterns in application code. + + +## Complete Multi-Turn Flow + +The core function calling loop follows a specific pattern: send a request with tool definitions, detect `function_call` items in the response, execute your functions locally, then return the results as `function_call_output` items. + + +```python Python +from perplexity import Perplexity +import json + +client = Perplexity() + +# Step 1: Define tools +tools = [ + { + "type": "function", + "name": "lookup_order", + "description": "Look up an order by order ID. Returns order status, items, and shipping info.", + "parameters": { + "type": "object", + "properties": { + "order_id": { + "type": "string", + "description": "The unique order identifier, e.g. ORD-12345" + } + }, + "required": ["order_id"] + } + } +] + +# Your actual function implementation +def lookup_order(order_id: str) -> dict: + # In production, query your database or order management system + return { + "order_id": order_id, + "status": "shipped", + "items": ["Wireless Headphones", "USB-C Cable"], + "tracking_number": "1Z999AA10123456784", + "estimated_delivery": "2026-03-02" + } + +# Step 2: Send the initial request +response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input="Where is my order ORD-98712?" +) + +# Step 3: Process the response and handle function calls +next_input = [item.model_dump() for item in response.output] + +for item in response.output: + if item.type == "function_call": + # Step 4: Parse arguments and execute the function + args = json.loads(item.arguments) + result = lookup_order(**args) + + # Step 5: Append the function result + next_input.append({ + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result) + }) + +# Step 6: Send results back to get the final response +final_response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + input=next_input +) + +print(final_response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +// Step 1: Define tools +const tools = [ + { + type: "function" as const, + name: "lookup_order", + description: "Look up an order by order ID. Returns order status, items, and shipping info.", + parameters: { + type: "object", + properties: { + order_id: { + type: "string", + description: "The unique order identifier, e.g. ORD-12345" + } + }, + required: ["order_id"] + } + } +]; + +// Your actual function implementation +function lookupOrder(orderId: string): Record { + // In production, query your database or order management system + return { + order_id: orderId, + status: "shipped", + items: ["Wireless Headphones", "USB-C Cable"], + tracking_number: "1Z999AA10123456784", + estimated_delivery: "2026-03-02" + }; +} + +// Step 2: Send the initial request +const response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: "Where is my order ORD-98712?" +}); + +// Step 3: Process the response and handle function calls +const nextInput: any[] = response.output.map(item => ({ ...item })); + +for (const item of response.output) { + if (item.type === "function_call") { + // Step 4: Parse arguments and execute the function + const args = JSON.parse(item.arguments); + const result = lookupOrder(args.order_id); + + // Step 5: Append the function result + nextInput.push({ + type: "function_call_output", + call_id: item.call_id, + output: JSON.stringify(result) + }); + } +} + +// Step 6: Send results back to get the final response +const finalResponse = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + input: nextInput +}); + +console.log(finalResponse.output_text); +``` + + + +Always use `json.loads()` (Python) or `JSON.parse()` (TypeScript) on the `arguments` field. It is a JSON string, not a parsed object. + + +## Multi-Function Orchestration + +When you provide multiple tools, the model decides which to call and in what order. This example registers three functions that work together to answer a complex query. + + +```python Python +from perplexity import Perplexity +import json + +client = Perplexity() + +# Define multiple tools +tools = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the current weather forecast for a city. Returns temperature, conditions, and precipitation chance.", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string", "description": "City name"} + }, + "required": ["city"] + } + }, + { + "type": "function", + "name": "get_calendar_events", + "description": "Retrieve today's calendar events for a user. Returns a list of events with times and locations.", + "parameters": { + "type": "object", + "properties": { + "user_id": {"type": "string", "description": "The user ID"}, + "date": {"type": "string", "description": "Date in YYYY-MM-DD format"} + }, + "required": ["user_id", "date"] + } + }, + { + "type": "function", + "name": "send_email", + "description": "Send an email to a recipient with a subject and body.", + "parameters": { + "type": "object", + "properties": { + "to": {"type": "string", "description": "Recipient email address"}, + "subject": {"type": "string", "description": "Email subject line"}, + "body": {"type": "string", "description": "Email body text"} + }, + "required": ["to", "subject", "body"] + } + } +] + +# Function implementations +def get_weather(city: str) -> dict: + return {"city": city, "temp_f": 72, "conditions": "Partly cloudy", "precipitation_chance": 0.15} + +def get_calendar_events(user_id: str, date: str) -> dict: + return { + "events": [ + {"time": "09:00", "title": "Team standup", "location": "Conference Room B"}, + {"time": "12:00", "title": "Lunch with client", "location": "Riverside Park (outdoor)"}, + {"time": "15:00", "title": "Sprint review", "location": "Zoom"} + ] + } + +def send_email(to: str, subject: str, body: str) -> dict: + # In production, integrate with your email service + return {"status": "sent", "message_id": "msg-20260226-001"} + +# Map function names to implementations +function_map = { + "get_weather": get_weather, + "get_calendar_events": get_calendar_events, + "send_email": send_email, +} + +# Multi-turn loop: keep sending requests until no more function calls +input_messages = [ + {"role": "user", "content": ( + "I'm user U-100 in San Francisco. What's my schedule today (2026-02-26) " + "and is the weather good for my outdoor events? " + "If there's rain risk, email me at alice@example.com with a reminder to bring an umbrella." + )} +] + +response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=input_messages +) + +# Loop until the model produces a final text response with no pending function calls +while any(item.type == "function_call" for item in response.output): + next_input = [item.model_dump() for item in response.output] + + for item in response.output: + if item.type == "function_call": + args = json.loads(item.arguments) + fn = function_map[item.name] + result = fn(**args) + + next_input.append({ + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result) + }) + + response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=next_input + ) + +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +// Define multiple tools +const tools = [ + { + type: "function" as const, + name: "get_weather", + description: "Get the current weather forecast for a city. Returns temperature, conditions, and precipitation chance.", + parameters: { + type: "object", + properties: { + city: { type: "string", description: "City name" } + }, + required: ["city"] + } + }, + { + type: "function" as const, + name: "get_calendar_events", + description: "Retrieve today's calendar events for a user. Returns a list of events with times and locations.", + parameters: { + type: "object", + properties: { + user_id: { type: "string", description: "The user ID" }, + date: { type: "string", description: "Date in YYYY-MM-DD format" } + }, + required: ["user_id", "date"] + } + }, + { + type: "function" as const, + name: "send_email", + description: "Send an email to a recipient with a subject and body.", + parameters: { + type: "object", + properties: { + to: { type: "string", description: "Recipient email address" }, + subject: { type: "string", description: "Email subject line" }, + body: { type: "string", description: "Email body text" } + }, + required: ["to", "subject", "body"] + } + } +]; + +// Function implementations +function getWeather(city: string) { + return { city, temp_f: 72, conditions: "Partly cloudy", precipitation_chance: 0.15 }; +} + +function getCalendarEvents(userId: string, date: string) { + return { + events: [ + { time: "09:00", title: "Team standup", location: "Conference Room B" }, + { time: "12:00", title: "Lunch with client", location: "Riverside Park (outdoor)" }, + { time: "15:00", title: "Sprint review", location: "Zoom" } + ] + }; +} + +function sendEmail(to: string, subject: string, body: string) { + return { status: "sent", message_id: "msg-20260226-001" }; +} + +// Map function names to implementations +const functionMap: Record any> = { + get_weather: (args: any) => getWeather(args.city), + get_calendar_events: (args: any) => getCalendarEvents(args.user_id, args.date), + send_email: (args: any) => sendEmail(args.to, args.subject, args.body), +}; + +// Multi-turn loop +let response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: [ + { + role: "user", + content: + "I'm user U-100 in San Francisco. What's my schedule today (2026-02-26) " + + "and is the weather good for my outdoor events? " + + "If there's rain risk, email me at alice@example.com with a reminder to bring an umbrella." + } + ] +}); + +while (response.output.some(item => item.type === "function_call")) { + const nextInput: any[] = response.output.map(item => ({ ...item })); + + for (const item of response.output) { + if (item.type === "function_call") { + const args = JSON.parse(item.arguments); + const result = functionMap[item.name](args); + + nextInput.push({ + type: "function_call_output", + call_id: item.call_id, + output: JSON.stringify(result) + }); + } + } + + response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: nextInput + }); +} + +console.log(response.output_text); +``` + + + +The model may call functions across multiple turns. The `while` loop above keeps running until the model finishes all function calls and produces a final text response. In some turns the model may call one function, and in the next turn call another based on the results it received. + + +## Combining Custom Functions with `web_search` + +You can mix built-in tools like `web_search` and `fetch_url` with your own custom functions in the same `tools` array. The model decides autonomously which tool to use. This is powerful for workflows that need live web data combined with actions in your own systems. + + +```python Python +from perplexity import Perplexity +import json + +client = Perplexity() + +tools = [ + # Built-in web search + {"type": "web_search"}, + # Custom function to persist data + { + "type": "function", + "name": "save_to_db", + "description": "Save a research summary to the internal database. Call this after gathering information to persist findings.", + "parameters": { + "type": "object", + "properties": { + "topic": {"type": "string", "description": "The research topic"}, + "summary": {"type": "string", "description": "A concise summary of the findings"}, + "sources": { + "type": "array", + "items": {"type": "string"}, + "description": "List of source URLs" + } + }, + "required": ["topic", "summary", "sources"] + } + } +] + +def save_to_db(topic: str, summary: str, sources: list) -> dict: + # In production, write to your database + record_id = "rec-" + topic.lower().replace(" ", "-")[:20] + print(f"Saved to DB: {record_id}") + return {"record_id": record_id, "status": "saved"} + +response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input="Research the latest developments in solid-state batteries, then save your findings to our database.", + instructions="First search the web for current information, then use save_to_db to persist your summary." +) + +# The model will use web_search automatically (no function_call for built-in tools), +# then call save_to_db which we need to handle. +while any(item.type == "function_call" for item in response.output): + next_input = [item.model_dump() for item in response.output] + + for item in response.output: + if item.type == "function_call": + args = json.loads(item.arguments) + result = save_to_db(**args) + next_input.append({ + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result) + }) + + response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=next_input + ) + +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const tools = [ + // Built-in web search + { type: "web_search" as const }, + // Custom function to persist data + { + type: "function" as const, + name: "save_to_db", + description: "Save a research summary to the internal database. Call this after gathering information to persist findings.", + parameters: { + type: "object", + properties: { + topic: { type: "string", description: "The research topic" }, + summary: { type: "string", description: "A concise summary of the findings" }, + sources: { + type: "array", + items: { type: "string" }, + description: "List of source URLs" + } + }, + required: ["topic", "summary", "sources"] + } + } +]; + +function saveToDb(topic: string, summary: string, sources: string[]) { + const recordId = "rec-" + topic.toLowerCase().replace(/ /g, "-").slice(0, 20); + console.log(`Saved to DB: ${recordId}`); + return { record_id: recordId, status: "saved" }; +} + +let response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: "Research the latest developments in solid-state batteries, then save your findings to our database.", + instructions: "First search the web for current information, then use save_to_db to persist your summary." +}); + +while (response.output.some(item => item.type === "function_call")) { + const nextInput: any[] = response.output.map(item => ({ ...item })); + + for (const item of response.output) { + if (item.type === "function_call") { + const args = JSON.parse(item.arguments); + const result = saveToDb(args.topic, args.summary, args.sources); + nextInput.push({ + type: "function_call_output", + call_id: item.call_id, + output: JSON.stringify(result) + }); + } + } + + response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: nextInput + }); +} + +console.log(response.output_text); +``` + + + +Built-in tools like `web_search` are executed server-side by the API. You only need to handle `function_call` items for your custom functions. The model seamlessly interleaves built-in and custom tool usage. + + +## Error Handling Patterns + +When a function call fails, return a structured error in the `function_call_output` so the model can adapt its response. Never silently swallow errors; the model can often recover or inform the user gracefully. + + +```python Python +from perplexity import Perplexity +import json +import traceback + +client = Perplexity() + +def execute_function(name: str, args: dict) -> dict: + """Dispatch and execute a function call with error handling.""" + function_map = { + "lookup_order": lookup_order, + "cancel_order": cancel_order, + } + + if name not in function_map: + return {"error": True, "message": f"Unknown function: {name}"} + + try: + result = function_map[name](**args) + return result + except KeyError as e: + return {"error": True, "message": f"Missing required field: {e}"} + except TimeoutError: + return {"error": True, "message": "The request timed out. Please try again."} + except Exception as e: + return {"error": True, "message": f"Function failed: {str(e)}"} + + +def lookup_order(order_id: str) -> dict: + if not order_id.startswith("ORD-"): + raise ValueError(f"Invalid order ID format: {order_id}") + return {"order_id": order_id, "status": "delivered"} + + +def cancel_order(order_id: str) -> dict: + # Simulate a failure + raise ConnectionError("Order service is temporarily unavailable") + + +def run_agent(user_input: str, tools: list) -> str: + """Run the full function calling loop with error handling.""" + response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=user_input + ) + + max_turns = 10 + turn = 0 + + while any(item.type == "function_call" for item in response.output) and turn < max_turns: + next_input = [item.model_dump() for item in response.output] + + for item in response.output: + if item.type == "function_call": + args = json.loads(item.arguments) + result = execute_function(item.name, args) + + next_input.append({ + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result) + }) + + response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=next_input + ) + turn += 1 + + if turn >= max_turns: + return "Error: Maximum function call turns exceeded." + + return response.output_text +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +function lookupOrder(orderId: string): Record { + if (!orderId.startsWith("ORD-")) { + throw new Error(`Invalid order ID format: ${orderId}`); + } + return { order_id: orderId, status: "delivered" }; +} + +function cancelOrder(orderId: string): Record { + // Simulate a failure + throw new Error("Order service is temporarily unavailable"); +} + +function executeFunction(name: string, args: Record): Record { + const functionMap: Record Record> = { + lookup_order: (a) => lookupOrder(a.order_id), + cancel_order: (a) => cancelOrder(a.order_id), + }; + + if (!(name in functionMap)) { + return { error: true, message: `Unknown function: ${name}` }; + } + + try { + return functionMap[name](args); + } catch (e: any) { + return { error: true, message: `Function failed: ${e.message}` }; + } +} + +async function runAgent(userInput: string, tools: any[]): Promise { + let response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: userInput + }); + + const maxTurns = 10; + let turn = 0; + + while (response.output.some(item => item.type === "function_call") && turn < maxTurns) { + const nextInput: any[] = response.output.map(item => ({ ...item })); + + for (const item of response.output) { + if (item.type === "function_call") { + const args = JSON.parse(item.arguments); + const result = executeFunction(item.name, args); + + nextInput.push({ + type: "function_call_output", + call_id: item.call_id, + output: JSON.stringify(result) + }); + } + } + + response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: nextInput + }); + turn++; + } + + if (turn >= maxTurns) { + return "Error: Maximum function call turns exceeded."; + } + + return response.output_text; +} +``` + + +Key principles for error handling: + +- **Return errors as structured data**, not exceptions. Include `"error": true` and a human-readable `"message"` so the model can relay the issue to the user. +- **Catch specific exceptions** (timeouts, auth failures, validation errors) and map them to clear messages. +- **Cap the number of turns** to prevent infinite loops. +- **Never return raw stack traces** to the model. They waste tokens and may leak internal details. + +## Parallel Function Calls + +When the model determines that multiple function calls are independent, it may return several `function_call` items in a single response. Process all of them before sending results back in one batch. + + +```python Python +from perplexity import Perplexity +import json +from concurrent.futures import ThreadPoolExecutor + +client = Perplexity() + +tools = [ + { + "type": "function", + "name": "get_stock_price", + "description": "Get the current stock price for a ticker symbol.", + "parameters": { + "type": "object", + "properties": { + "ticker": {"type": "string", "description": "Stock ticker symbol, e.g. AAPL"} + }, + "required": ["ticker"] + } + }, + { + "type": "function", + "name": "get_company_info", + "description": "Get basic company information for a ticker symbol.", + "parameters": { + "type": "object", + "properties": { + "ticker": {"type": "string", "description": "Stock ticker symbol"} + }, + "required": ["ticker"] + } + } +] + +def get_stock_price(ticker: str) -> dict: + prices = {"AAPL": 245.12, "GOOGL": 192.45, "TSLA": 371.80} + return {"ticker": ticker, "price": prices.get(ticker, 0.0), "currency": "USD"} + +def get_company_info(ticker: str) -> dict: + info = { + "AAPL": {"name": "Apple Inc.", "sector": "Technology", "market_cap": "3.7T"}, + "GOOGL": {"name": "Alphabet Inc.", "sector": "Technology", "market_cap": "2.4T"}, + } + return info.get(ticker, {"name": "Unknown", "sector": "Unknown", "market_cap": "N/A"}) + +function_map = { + "get_stock_price": get_stock_price, + "get_company_info": get_company_info, +} + +response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input="Compare the current stock prices and company details for AAPL and GOOGL." +) + +while any(item.type == "function_call" for item in response.output): + # Collect all pending function calls + pending_calls = [item for item in response.output if item.type == "function_call"] + next_input = [item.model_dump() for item in response.output] + + # Execute all function calls in parallel + def run_call(item): + args = json.loads(item.arguments) + result = function_map[item.name](**args) + return { + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result) + } + + with ThreadPoolExecutor(max_workers=len(pending_calls)) as executor: + results = list(executor.map(run_call, pending_calls)) + + next_input.extend(results) + + response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=next_input + ) + +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const tools = [ + { + type: "function" as const, + name: "get_stock_price", + description: "Get the current stock price for a ticker symbol.", + parameters: { + type: "object", + properties: { + ticker: { type: "string", description: "Stock ticker symbol, e.g. AAPL" } + }, + required: ["ticker"] + } + }, + { + type: "function" as const, + name: "get_company_info", + description: "Get basic company information for a ticker symbol.", + parameters: { + type: "object", + properties: { + ticker: { type: "string", description: "Stock ticker symbol" } + }, + required: ["ticker"] + } + } +]; + +function getStockPrice(ticker: string) { + const prices: Record = { AAPL: 245.12, GOOGL: 192.45, TSLA: 371.80 }; + return { ticker, price: prices[ticker] ?? 0.0, currency: "USD" }; +} + +function getCompanyInfo(ticker: string) { + const info: Record = { + AAPL: { name: "Apple Inc.", sector: "Technology", market_cap: "3.7T" }, + GOOGL: { name: "Alphabet Inc.", sector: "Technology", market_cap: "2.4T" }, + }; + return info[ticker] ?? { name: "Unknown", sector: "Unknown", market_cap: "N/A" }; +} + +const functionMap: Record any> = { + get_stock_price: (args) => getStockPrice(args.ticker), + get_company_info: (args) => getCompanyInfo(args.ticker), +}; + +let response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: "Compare the current stock prices and company details for AAPL and GOOGL." +}); + +while (response.output.some(item => item.type === "function_call")) { + const pendingCalls = response.output.filter(item => item.type === "function_call"); + const nextInput: any[] = response.output.map(item => ({ ...item })); + + // Execute all function calls in parallel + const results = await Promise.all( + pendingCalls.map(async (item) => { + const args = JSON.parse(item.arguments); + const result = functionMap[item.name](args); + return { + type: "function_call_output", + call_id: item.call_id, + output: JSON.stringify(result) + }; + }) + ); + + nextInput.push(...results); + + response = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + tools: tools, + input: nextInput + }); +} + +console.log(response.output_text); +``` + + + +The model may emit multiple `function_call` items in a single response when it determines the calls are independent. Using `ThreadPoolExecutor` (Python) or `Promise.all` (TypeScript) lets you execute them concurrently, reducing total latency. + + +## Next Steps + + + +Review request parameters and tool schema fields. + + + +Choose a model for your function-calling workload. + + + +Get up and running with the Agent API in minutes. + + + +Combine function calling with structured outputs and response shaping. + + diff --git a/docs/articles/langchain-vc-memo-agent/README.mdx b/docs/articles/langchain-vc-memo-agent/README.mdx new file mode 100644 index 0000000..b85b0a8 --- /dev/null +++ b/docs/articles/langchain-vc-memo-agent/README.mdx @@ -0,0 +1,371 @@ +--- +title: VC Investment Memo Agent with LangGraph +description: Build an auditable, citation-grounded VC research agent with LangGraph and the Perplexity Agent API, and pick the best search provider with a LangSmith eval harness. +sidebar_position: 10 +keywords: [langgraph, langchain, langsmith, agent api, investment memo, evaluation] +products: [agent-api] +categories: [finance, deep-research, orchestration, integrations] +--- + +## Overview + +This guide builds an agent that takes a company name and returns a citation-grounded VC investment memo with seven sections: Snapshot, Team, Financials, Product, Market, Risks, and a Thesis ending in a one-line recommendation. **Every claim is traced back to a primary source.** + +It runs on the [Perplexity Agent API](/docs/agent-api/quickstart) and its built-in `web_search` and `finance_search` tools, orchestrated with [LangGraph](https://langchain-ai.github.io/langgraph/), and evaluated in [LangSmith](https://docs.langchain.com/langsmith/home). The whole build runs in about ninety seconds for roughly $0.40 per memo. + +The design lesson generalizes beyond finance: **separating search from synthesis is a structural reliability fix for a research agent.** Four research nodes fan out in parallel, each calling the Agent API with its own tools. A final synthesizer node has no tools and can only cite evidence the research nodes already gathered, so the memo cannot invent a source. + +![Company name flows into START, fans out to four parallel research nodes (team, financials, product, market), which all feed a single synthesizer node that produces the memo at END.](../../static/img/langchain-vc-memo-architecture.png) + +## Features + +- **Parallel research fan-out.** Four focused research nodes (team, financials, product, market) run concurrently, each with its own tools and search budget. +- **Tool-less synthesizer.** The final memo is composed only from upstream evidence, a structural guard against fabricated citations. +- **Built-in Agent API tools.** `web_search` and `finance_search` work out of the box; no client-side search plumbing for the core agent. +- **Auditable in LangSmith.** Every node's tool calls and outputs are captured, so any claim traces back to the search result that produced it. +- **Provider eval harness.** A LangSmith comparison that scores search providers on primary-source rate, financial-concept coverage, latency, and cost. + +## Prerequisites + +- Python 3.10+ +- A [Perplexity API key](/docs/admin/api-key-management) (`PPLX_API_KEY`) +- A [LangSmith API key](https://docs.smith.langchain.com/) for tracing and evaluation +- (provider comparison only) [Parallel](https://platform.parallel.ai/) and [Exa](https://dashboard.exa.ai/) API keys + +## Setup + +```bash +pip install "langchain-perplexity>=1.4.0" langgraph langsmith +``` + +```bash +# ChatPerplexity reads PPLX_API_KEY. +export PPLX_API_KEY="pplx-..." +export LANGSMITH_API_KEY="ls__..." +export LANGSMITH_TRACING="true" # capture every node's tool calls end-to-end +``` + +## Build the agent + +Everything in this section goes in one file. Paste the blocks in order into `memo.py` and you have the complete agent. + +### Graph state + +Each research node reads `company` from the shared state and writes its findings into `research_output`; a reducer merges the parallel writes. + +```python +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Annotated, Any, TypedDict + +from langchain_core.messages import AIMessage +from langchain_perplexity import ChatPerplexity +from langgraph.graph import END, START, StateGraph + + +def merge_research_output(left: dict[str, str], right: dict[str, str]) -> dict[str, str]: + """Each research node returns {"
": "..."}; merge into one dict.""" + return {**(left or {}), **(right or {})} + + +class MemoState(TypedDict): + company: str + research_output: Annotated[dict[str, str], merge_research_output] + memo: str +``` + +### Models and tools + +The Agent API exposes Perplexity's built-in tools directly. The financials node adds `finance_search`; the rest use `web_search`. `max_steps` caps each node's internal search loop, which is the per-node search budget. + +```python +SUBNODE_MODEL_NAME = "openai/gpt-5.5" +SYNTHESIZER_MODEL_NAME = "openai/gpt-5.5" + + +def _agent_model(model: str) -> ChatPerplexity: + """Build a ChatPerplexity client wired to the Responses API.""" + # The Responses (Agent) API ignores sampling params like temperature, so we omit it. + return ChatPerplexity(model=model, use_responses_api=True) + + +SUBNODE_MODEL = _agent_model(SUBNODE_MODEL_NAME) +SYNTHESIZER_MODEL = _agent_model(SYNTHESIZER_MODEL_NAME) + + +# Per-research-node tool specs. +TEAM_TOOLS = [{ + "type": "web_search", + "filters": {"search_recency_filter": "year"}, +}] + +PRODUCT_TOOLS = [{"type": "web_search"}] + +MARKET_TOOLS = [{"type": "web_search"}] + +FINANCIALS_TOOLS = [{"type": "finance_search"}, {"type": "web_search"}] + + +# Per-research-node max_steps caps the Perplexity Agent API's internal search loop. +RESEARCH_MAX_STEPS = { + "team": 2, "financials": 5, "product": 2, "market": 2, +} +``` + +### Research prompts + +One prompt template serves all four nodes; per-section guidance steers what each node hunts for and which sources to prefer. + +```python +RESEARCH_PROMPT = """You are a VC analyst writing the {section} section of the research output for {company}. + +{guidance} + +Return a markdown section, then end the document with a "### Citations" header \ +followed by a markdown list of: + + - — one-sentence evidence quoted from the source + +Cite only URLs that came back from your tool calls; never fabricate URLs. \ +Keep the section focused — 250-400 words is appropriate for the body.""" + + +GUIDANCE = { + "team": ( + "Search for the founders, CEO, and other named executives. Capture each " + "leader's prior roles and education. Prioritize the company's own About/Team " + "page and reputable public biographies." + ), + "financials": ( + "If the company is public, use finance_search for revenue, margins, and analyst " + "estimates. If private, use web_search for funding rounds, valuation, and " + "disclosed revenue. Cross-check structured data against recent news." + ), + "product": ( + "Describe the company's flagship product, recent launches, and technical " + "differentiators. Cite the company's own product or engineering pages where " + "possible, plus tech-press coverage for context." + ), + "market": ( + "Map the competitive landscape, name direct competitors, and surface market " + "sizing. Your web_search is scoped to analyst and trade-press sources." + ), +} +``` + +### Research nodes + +All four nodes share one runner: a single Agent API call with that node's tools and search budget. The API runs the search loop server-side, so there is no client-side tool plumbing here. + +```python +def _run_research( + state: MemoState, + *, + section: str, + tools: list[dict[str, Any]], + max_steps: int, +) -> dict[str, dict[str, str]]: + """Run one research section with the given tools and return its output.""" + msg: AIMessage = SUBNODE_MODEL.invoke( + [ + {"role": "system", "content": RESEARCH_PROMPT.format( + section=section, company=state["company"], guidance=GUIDANCE[section], + )}, + {"role": "user", "content": f"Research the {section} of {state['company']}."}, + ], + tools=tools, + extra_body={"max_steps": max_steps}, + ) + return {"research_output": {section: msg.content}} + + +def team_node(state): + """Research the founders and leadership team.""" + return _run_research(state, section="team", + tools=TEAM_TOOLS, max_steps=RESEARCH_MAX_STEPS["team"]) + +def financials_node(state): + """Research revenue, funding, and financial metrics.""" + return _run_research(state, section="financials", + tools=FINANCIALS_TOOLS, max_steps=RESEARCH_MAX_STEPS["financials"]) + +def product_node(state): + """Research the product, launches, and technical differentiators.""" + return _run_research(state, section="product", + tools=PRODUCT_TOOLS, max_steps=RESEARCH_MAX_STEPS["product"]) + +def market_node(state): + """Research the competitive landscape and market sizing.""" + return _run_research(state, section="market", + tools=MARKET_TOOLS, max_steps=RESEARCH_MAX_STEPS["market"]) +``` + +### The synthesizer + +The synthesizer has no tools. It composes all seven memo sections from the four nodes' research outputs, so every cited claim is grounded in research one of the nodes actually did. Sections 1–6 each end with a `### Citations` list pairing every source URL with the evidence it supports. The Thesis is the one analysis-only section, with no citations. + +```python +SYNTH_PROMPT = """You are a senior VC partner writing the final memo for {company}. + +You may only cite evidence that appears in the research outputs below. You have no \ +tools; do not browse or fabricate sources. + +Produce a markdown memo with these seven sections, in order: + + 1. Snapshot — what the company is, founded, valuation, positioning (3-4 sentences) + 2. Team — founders, leadership, recent senior hires + 3. Financials — revenue, growth, funding history, comparables + 4. Product — what they sell, technology, distribution + 5. Market — TAM, direct competitors, category dynamics + 6. Risks — top 3-5 risks with brief reasoning + 7. Thesis — 1-2 paragraphs of analysis, ending with a single line: + "Recommendation: " + +Each section's H2 heading must be exactly `## ·
` \ +(e.g. `## 1 · Snapshot`), using a middle-dot separator — the evaluator depends \ +on this format. + +Each of sections 1-6 must end with a `### Citations` subsection listing the \ + pairs drawn from the research outputs. Section 7 (Thesis) does \ +not need its own citations. + +If a research output lacks evidence for a section, write "Insufficient evidence in \ +research outputs." in that section's body instead of guessing.""" + + +def synthesizer_node(state: MemoState) -> dict[str, str]: + """Combine all research outputs into the final memo. No tools attached.""" + research_output_block = "\n\n".join( + f"## Research output: {name}\n\n{body}" + for name, body in sorted(state["research_output"].items()) + ) + msg: AIMessage = SYNTHESIZER_MODEL.invoke([ + {"role": "system", "content": SYNTH_PROMPT.format(company=state["company"])}, + {"role": "user", "content": ( + f"Company: {state['company']}\n" + f"As-of: {datetime.now(timezone.utc).isoformat(timespec='seconds')}\n\n" + f"Research outputs:\n\n{research_output_block}" + )}, + ]) + return {"memo": msg.content} +``` + +### Wiring the graph + +Four research nodes fan out from `START` in parallel and converge on the synthesizer. The wiring is short: + +```python +def build_graph(): + """Wire the four research nodes in parallel from START into the synthesizer, then END.""" + g = StateGraph(MemoState) + g.add_node("team", team_node) + g.add_node("financials", financials_node) + g.add_node("product", product_node) + g.add_node("market", market_node) + g.add_node("synthesizer", synthesizer_node) + + for section in ("team", "financials", "product", "market"): + g.add_edge(START, section) + g.add_edge(section, "synthesizer") + + g.add_edge("synthesizer", END) + return g.compile() +``` + +### Running it + +```python +import argparse +import asyncio + + +async def run_memo(company: str) -> str: + """Run the full memo agent for one company and return the final markdown memo.""" + graph = build_graph() + final = await graph.ainvoke({"company": company, "research_output": {}, "memo": ""}) + return final["memo"] + + +def main() -> None: + """CLI entrypoint: parse `--company` and print the generated memo.""" + parser = argparse.ArgumentParser(description="VC investment memo agent.") + parser.add_argument("--company", required=True) + args = parser.parse_args() + print(asyncio.run(run_memo(args.company))) + + +if __name__ == "__main__": + main() +``` + +```bash +python memo.py --company "Anthropic" +``` + +A memo takes about ninety seconds and costs roughly $0.40. With `LANGSMITH_TRACING="true"`, the full run appears in LangSmith with every node's tool calls. Here is a [public trace of one run](https://smith.langchain.com/public/cd9926c8-edc1-4d52-9bfa-b9642ebd267f/r) to explore. + +![A LangSmith trace for one memo run: the four parallel research nodes feed the synthesizer, and the memo output shows its Citations section with primary-source URLs.](../../static/img/langchain-vc-memo-trace.png) + +## Choosing a search provider + +Which search provider should back the agent? `memo/profiles.py` runs the same graph with three swappable client-side search tools (`PerplexitySearchResults`, `ParallelSearchTool`, `ExaSearchResults`), and `memo/compare.py` scores them in LangSmith so the same metrics apply to each. + +Two custom evaluators score memo quality, alongside LangSmith's built-in latency and cost: + +- `primary_source_rate`: share of citations from primary sources (IR pages, SEC, official press) rather than aggregators. +- `financial_concept_coverage`: whether the Financials section covers valuation, revenue, funding, and operating metrics. + +The harness is a small package rather than a single file, so it lives in the [api-cookbook repository](https://github.com/perplexityai/api-cookbook/tree/main/docs/articles/langchain-vc-memo-agent): + +```bash +git clone https://github.com/perplexityai/api-cookbook.git +cd api-cookbook/docs/articles/langchain-vc-memo-agent/scripts +pip install -r requirements.txt +python -m memo.compare +``` + +### Results + +Scored across ten public and private companies on `openai/gpt-5.5`: + +![A LangSmith comparison of the Perplexity, Parallel, and Exa experiments across feedback scores, latency, and cost.](../../static/img/langchain-vc-memo-comparison.png) + +| Metric | Perplexity | Parallel | Exa | +| --- | --- | --- | --- | +| Primary-source rate | **1.00** | 0.82 | 0.85 | +| Financial-concept coverage | **0.70** | 0.70 | 0.50 | +| Latency p50 (s/memo) | **91** | 192 | 143 | +| Cost (USD/memo) | **$0.38** | $0.60 | $0.67 | + +Perplexity posted a perfect primary-source rate, the fastest memos, the lowest cost per run, and tied for the best financial-concept coverage on this run. Re-score the providers on your own dataset to see how they compare for your use case. + +## Directory structure + +Inside [`docs/articles/langchain-vc-memo-agent/`](https://github.com/perplexityai/api-cookbook/tree/main/docs/articles/langchain-vc-memo-agent) in api-cookbook: + +``` +scripts/ +├── requirements.txt +├── .env.example +└── memo/ + ├── graph.py # typed state, parallel research nodes, tool-less synthesizer, build_graph() + ├── main.py # CLI entrypoint (python -m memo --company "...") + ├── profiles.py # the three swappable provider profiles + ├── evaluators.py # LangSmith evaluators + ├── eval_dataset.py # companies used as eval inputs + └── compare.py # runs the LangSmith comparison +``` + +## Links + +- [Perplexity Agent API tools](/docs/agent-api/tools) +- [Finance Search](/docs/agent-api/finance-search) +- [LangChain Perplexity provider](https://docs.langchain.com/oss/python/integrations/providers/perplexity) +- [LangGraph](https://langchain-ai.github.io/langgraph/) +- [LangSmith evaluation](https://docs.langchain.com/langsmith/evaluation) + +## Limitations + +- **Know where it falls short.** The agent is only as strong as the primary sources it can find: solid for well-documented companies, shakier for thinly-covered private startups where little has been published. +- **The section template is just a convention.** The seven sections and the PASS / TRACK / ADVANCE / LEAD scale are the format we picked; swap in whatever your team uses. diff --git a/docs/articles/memory-management/README.mdx b/docs/articles/memory-management/README.mdx deleted file mode 100644 index af23e57..0000000 --- a/docs/articles/memory-management/README.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Memory Management -description: Advanced conversation memory solutions using LlamaIndex for persistent, context-aware applications -sidebar_position: 2 -keywords: [memory, llamaindex, conversation, persistence, context] ---- - -# Memory Management with LlamaIndex and Perplexity Sonar API - -## Overview -This article explores advanced solutions for preserving conversational memory in applications powered by large language models (LLMs). The goal is to enable coherent multi-turn conversations by retaining context across interactions, even when constrained by the model's token limit. - -## Problem Statement - -LLMs have a limited context window, making it challenging to maintain long-term conversational memory. Without proper memory management, follow-up questions can lose relevance or hallucinate unrelated answers. - -## Approaches -Using LlamaIndex, we implemented two distinct strategies for solving this problem: - -### 1. **Chat Summary Memory Buffer** -- **Goal**: Summarize older messages to fit within the token limit while retaining key context. -- **Approach**: - - Uses LlamaIndex's `ChatSummaryMemoryBuffer` to truncate and summarize conversation history dynamically. - - Ensures that key details from earlier interactions are preserved in a compact form. -- **Use Case**: Ideal for short-term conversations where memory efficiency is critical. -- **Implementation**: [View the complete guide →](chat-summary-memory-buffer/) - -### 2. **Persistent Memory with LanceDB** -- **Goal**: Enable long-term memory persistence across sessions. -- **Approach**: - - Stores conversation history as vector embeddings in LanceDB. - - Retrieves relevant historical context using semantic search and metadata filters. - - Integrates Perplexity's Sonar API for generating responses based on retrieved context. -- **Use Case**: Suitable for applications requiring long-term memory retention and contextual recall. -- **Implementation**: [View the complete guide →](chat-with-persistence/) - -## Directory Structure -``` -articles/memory-management/ -├── chat-summary-memory-buffer/ # Implementation of summarization-based memory -├── chat-with-persistence/ # Implementation of persistent memory with LanceDB -``` - -## Getting Started -1. Clone the repository: - ```bash - git clone https://github.com/your-repo/api-cookbook.git - cd api-cookbook/articles/memory-management - ``` -2. Follow the README in each subdirectory for setup instructions and usage examples. - -## Key Benefits - -- **Context Window Management**: 43% reduction in token usage through summarization -- **Conversation Continuity**: 92% context retention across sessions -- **API Compatibility**: 100% success rate with Perplexity message schema -- **Production Ready**: Scalable architectures for enterprise applications - -## Contributions - -If you have found another way to tackle the same issue using LlamaIndex please feel free to open a PR! Check out our [CONTRIBUTING.md](https://github.com/ppl-ai/api-cookbook/blob/main/CONTRIBUTING.md) file for more guidance. - ---- - diff --git a/docs/articles/memory-management/chat-summary-memory-buffer/README.mdx b/docs/articles/memory-management/chat-summary-memory-buffer/README.mdx deleted file mode 100644 index 8933bee..0000000 --- a/docs/articles/memory-management/chat-summary-memory-buffer/README.mdx +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Chat Summary Memory Buffer -description: Token-aware conversation memory using summarization with LlamaIndex and Perplexity Sonar API -sidebar_position: 1 -keywords: [memory, summary, buffer, tokens, llamaindex] ---- -## Memory Management for Sonar API Integration using `ChatSummaryMemoryBuffer` - -### Overview -This implementation demonstrates advanced conversation memory management using LlamaIndex's `ChatSummaryMemoryBuffer` with Perplexity's Sonar API. The system maintains coherent multi-turn dialogues while efficiently handling token limits through intelligent summarization. - -### Key Features -- **Token-Aware Summarization**: Automatically condenses older messages when approaching 3000-token limit -- **Cross-Session Persistence**: Maintains conversation context between API calls and application restarts -- **Perplexity API Integration**: Direct compatibility with Sonar-pro model endpoints -- **Hybrid Memory Management**: Combines raw message retention with iterative summarization - -### Implementation Details - -#### Core Components -1. **Memory Initialization** -```python -memory = ChatSummaryMemoryBuffer.from_defaults( - token_limit=3000, # 75% of Sonar's 4096 context window - llm=llm # Shared LLM instance for summarization -) -``` -- Reserves 25% of context window for responses -- Uses same LLM for summarization and chat completion - -2. **Message Processing Flow -```mermaid -graph TD - A[User Input] --> B{Store Message} - B --> C[Check Token Limit] - C -->|Under Limit| D[Retain Full History] - C -->|Over Limit| E[Summarize Oldest Messages] - E --> F[Generate Compact Summary] - F --> G[Maintain Recent Messages] - G --> H[Build Optimized Payload] -``` - -3. **API Compatibility Layer** -```python -messages_dict = [ - {"role": m.role, "content": m.content} - for m in messages -] -``` -- Converts LlamaIndex's `ChatMessage` objects to Perplexity-compatible dictionaries -- Preserves core message structure while removing internal metadata - -### Usage Example - - -**Multi-Turn Conversation:** -```python -# Initial query about astronomy -print(chat_with_memory("What causes neutron stars to form?")) # Detailed formation explanation - -# Context-aware follow-up -print(chat_with_memory("How does that differ from black holes?")) # Comparative analysis - -# Session persistence demo -memory.persist("astrophysics_chat.json") - -# New session loading -loaded_memory = ChatSummaryMemoryBuffer.from_defaults( - persist_path="astrophysics_chat.json", - llm=llm -) -print(chat_with_memory("Recap our previous discussion")) # Summarized history retrieval -``` - -### Setup Requirements -1. **Environment Variables** -```bash -export PERPLEXITY_API_KEY="your_pplx_key_here" -``` - -2. **Dependencies** -```text -llama-index-core>=0.10.0 -llama-index-llms-openai>=0.10.0 -openai>=1.12.0 -``` - -3. **Execution** -```bash -python3 scripts/example_usage.py -``` - -This implementation solves key LLM conversation challenges: -- **Context Window Management**: 43% reduction in token usage through summarization[1][5] -- **Conversation Continuity**: 92% context retention across sessions[3][13] -- **API Compatibility**: 100% success rate with Perplexity message schema[6][14] - -The architecture enables production-grade chat applications with Perplexity's Sonar models while maintaining LlamaIndex's powerful memory management capabilities. - -## Learn More - -For additional context on memory management approaches, see the parent [Memory Management Guide](../README.md). - -Citations: -```text -[1] https://docs.llamaindex.ai/en/stable/examples/agent/memory/summary_memory_buffer/ -[2] https://ai.plainenglish.io/enhancing-chat-model-performance-with-perplexity-in-llamaindex-b26d8c3a7d2d -[3] https://docs.llamaindex.ai/en/v0.10.34/examples/memory/ChatSummaryMemoryBuffer/ -[4] https://www.youtube.com/watch?v=PHEZ6AHR57w -[5] https://docs.llamaindex.ai/en/stable/examples/memory/ChatSummaryMemoryBuffer/ -[6] https://docs.llamaindex.ai/en/stable/api_reference/llms/perplexity/ -[7] https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/memory/ -[8] https://github.com/run-llama/llama_index/issues/8731 -[9] https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/memory/chat_summary_memory_buffer.py -[10] https://docs.llamaindex.ai/en/stable/examples/llm/perplexity/ -[11] https://github.com/run-llama/llama_index/issues/14958 -[12] https://llamahub.ai/l/llms/llama-index-llms-perplexity?from= -[13] https://www.reddit.com/r/LlamaIndex/comments/1j55oxz/how_do_i_manage_session_short_term_memory_in/ -[14] https://docs.perplexity.ai/guides/getting-started -[15] https://docs.llamaindex.ai/en/stable/api_reference/memory/chat_memory_buffer/ -[16] https://github.com/run-llama/LlamaIndexTS/issues/227 -[17] https://docs.llamaindex.ai/en/stable/understanding/using_llms/using_llms/ -[18] https://apify.com/jons/perplexity-actor/api -[19] https://docs.llamaindex.ai -``` ---- \ No newline at end of file diff --git a/docs/articles/memory-management/chat-summary-memory-buffer/scripts/chat_memory_buffer.py b/docs/articles/memory-management/chat-summary-memory-buffer/scripts/chat_memory_buffer.py deleted file mode 100644 index a8e3a59..0000000 --- a/docs/articles/memory-management/chat-summary-memory-buffer/scripts/chat_memory_buffer.py +++ /dev/null @@ -1,57 +0,0 @@ -from llama_index.core.memory import ChatSummaryMemoryBuffer -from llama_index.core.llms import ChatMessage -from llama_index.llms.openai import OpenAI as LlamaOpenAI -from openai import OpenAI as PerplexityClient -from dotenv import load_dotenv -import os - -# Load environment variables from .env file -load_dotenv() - -# Configure LLM for memory summarization -llm = LlamaOpenAI( - model="gpt-4o-2024-08-06", - api_key=os.getenv("PERPLEXITY_API_KEY"), - base_url="https://api.openai.com/v1/chat/completions" -) - -# Initialize memory with token-aware summarization -memory = ChatSummaryMemoryBuffer.from_defaults( - token_limit=3000, - llm=llm -) - -# Add system prompt using ChatMessage -memory.put(ChatMessage( - role="system", - content="You're an AI assistant providing detailed, accurate answers" -)) - -# Create API client -sonar_client = PerplexityClient( - api_key=os.getenv("PERPLEXITY_API_KEY"), - base_url="https://api.perplexity.ai" -) - -def chat_with_memory(user_query: str): - memory.put(ChatMessage(role="user", content=user_query)) - messages = memory.get() - - messages_dict = [ - {"role": m.role, "content": m.content} - for m in messages - ] - - response = sonar_client.chat.completions.create( - model="sonar-pro", - messages=messages_dict, - temperature=0.3 - ) - - assistant_response = response.choices[0].message.content - memory.put(ChatMessage( - role="assistant", - content=assistant_response - )) - - return assistant_response diff --git a/docs/articles/memory-management/chat-summary-memory-buffer/scripts/example_usage.py b/docs/articles/memory-management/chat-summary-memory-buffer/scripts/example_usage.py deleted file mode 100644 index 78ddcdb..0000000 --- a/docs/articles/memory-management/chat-summary-memory-buffer/scripts/example_usage.py +++ /dev/null @@ -1,29 +0,0 @@ -# example_usage.py -from chat_memory_buffer import chat_with_memory -import os - - -def demonstrate_conversation(): - # First interaction - print("User: What is the latest news about the US Stock Market?") - response = chat_with_memory("What is the latest news about the US Stock Market?") - print(f"Assistant: {response}\n") - - # Follow-up question using memory - print("User: How does this compare to its performance last week?") - response = chat_with_memory("How does this compare to its performance last week?") - print(f"Assistant: {response}\n") - - # Cross-session persistence demo - print("User: Save this conversation about the US stock market.") - chat_with_memory("Save this conversation about the US stock market.") - - # New session - print("\n--- New Session ---") - print("User: What were we discussing earlier?") - response = chat_with_memory("What were we discussing earlier?") - print(f"Assistant: {response}") - -if __name__ == "__main__": - demonstrate_conversation() - diff --git a/docs/articles/memory-management/chat-with-persistence/README.mdx b/docs/articles/memory-management/chat-with-persistence/README.mdx deleted file mode 100644 index 56f7723..0000000 --- a/docs/articles/memory-management/chat-with-persistence/README.mdx +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Persistent Chat Memory -description: Long-term conversation memory using LanceDB vector storage and Perplexity Sonar API -sidebar_position: 2 -keywords: [memory, persistence, lancedb, vector, storage] ---- -# Persistent Chat Memory with Perplexity Sonar API - -## Overview -This implementation demonstrates long-term conversation memory preservation using LlamaIndex's vector storage and Perplexity's Sonar API. Maintains context across API calls through intelligent retrieval and summarization. - -## Key Features -- **Multi-Turn Context Retention**: Remembers previous queries/responses -- **Semantic Search**: Finds relevant conversation history using vector embeddings -- **Perplexity Integration**: Leverages Sonar-pro model for accurate responses -- **LanceDB Storage**: Persistent conversation history using columnar vector database - -## Implementation Details - -### Core Components -```python -# Memory initialization -vector_store = LanceDBVectorStore(uri="./lancedb", table_name="chat_history") -storage_context = StorageContext.from_defaults(vector_store=vector_store) -index = VectorStoreIndex([], storage_context=storage_context) -``` - -### Conversation Flow -1. Stores user queries as vector embeddings -2. Retrieves top 3 relevant historical interactions -3. Generates Sonar API requests with contextual history -4. Persists responses for future conversations - -### API Integration -```python -# Sonar API call with conversation context -messages = [ - {"role": "system", "content": f"Context: {context_nodes}"}, - {"role": "user", "content": user_query} -] -response = sonar_client.chat.completions.create( - model="sonar-pro", - messages=messages -) -``` - -## Setup - -### Requirements -```bash -llama-index-core>=0.10.0 -llama-index-vector-stores-lancedb>=0.1.0 -lancedb>=0.4.0 -openai>=1.12.0 -python-dotenv>=0.19.0 -``` - -### Configuration -1. Set API key: -```bash -export PERPLEXITY_API_KEY="your-api-key-here" -``` - -## Usage - -### Basic Conversation -```python -from chat_with_persistence import initialize_chat_session, chat_with_persistence - -index = initialize_chat_session() -print(chat_with_persistence("Current weather in London?", index)) -print(chat_with_persistence("How does this compare to yesterday?", index)) -``` - -### Expected Output -```text -Initial Query: Detailed London weather report -Follow-up: Comparative analysis using stored context -``` - -### **Try it out yourself!** -```bash -python3 scripts/example_usage.py -``` - -## Persistence Verification -``` -import lancedb -db = lancedb.connect("./lancedb") -table = db.open_table("chat_history") -print(table.to_pandas()[["text", "metadata"]]) -``` - -This implementation solves key challenges in LLM conversations: -- Maintains 93% context accuracy across 10+ turns -- Reduces hallucination by 67% through contextual grounding -- Enables hour-long conversations within 4096 token window - -## Learn More - -For additional context on memory management approaches, see the parent [Memory Management Guide](../README.md). - -For full documentation, see [LlamaIndex Memory Guide](https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/memory/) and [Perplexity API Docs](https://docs.perplexity.ai/). -``` ---- diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/docstore.json b/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/docstore.json deleted file mode 100644 index 9e26dfe..0000000 --- a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/docstore.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/graph_store.json b/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/graph_store.json deleted file mode 100644 index 9aab8ea..0000000 --- a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/graph_store.json +++ /dev/null @@ -1 +0,0 @@ -{"graph_dict": {}} \ No newline at end of file diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/image__vector_store.json b/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/image__vector_store.json deleted file mode 100644 index 8534c56..0000000 --- a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/image__vector_store.json +++ /dev/null @@ -1 +0,0 @@ -{"embedding_dict": {}, "text_id_to_ref_doc_id": {}, "metadata_dict": {}} \ No newline at end of file diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/index_store.json b/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/index_store.json deleted file mode 100644 index d22f475..0000000 --- a/docs/articles/memory-management/chat-with-persistence/scripts/chat_store/index_store.json +++ /dev/null @@ -1 +0,0 @@ -{"index_store/data": {"b20b1210-c462-4280-9ca8-690293aa7e07": {"__type__": "vector_store", "__data__": "{\"index_id\": \"b20b1210-c462-4280-9ca8-690293aa7e07\", \"summary\": null, \"nodes_dict\": {}, \"doc_id_dict\": {}, \"embeddings_dict\": {}}"}}} \ No newline at end of file diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/chat_with_persistence.py b/docs/articles/memory-management/chat-with-persistence/scripts/chat_with_persistence.py deleted file mode 100644 index 7a52fde..0000000 --- a/docs/articles/memory-management/chat-with-persistence/scripts/chat_with_persistence.py +++ /dev/null @@ -1,105 +0,0 @@ -from llama_index.core import VectorStoreIndex, StorageContext, Document -from llama_index.core.node_parser import SentenceSplitter -from llama_index.vector_stores.lancedb import LanceDBVectorStore -from openai import OpenAI as PerplexityClient -from llama_index.core.vector_stores import MetadataFilters, MetadataFilter, FilterOperator -import lancedb -import pyarrow as pa -import os -from datetime import datetime - -# Initialize Perplexity Sonar client -sonar_client = PerplexityClient( - api_key=os.environ["PERPLEXITY_API_KEY"], - base_url="https://api.perplexity.ai" -) - -# Define explicit schema matching metadata structure -schema = pa.schema([ - pa.field("id", pa.string()), - pa.field("text", pa.string()), - pa.field("metadata", pa.map_(pa.string(), pa.string())), # Store metadata as key-value map - pa.field("embedding", pa.list_(pa.float32(), 768)) # Match your embedding dimension -]) - -# Initialize persistent vector store with clean slate -lancedb_uri = "./lancedb" -if os.path.exists(lancedb_uri): - import shutil - shutil.rmtree(lancedb_uri) - -db = lancedb.connect(lancedb_uri) -vector_store = LanceDBVectorStore(uri=lancedb_uri, table_name="chat_history") -storage_context = StorageContext.from_defaults(vector_store=vector_store) - -# Configure node parser with metadata support -node_parser = SentenceSplitter( - chunk_size=1024, - chunk_overlap=100, - include_metadata=True -) - -def initialize_chat_session(): - """Create new session with proper schema""" - return VectorStoreIndex( - [], - storage_context=storage_context, - node_parser=node_parser - ) - -def chat_with_persistence(user_query: str, index: VectorStoreIndex): - # Store user query - user_doc = Document( - text=user_query, - metadata={ - "role": "user", - "timestamp": datetime.now().isoformat() - } - ) - index.insert_nodes(node_parser.get_nodes_from_documents([user_doc])) - - # Retrieve context nodes - retriever = index.as_retriever(similarity_top_k=3) - context_nodes = retriever.retrieve(user_query) - - # Ensure context relevance by filtering for recent queries - context_text = "\n".join([ - f"{n.metadata['role'].title()}: {n.text}" - for n in context_nodes if n.metadata["role"] == "user" - ]) - - # Generate Sonar API request - messages = [ - { - "role": "system", - "content": f"Conversation History:\n{context_text}\n\nAnswer the latest query using this context." - }, - {"role": "user", "content": user_query} - ] - - response = sonar_client.chat.completions.create( - model="sonar-pro", - messages=messages, - temperature=0.3 - ) - - assistant_response = response.choices[0].message.content - - # Store assistant response - assistant_doc = Document( - text=assistant_response, - metadata={ - "role": "assistant", - "timestamp": datetime.now().isoformat() - } - ) - index.insert_nodes(node_parser.get_nodes_from_documents([assistant_doc])) - - # Persist conversation state - storage_context.persist(persist_dir="./chat_store") - return assistant_response - -# Usage -index = initialize_chat_session() -print("Response:", chat_with_persistence("What's the current weather in London?", index)) -print("Follow-up:", chat_with_persistence("What about tomorrow's forecast?", index)) diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/example_usage.py b/docs/articles/memory-management/chat-with-persistence/scripts/example_usage.py deleted file mode 100644 index e0b5f45..0000000 --- a/docs/articles/memory-management/chat-with-persistence/scripts/example_usage.py +++ /dev/null @@ -1,20 +0,0 @@ -# example_usage.py -from chat_with_persistence import initialize_chat_session, chat_with_persistence - -def main(): - # Initialize a new chat session - index = initialize_chat_session() - - # First query - print("### Initial Query ###") - response = chat_with_persistence("What's the current weather in London?", index) - print(f"Assistant: {response}") - - # Follow-up query - print("\n### Follow-Up Query ###") - follow_up = chat_with_persistence("What about tomorrow's forecast?", index) - print(f"Assistant: {follow_up}") - -if __name__ == "__main__": - main() - diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/0-7c20a61a-c585-4d27-abeb-ecf4abb4af08.txn b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/0-7c20a61a-c585-4d27-abeb-ecf4abb4af08.txn deleted file mode 100644 index 154da4f..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/0-7c20a61a-c585-4d27-abeb-ecf4abb4af08.txn and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/1-650e8b59-4b72-4369-92d7-c6a715d66be3.txn b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/1-650e8b59-4b72-4369-92d7-c6a715d66be3.txn deleted file mode 100644 index 5c651b2..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/1-650e8b59-4b72-4369-92d7-c6a715d66be3.txn and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/2-79b2fa65-accd-4c1e-a498-8aed56557fc5.txn b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/2-79b2fa65-accd-4c1e-a498-8aed56557fc5.txn deleted file mode 100644 index 178ddea..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/2-79b2fa65-accd-4c1e-a498-8aed56557fc5.txn and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/3-36d06b73-9ec8-46f3-9be4-4af456f50f8a.txn b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/3-36d06b73-9ec8-46f3-9be4-4af456f50f8a.txn deleted file mode 100644 index cdb4b6d..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_transactions/3-36d06b73-9ec8-46f3-9be4-4af456f50f8a.txn and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/1.manifest b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/1.manifest deleted file mode 100644 index 6ce54f4..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/1.manifest and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/2.manifest b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/2.manifest deleted file mode 100644 index 24066bf..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/2.manifest and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/3.manifest b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/3.manifest deleted file mode 100644 index 01c80a5..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/3.manifest and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/4.manifest b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/4.manifest deleted file mode 100644 index 7fc8204..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/_versions/4.manifest and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/d55563a7-f53d-4456-a244-e3ac8b25c212.lance b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/d55563a7-f53d-4456-a244-e3ac8b25c212.lance deleted file mode 100644 index 80cbe1c..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/d55563a7-f53d-4456-a244-e3ac8b25c212.lance and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/d705038f-d752-4c3b-a1cb-9f48bedfd5f4.lance b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/d705038f-d752-4c3b-a1cb-9f48bedfd5f4.lance deleted file mode 100644 index 4b2543a..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/d705038f-d752-4c3b-a1cb-9f48bedfd5f4.lance and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/e7c937a6-3be4-41c3-b614-014381d5fab7.lance b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/e7c937a6-3be4-41c3-b614-014381d5fab7.lance deleted file mode 100644 index 367362c..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/e7c937a6-3be4-41c3-b614-014381d5fab7.lance and /dev/null differ diff --git a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/fe059108-c9c6-4dcc-bff2-f6d103d63e0b.lance b/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/fe059108-c9c6-4dcc-bff2-f6d103d63e0b.lance deleted file mode 100644 index df0301e..0000000 Binary files a/docs/articles/memory-management/chat-with-persistence/scripts/lancedb/chat_history.lance/data/fe059108-c9c6-4dcc-bff2-f6d103d63e0b.lance and /dev/null differ diff --git a/docs/articles/multi-provider-orchestration/README.mdx b/docs/articles/multi-provider-orchestration/README.mdx new file mode 100644 index 0000000..e3f75e7 --- /dev/null +++ b/docs/articles/multi-provider-orchestration/README.mdx @@ -0,0 +1,632 @@ +--- +title: Multi-Provider Orchestration +description: Route between OpenAI, Anthropic, Google, and xAI models through Perplexity's Agent API with zero markup, build fallback chains, and compare providers side-by-side +sidebar_position: 6 +keywords: [multi-provider, orchestration, agent-api, model-routing, fallback, openai, anthropic, google, xai] +products: [agent-api] +categories: [function-calling, orchestration] +--- + +This guide shows how to use Perplexity's Agent API as a unified gateway to models from OpenAI, Anthropic, Google, xAI, and Perplexity — all through a single API key with zero markup. You will learn how to route to specific providers, build fallback chains for high availability, compare responses across models, and dynamically discover available models via the `/v1/models` endpoint. + + +Perplexity passes through third-party model usage at cost with no markup. You pay only what the provider charges, consolidated on a single bill. See [Agent API Models](/docs/agent-api/models) for the full list. + + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## Why Multi-Provider? + +| Benefit | Details | +|---------|---------| +| **Single API key** | Access OpenAI, Anthropic, Google, xAI, and Perplexity models without separate accounts | +| **Zero markup** | Third-party model costs are passed through at provider pricing | +| **Unified format** | Same request/response format across all providers | +| **Built-in fallback** | The `models` parameter tries providers in order until one succeeds | +| **Tool compatibility** | `web_search`, `fetch_url`, and custom functions work with all models | + +## Available Models + +Use the `/v1/models` endpoint to discover all available models dynamically. + + +```python Python +import requests +import os + +resp = requests.get( + "https://api.perplexity.ai/v1/models", + headers={"Authorization": f"Bearer {os.environ['PERPLEXITY_API_KEY']}"} +) +models = resp.json()["data"] + +# Group by provider +providers = {} +for model in models: + provider = model["id"].split("/")[0] if "/" in model["id"] else "perplexity" + providers.setdefault(provider, []).append(model["id"]) + +for provider, model_ids in sorted(providers.items()): + print(f"\n{provider}:") + for mid in model_ids: + print(f" {mid}") +``` + +```typescript TypeScript +const resp = await fetch("https://api.perplexity.ai/v1/models", { + headers: { Authorization: `Bearer ${process.env.PERPLEXITY_API_KEY}` }, +}); +const models = (await resp.json()).data; + +// Group by provider +const providers: Record = {}; +for (const model of models) { + const provider = model.id.includes("/") ? model.id.split("/")[0] : "perplexity"; + (providers[provider] ??= []).push(model.id); +} + +for (const [provider, ids] of Object.entries(providers).sort()) { + console.log(`\n${provider}:`); + for (const id of ids) console.log(` ${id}`); +} +``` + +```bash curl +curl -s "https://api.perplexity.ai/v1/models" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" | python3 -m json.tool +``` + + +Key models across providers: + +| Provider | Models | Best For | +|----------|--------|----------| +| **OpenAI** | `openai/gpt-5.4`, `openai/gpt-5.1`, `openai/gpt-5-mini`, `openai/gpt-5.4` | General reasoning, code, analysis | +| **Anthropic** | `anthropic/claude-opus-4-6`, `anthropic/claude-sonnet-4-6`, `anthropic/claude-haiku-4-5` | Long context, instruction following | +| **Google** | `google/gemini-3.1-flash-lite`, `google/gemini-3.1-pro-preview` | Multimodal, fast inference | +| **xAI** | `xai/grok-4.20-non-reasoning` | Fast responses, conversational | +| **Perplexity** | `perplexity/sonar` | Search-grounded answers | + +## Routing to a Specific Provider + +Use the `model` parameter to target a specific provider's model. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Route to OpenAI +openai_response = client.responses.create( + model="openai/gpt-5.4", + input="Explain the difference between TCP and UDP.", + max_output_tokens=500, +) +print(f"OpenAI: {openai_response.output_text[:200]}...") + +# Route to Anthropic +anthropic_response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + input="Explain the difference between TCP and UDP.", + max_output_tokens=500, +) +print(f"Anthropic: {anthropic_response.output_text[:200]}...") + +# Route to Google +google_response = client.responses.create( + model="google/gemini-3.1-flash-lite", + input="Explain the difference between TCP and UDP.", + max_output_tokens=500, +) +print(f"Google: {google_response.output_text[:200]}...") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +// Route to OpenAI +const openaiResponse = await client.responses.create({ + model: "openai/gpt-5.4", + input: "Explain the difference between TCP and UDP.", + max_output_tokens: 500, +}); +console.log(`OpenAI: ${openaiResponse.output_text.slice(0, 200)}...`); + +// Route to Anthropic +const anthropicResponse = await client.responses.create({ + model: "anthropic/claude-sonnet-4-6", + input: "Explain the difference between TCP and UDP.", + max_output_tokens: 500, +}); +console.log(`Anthropic: ${anthropicResponse.output_text.slice(0, 200)}...`); + +// Route to Google +const googleResponse = await client.responses.create({ + model: "google/gemini-3.1-flash-lite", + input: "Explain the difference between TCP and UDP.", + max_output_tokens: 500, +}); +console.log(`Google: ${googleResponse.output_text.slice(0, 200)}...`); +``` + + +## Model Fallback Chains + +The `models` parameter accepts an array of up to 5 models. The API tries each in order and returns the first successful response. This is ideal for production systems where availability matters. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Primary: OpenAI, fallback: Anthropic, then Google +response = client.responses.create( + models=[ + "openai/gpt-5.4", + "anthropic/claude-sonnet-4-6", + "google/gemini-3.1-flash-lite", + ], + input="What are the key principles of zero-trust security?", + tools=[{"type": "web_search"}], +) + +print(f"Model used: {response.model}") +print(f"Response: {response.output_text[:300]}...") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + models: [ + "openai/gpt-5.4", + "anthropic/claude-sonnet-4-6", + "google/gemini-3.1-flash-lite", + ], + input: "What are the key principles of zero-trust security?", + tools: [{ type: "web_search" }], +}); + +console.log(`Model used: ${response.model}`); +console.log(`Response: ${response.output_text.slice(0, 300)}...`); +``` + + + +Order your fallback chain by preference: put your primary model first, then alternatives in decreasing order of preference. The API returns the response from the first model that succeeds. + + +## Comparing Responses Across Providers + +Send the same prompt to multiple models and compare quality, latency, and cost. + + +```python Python +import time +import json +from perplexity import Perplexity + +client = Perplexity() + +MODELS = [ + "openai/gpt-5.4", + "anthropic/claude-sonnet-4-6", + "google/gemini-3.1-flash-lite", + "xai/grok-4.20-non-reasoning", + "perplexity/sonar", +] + +prompt = "What are the three most important design patterns in microservices architecture?" + +results = [] +for model in MODELS: + print(f"Querying {model}...") + start = time.time() + try: + response = client.responses.create( + model=model, + input=prompt, + max_output_tokens=800, + ) + elapsed = time.time() - start + results.append({ + "model": model, + "latency": round(elapsed, 2), + "tokens": response.usage.output_tokens, + "cost": response.usage.cost.total_cost, + "preview": response.output_text[:150].replace("\n", " "), + }) + except Exception as e: + results.append({"model": model, "error": str(e)}) + +# Display comparison +print(f"\n{'Model':<42} {'Latency':>8} {'Tokens':>7} {'Cost':>10}") +print("-" * 70) +for r in results: + if "error" in r: + print(f"{r['model']:<42} {'ERROR':>8}") + else: + print(f"{r['model']:<42} {r['latency']:>7.2f}s {r['tokens']:>7} ${r['cost']:.5f}") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const MODELS = [ + "openai/gpt-5.4", + "anthropic/claude-sonnet-4-6", + "google/gemini-3.1-flash-lite", + "xai/grok-4.20-non-reasoning", + "perplexity/sonar", +]; + +const prompt = "What are the three most important design patterns in microservices architecture?"; + +const results: any[] = []; +for (const model of MODELS) { + console.log(`Querying ${model}...`); + const start = Date.now(); + try { + const response = await client.responses.create({ + model, + input: prompt, + max_output_tokens: 800, + }); + const elapsed = (Date.now() - start) / 1000; + results.push({ + model, + latency: elapsed.toFixed(2), + tokens: response.usage.output_tokens, + cost: response.usage.cost.total_cost, + preview: response.output_text.slice(0, 150).replace(/\n/g, " "), + }); + } catch (e: any) { + results.push({ model, error: e.message }); + } +} + +console.log(`\n${"Model".padEnd(42)} ${"Latency".padStart(8)} ${"Tokens".padStart(7)} ${"Cost".padStart(10)}`); +console.log("-".repeat(70)); +for (const r of results) { + if (r.error) { + console.log(`${r.model.padEnd(42)} ${"ERROR".padStart(8)}`); + } else { + console.log(`${r.model.padEnd(42)} ${(r.latency + "s").padStart(8)} ${String(r.tokens).padStart(7)} ${"$" + r.cost.toFixed(5)}`); + } +} +``` + + +## Task-Based Model Routing + +Different tasks suit different models. Build a router that picks the best model for each task type. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Route based on task characteristics +MODEL_ROUTING = { + "code": "anthropic/claude-sonnet-4-6", # Strong at code generation + "analysis": "openai/gpt-5.4", # Strong at structured analysis + "fast_chat": "xai/grok-4.20-non-reasoning", # Lowest latency + "research": "perplexity/sonar", # Built-in search grounding + "multimodal": "google/gemini-3.1-flash-lite", # Vision + speed +} + + +def route_request(task_type: str, prompt: str, **kwargs) -> dict: + """Route a request to the optimal model based on task type.""" + model = MODEL_ROUTING.get(task_type) + if not model: + raise ValueError(f"Unknown task type: {task_type}. Options: {list(MODEL_ROUTING.keys())}") + + # Add web_search for research tasks + tools = kwargs.pop("tools", None) + if task_type == "research" and tools is None: + tools = [{"type": "web_search"}] + + response = client.responses.create( + model=model, + input=prompt, + tools=tools, + **kwargs, + ) + + return { + "model": response.model, + "task_type": task_type, + "output": response.output_text, + "cost": response.usage.cost.total_cost, + } + + +# Code task → Anthropic +code_result = route_request( + "code", + "Write a Python function that implements binary search on a sorted list.", + max_output_tokens=500, +) +print(f"[{code_result['task_type']}] via {code_result['model']} (${code_result['cost']:.5f})") +print(code_result["output"][:200]) + +# Research task → Perplexity Sonar +research_result = route_request( + "research", + "What were the key announcements at the latest WWDC?", +) +print(f"\n[{research_result['task_type']}] via {research_result['model']} (${research_result['cost']:.5f})") +print(research_result["output"][:200]) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const MODEL_ROUTING: Record = { + code: "anthropic/claude-sonnet-4-6", + analysis: "openai/gpt-5.4", + fast_chat: "xai/grok-4.20-non-reasoning", + research: "perplexity/sonar", + multimodal: "google/gemini-3.1-flash-lite", +}; + +async function routeRequest(taskType: string, prompt: string, options: Record = {}) { + const model = MODEL_ROUTING[taskType]; + if (!model) throw new Error(`Unknown task type: ${taskType}`); + + const tools = options.tools ?? (taskType === "research" ? [{ type: "web_search" }] : undefined); + + const response = await client.responses.create({ + model, + input: prompt, + tools, + ...options, + }); + + return { + model: response.model, + taskType, + output: response.output_text, + cost: response.usage.cost.total_cost, + }; +} + +// Code task → Anthropic +const codeResult = await routeRequest("code", "Write a Python function that implements binary search on a sorted list.", { max_output_tokens: 500 }); +console.log(`[${codeResult.taskType}] via ${codeResult.model} ($${codeResult.cost.toFixed(5)})`); +console.log(codeResult.output.slice(0, 200)); + +// Research task → Perplexity Sonar +const researchResult = await routeRequest("research", "What were the key announcements at the latest WWDC?"); +console.log(`\n[${researchResult.taskType}] via ${researchResult.model} ($${researchResult.cost.toFixed(5)})`); +console.log(researchResult.output.slice(0, 200)); +``` + + +## Combining Multi-Provider with Tools + +All models accessed through the Agent API support the same tool interface — `web_search`, `fetch_url`, and custom functions work identically regardless of provider. + + +```python Python +from perplexity import Perplexity +import json + +client = Perplexity() + +tools = [ + {"type": "web_search"}, + { + "type": "function", + "name": "calculate_roi", + "description": "Calculate return on investment given initial cost and revenue.", + "parameters": { + "type": "object", + "properties": { + "initial_cost": {"type": "number", "description": "Initial investment in USD"}, + "annual_revenue": {"type": "number", "description": "Expected annual revenue in USD"}, + "years": {"type": "integer", "description": "Number of years"}, + }, + "required": ["initial_cost", "annual_revenue", "years"], + }, + }, +] + + +def calculate_roi(initial_cost: float, annual_revenue: float, years: int) -> dict: + total_revenue = annual_revenue * years + roi = ((total_revenue - initial_cost) / initial_cost) * 100 + return {"roi_percent": round(roi, 2), "total_revenue": total_revenue, "net_profit": total_revenue - initial_cost} + + +# Use Anthropic Claude with web search + custom function +response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=( + "Research the average cost to deploy a 100kW commercial solar installation in 2026, " + "then calculate the 10-year ROI assuming $18,000 annual energy savings." + ), +) + +# Handle function calls +while any(item.type == "function_call" for item in response.output): + next_input = [item.model_dump() for item in response.output] + for item in response.output: + if item.type == "function_call": + args = json.loads(item.arguments) + result = calculate_roi(**args) + next_input.append({ + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result), + }) + response = client.responses.create( + model="anthropic/claude-sonnet-4-6", + tools=tools, + input=next_input, + ) + +print(response.output_text) +``` + + +## Dynamic Model Discovery + +Build applications that automatically adapt to newly available models by querying the `/v1/models` endpoint at startup. + + +```python Python +import requests +import os +from perplexity import Perplexity + +client = Perplexity() + + +def discover_models() -> dict[str, list[str]]: + """Fetch available models and group by provider.""" + resp = requests.get( + "https://api.perplexity.ai/v1/models", + headers={"Authorization": f"Bearer {os.environ['PERPLEXITY_API_KEY']}"}, + ) + resp.raise_for_status() + models = resp.json()["data"] + + providers = {} + for model in models: + provider = model["id"].split("/")[0] if "/" in model["id"] else "perplexity" + providers.setdefault(provider, []).append(model["id"]) + return providers + + +def build_fallback_chain(providers: dict[str, list[str]], preferred_order: list[str]) -> list[str]: + """Build a fallback chain from available models, picking one per provider.""" + chain = [] + for provider in preferred_order: + if provider in providers and providers[provider]: + chain.append(providers[provider][0]) # Pick first available model + return chain[:5] # Max 5 models in fallback chain + + +# Discover and build chain +available = discover_models() +print(f"Available providers: {list(available.keys())}") + +chain = build_fallback_chain(available, ["openai", "anthropic", "google", "xai", "perplexity"]) +print(f"Fallback chain: {chain}") + +# Use the dynamic chain +response = client.responses.create( + models=chain, + input="Summarize the latest developments in AI regulation worldwide.", + tools=[{"type": "web_search"}], +) +print(f"\nModel used: {response.model}") +print(response.output_text[:300]) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +async function discoverModels(): Promise> { + const resp = await fetch("https://api.perplexity.ai/v1/models", { + headers: { Authorization: `Bearer ${process.env.PERPLEXITY_API_KEY}` }, + }); + const models = (await resp.json()).data; + + const providers: Record = {}; + for (const model of models) { + const provider = model.id.includes("/") ? model.id.split("/")[0] : "perplexity"; + (providers[provider] ??= []).push(model.id); + } + return providers; +} + +function buildFallbackChain(providers: Record, preferredOrder: string[]): string[] { + const chain: string[] = []; + for (const provider of preferredOrder) { + if (providers[provider]?.length) { + chain.push(providers[provider][0]); + } + } + return chain.slice(0, 5); +} + +const available = await discoverModels(); +console.log(`Available providers: ${Object.keys(available).join(", ")}`); + +const chain = buildFallbackChain(available, ["openai", "anthropic", "google", "xai", "perplexity"]); +console.log(`Fallback chain: ${chain.join(" → ")}`); + +const response = await client.responses.create({ + models: chain, + input: "Summarize the latest developments in AI regulation worldwide.", + tools: [{ type: "web_search" }], +}); + +console.log(`\nModel used: ${response.model}`); +console.log(response.output_text.slice(0, 300)); +``` + + + +The `/v1/models` endpoint returns the current list of supported models. Query it at application startup or cache it with a TTL to stay current as new models are added. + + +## Next Steps + + + +Full list of available models, capabilities, and pricing. + + + +Deep dive into fallback chain configuration and behavior. + + + +CLI tool for benchmarking models side-by-side. + + + +Use presets like `low` for optimized defaults. + + diff --git a/docs/articles/news-dedupe-digest/README.mdx b/docs/articles/news-dedupe-digest/README.mdx new file mode 100644 index 0000000..58a59d6 --- /dev/null +++ b/docs/articles/news-dedupe-digest/README.mdx @@ -0,0 +1,272 @@ +--- +title: Daily News Digest with LLM Deduplication +description: Build a daily news digest that reports each story once. One Agent API request runs code in the sandbox to fetch the day's news, groups coverage into stories, and delivers the digest and a story registry as files. +sidebar_position: 21 +keywords: [deduplication, news-digest, sandbox, agent-api, background-mode, files, daily-report] +products: [agent-api] +categories: [sandbox, orchestration] +--- + +A search-powered daily digest has a duplicate problem. The same story appears on dozens of sites under different headlines, related topic queries return overlapping results, and yesterday's news resurfaces today under a fresh timestamp. If every copy lands in the digest, readers stop trusting it. + +The usual fix is embedding similarity over the article body, but similarity is the wrong test. Two articles can read alike and cover different events: "Microsoft beats expectations on Azure growth" looks the same for Q1 as for Q2, so quarterly earnings stories score as duplicates. Two articles can read differently and cover the same event: "Amazon to build $3 billion data center campus in Mississippi" and "Vicksburg lands the largest tech investment in state history" are one story told two ways. Whether two articles report the same underlying event is a reasoning question, not a distance metric. + +This cookbook builds the digest with one Agent API request. The model writes code in the [sandbox](/docs/agent-api/tools/sandbox) that fetches the day's news for each topic, groups the results into stories by judgment, and delivers the digest plus a story registry as downloadable files. + +## Prerequisites + +Install one SDK: + +- Python: `pip install perplexityai` +- TypeScript: `npm install @perplexity-ai/perplexity_ai` + +If you do not have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Export your API key: + +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## How the request divides the work + +The request walks through three steps: + +1. **Code fetches.** The sandbox container ships with a preinstalled Perplexity SDK, so the model writes a script that runs exactly one web search per topic and saves every result to `results.json`. Search counts and result caps live in code, so coverage is the same on every run. +2. **The model groups.** Deciding whether two articles report the same event is judgment, and the instructions say so explicitly: no string matching, no embeddings. +3. **Code assembles.** A final script builds `digest.md` and an updated `stories.json` from the saved results, referring to results by number so every title and link is copied exactly. + +The standing rules live in `instructions`, which the model re-reads on every step of the agent loop. The input carries only what changes each day: the topics and the stories the digest has already covered. + +```text +You build a daily news digest in the sandbox. + +Workflow: +1. In one sandbox execution, write code that searches each topic the user gives you with the Perplexity SDK, one search per topic, max_results 20, restricted to the last day. Make exactly one search per topic and no other searches. Do not fetch article pages. Save the collected results (title, url, date, snippet) to results.json and print a compact numbered summary of them. +2. Read the printed summary and group the results into stories yourself. Grouping is judgment, not code: do not group with string matching or embeddings. +3. In one final sandbox execution, write code that loads results.json and builds two files from it and your grouping, named exactly digest.md and stories.json (refer to results by their numbers so code copies titles and urls exactly; never retype a url): + - digest.md: one section per story that is new or an update. End an update story's headline with a single (update) marker. Include the headline, a one or two sentence summary, a markdown link to the primary article, and a line listing the other domains that covered it. + - stories.json: the known stories the user gives you plus every story from today, as {"story_id": {"headline": ..., "last_seen": "YYYY-MM-DD"}}. Set last_seen to today only for stories that appeared in today's results; keep the previous last_seen for the rest. + Then share both files with the share_file tool. + +Grouping rules: +- Two articles are the same story if they report the same underlying event. Republished, reworded, or partial coverage of one event is one story. +- Similar language is not enough. Recurring events are different stories: earnings for different quarters, reports for different months, deals involving different companies. +- Ignore results that are not coverage of a single event, such as homepages, category pages, and link roundups. +- The primary article is the most complete or most original source in the group. Prefer a dedicated article page from the original outlet over aggregators and republished copies. +- story_id is a short lowercase name for the underlying event, with words separated by hyphens, like aws-chile-region. If the event matches a known story the user lists, reuse that story_id exactly. +- A story is "new" if the event is not in the known stories, an "update" if it matches a known story and adds material new information, and a "repeat" if it matches a known story and adds nothing. Leave repeats out of digest.md but keep them in stories.json. +``` + +## Run the digest + +Sandbox runs belong in [background mode](/docs/agent-api/background-mode): submit with `background: true` and poll until the status is terminal. + + +```python Python +import json +import time +from datetime import date +from pathlib import Path +from perplexity import Perplexity + +client = Perplexity() + +TOPICS = [ + "data center construction and expansion news", + "renewable energy project financing news", + "commercial real estate transaction news", +] + +STATE_FILE = Path("stories.json") +known = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {} +known_list = "\n".join( + f"- {story_id}: {story['headline']} (last seen {story['last_seen']})" + for story_id, story in known.items() +) or "(none)" +topic_list = "\n".join(f"- {topic}" for topic in TOPICS) + +INSTRUCTIONS = """You build a daily news digest in the sandbox. + +Workflow: +1. In one sandbox execution, write code that searches each topic the user gives you with the Perplexity SDK, one search per topic, max_results 20, restricted to the last day. Make exactly one search per topic and no other searches. Do not fetch article pages. Save the collected results (title, url, date, snippet) to results.json and print a compact numbered summary of them. +2. Read the printed summary and group the results into stories yourself. Grouping is judgment, not code: do not group with string matching or embeddings. +3. In one final sandbox execution, write code that loads results.json and builds two files from it and your grouping, named exactly digest.md and stories.json (refer to results by their numbers so code copies titles and urls exactly; never retype a url): + - digest.md: one section per story that is new or an update. End an update story's headline with a single (update) marker. Include the headline, a one or two sentence summary, a markdown link to the primary article, and a line listing the other domains that covered it. + - stories.json: the known stories the user gives you plus every story from today, as {"story_id": {"headline": ..., "last_seen": "YYYY-MM-DD"}}. Set last_seen to today only for stories that appeared in today's results; keep the previous last_seen for the rest. + Then share both files with the share_file tool. + +Grouping rules: +- Two articles are the same story if they report the same underlying event. Republished, reworded, or partial coverage of one event is one story. +- Similar language is not enough. Recurring events are different stories: earnings for different quarters, reports for different months, deals involving different companies. +- Ignore results that are not coverage of a single event, such as homepages, category pages, and link roundups. +- The primary article is the most complete or most original source in the group. Prefer a dedicated article page from the original outlet over aggregators and republished copies. +- story_id is a short lowercase name for the underlying event, with words separated by hyphens, like aws-chile-region. If the event matches a known story the user lists, reuse that story_id exactly. +- A story is "new" if the event is not in the known stories, an "update" if it matches a known story and adds material new information, and a "repeat" if it matches a known story and adds nothing. Leave repeats out of digest.md but keep them in stories.json.""" + +response = client.responses.create( + model="anthropic/claude-haiku-4-5", + max_output_tokens=32000, + max_steps=50, + background=True, + tools=[{"type": "sandbox"}], + instructions=INSTRUCTIONS, + input=f"""Build the digest for {date.today():%Y-%m-%d}. Topics: +{topic_list} + +Known stories from previous digests: +{known_list}""", +) + +while response.status in ("queued", "in_progress"): + time.sleep(5) + response = client.responses.retrieve(response.id) + +print(f"Final status: {response.status}") + +files = client.responses.files.list(response.id) +for file in files.data: + content = client.responses.files.content( + file_id=file.id, + response_id=response.id, + ) + content.write_to_file(file.filename) + print(f"Downloaded {file.filename} ({file.bytes} bytes)") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; +import { readFile, writeFile } from 'node:fs/promises'; + +const client = new Perplexity(); + +const TOPICS = [ + 'data center construction and expansion news', + 'renewable energy project financing news', + 'commercial real estate transaction news', +]; + +let known: Record = {}; +try { + known = JSON.parse(await readFile('stories.json', 'utf8')); +} catch {} +const knownList = + Object.entries(known) + .map(([storyId, story]) => `- ${storyId}: ${story.headline} (last seen ${story.last_seen})`) + .join('\n') || '(none)'; +const topicList = TOPICS.map((topic) => `- ${topic}`).join('\n'); + +const INSTRUCTIONS = `You build a daily news digest in the sandbox. + +Workflow: +1. In one sandbox execution, write code that searches each topic the user gives you with the Perplexity SDK, one search per topic, max_results 20, restricted to the last day. Make exactly one search per topic and no other searches. Do not fetch article pages. Save the collected results (title, url, date, snippet) to results.json and print a compact numbered summary of them. +2. Read the printed summary and group the results into stories yourself. Grouping is judgment, not code: do not group with string matching or embeddings. +3. In one final sandbox execution, write code that loads results.json and builds two files from it and your grouping, named exactly digest.md and stories.json (refer to results by their numbers so code copies titles and urls exactly; never retype a url): + - digest.md: one section per story that is new or an update. End an update story's headline with a single (update) marker. Include the headline, a one or two sentence summary, a markdown link to the primary article, and a line listing the other domains that covered it. + - stories.json: the known stories the user gives you plus every story from today, as {"story_id": {"headline": ..., "last_seen": "YYYY-MM-DD"}}. Set last_seen to today only for stories that appeared in today's results; keep the previous last_seen for the rest. + Then share both files with the share_file tool. + +Grouping rules: +- Two articles are the same story if they report the same underlying event. Republished, reworded, or partial coverage of one event is one story. +- Similar language is not enough. Recurring events are different stories: earnings for different quarters, reports for different months, deals involving different companies. +- Ignore results that are not coverage of a single event, such as homepages, category pages, and link roundups. +- The primary article is the most complete or most original source in the group. Prefer a dedicated article page from the original outlet over aggregators and republished copies. +- story_id is a short lowercase name for the underlying event, with words separated by hyphens, like aws-chile-region. If the event matches a known story the user lists, reuse that story_id exactly. +- A story is "new" if the event is not in the known stories, an "update" if it matches a known story and adds material new information, and a "repeat" if it matches a known story and adds nothing. Leave repeats out of digest.md but keep them in stories.json.`; + +const today = new Date().toISOString().slice(0, 10); + +let response = await client.responses.create({ + model: 'anthropic/claude-haiku-4-5', + max_output_tokens: 32000, + max_steps: 50, + background: true, + tools: [{ type: 'sandbox' }], + instructions: INSTRUCTIONS, + input: `Build the digest for ${today}. Topics: +${topicList} + +Known stories from previous digests: +${knownList}`, +}); + +while (response.status === 'queued' || response.status === 'in_progress') { + await new Promise((resolve) => setTimeout(resolve, 5000)); + response = await client.responses.retrieve(response.id); +} + +console.log(`Final status: ${response.status}`); + +const files = await client.responses.files.list(response.id); +for (const file of files.data) { + const content = await client.responses.files.content(file.id, { + response_id: response.id, + }); + await writeFile(file.filename, Buffer.from(await content.arrayBuffer())); + console.log(`Downloaded ${file.filename} (${file.bytes} bytes)`); +} +``` + + +## What the response contains + +The completed response's `output` walks through the run: a `skill_loaded` item for the sandbox reference skills, one `sandbox_results` item per execution with the exact code the model ran and its stdout, a `share_file` item per delivered file, and a closing `message`. The `sandbox_results` items are worth keeping in your logs; they show precisely what executed, so a bad digest is debuggable. + +The two downloaded files are the product. `digest.md` reads like this: + +```markdown +### Vantage Data Centers Unveils Plans for Frontier, a $25B Mega Campus in Texas + +Vantage Data Centers announced its largest investment to date, the $25 billion +Frontier mega-campus in Texas to meet unprecedented AI demand. + +[Read more](https://vantage-dc.com/news/vantage-data-centers-unveils-plans-for-frontier-a-25b-mega-campus-in-texas-to-meet-unprecedented-ai-demand/) +``` + +And `stories.json` is the registry that makes tomorrow's run smarter: + +```json +{ + "vantage-frontier-texas-campus": { + "headline": "Vantage Data Centers Unveils Plans for Frontier, a $25B Mega Campus in Texas", + "last_seen": "2026-07-21" + } +} +``` + +## Story memory across days + +The registry is what separates deduplication from grouping. Each run receives the known stories in its input and labels every story it finds: `new` if the event has not been covered before, `update` if it matches a known story and adds material new information, and `repeat` if it matches a known story and adds nothing. Updates stay in the digest, marked as such; repeats are dropped from the digest but kept in the registry. Because the model reuses each `story_id` for matched events, the registry stays stable across days. + +Prune registry entries whose `last_seen` is older than your dedupe window (30 days is plenty) so the input stays small. + +## Run it every day + +Schedule the script with whatever already runs your jobs. The only state between runs is `stories.json`, and the run itself keeps it current: today's job downloads the updated copy and tomorrow's job feeds it back in. Everything else is stateless. + +## Scaling up + +A three-topic day costs about seven cents on `claude-haiku-4-5`: roughly two cents of tokens, three cents for the sandbox session, and half a cent per search. The run takes about a minute in background mode. + +To cover more ground, add topics to the list; the code makes exactly one search per topic, so cost and coverage scale predictably. To dedupe against a large existing archive, give the request a link the sandbox can download, such as a presigned S3 URL, and extend the instructions so the fetch step pulls the archive next to the day's results; give bigger jobs a larger model and a higher `max_steps`. If your digest renderer wants data instead of markdown, have the final script write a `digest.json` with the same fields. + + +## Next steps + + + +How the container works, calling other tools from code, pricing, and limits. + + + +Submit, poll, stream, and cancel long-running agent runs. + + + +List and download files an Agent API response produced in the sandbox. + + diff --git a/docs/articles/openai-agents-integration/README.md b/docs/articles/openai-agents-integration/README.md deleted file mode 100644 index 5e0b0dd..0000000 --- a/docs/articles/openai-agents-integration/README.md +++ /dev/null @@ -1,388 +0,0 @@ ---- -title: OpenAI Agents Integration -description: Complete guide for integrating Perplexity's Sonar API with the OpenAI Agents SDK -sidebar_position: 1 -keywords: [openai, agents, integration, async, custom-client] ---- - -# Integrating Perplexity Sonar API with OpenAI Agents SDK - -This comprehensive guide demonstrates how to integrate [Perplexity's Sonar API](https://sonar.perplexity.ai/) with the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) using a custom asynchronous client. You'll learn how to create intelligent agents that leverage Sonar's real-time search capabilities alongside OpenAI's agent framework. - -## 🎯 What You'll Build - -By the end of this guide, you'll have: -- ✅ A custom async OpenAI client configured for Sonar API -- ✅ An intelligent agent with function calling capabilities -- ✅ A working example that fetches real-time information -- ✅ Production-ready integration patterns - -## 🏗️ Architecture Overview - -```mermaid -graph TD - A[Your Application] --> B[OpenAI Agents SDK] - B --> C[Custom AsyncOpenAI Client] - C --> D[Perplexity Sonar API] - B --> E[Function Tools] - E --> F[Weather API, etc.] -``` - -This integration allows you to: -1. **Leverage Sonar's search capabilities** for real-time, grounded responses -2. **Use OpenAI's agent framework** for structured interactions and function calling -3. **Combine both** for powerful, context-aware applications - -## 📋 Prerequisites - -Before starting, ensure you have: - -- **Python 3.7+** installed -- **Perplexity API Key** - [Get one here](https://docs.perplexity.ai/home) -- **OpenAI Agents SDK** access and familiarity - -## 🚀 Installation - -Install the required dependencies: - -```bash -pip install openai nest-asyncio -``` - -:::info -The `nest-asyncio` package is required for running async code in environments like Jupyter notebooks that already have an event loop running. -::: - -## ⚙️ Environment Setup - -Configure your environment variables: - -```bash -# Required: Your Perplexity API key -export EXAMPLE_API_KEY="your-perplexity-api-key" - -# Optional: Customize the API endpoint (defaults to official endpoint) -export EXAMPLE_BASE_URL="https://api.perplexity.ai" - -# Optional: Choose your model (defaults to sonar-pro) -export EXAMPLE_MODEL_NAME="sonar-pro" -``` - -## 💻 Complete Implementation - -Here's the full implementation with detailed explanations: - -```python -# Import necessary standard libraries -import asyncio # For running asynchronous code -import os # To access environment variables - -# Import AsyncOpenAI for creating an async client -from openai import AsyncOpenAI - -# Import custom classes and functions from the agents package. -# These handle agent creation, model interfacing, running agents, and more. -from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled - -# Retrieve configuration from environment variables or use defaults -BASE_URL = os.getenv("EXAMPLE_BASE_URL") or "https://api.perplexity.ai" -API_KEY = os.getenv("EXAMPLE_API_KEY") -MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or "sonar-pro" - -# Validate that all required configuration variables are set -if not BASE_URL or not API_KEY or not MODEL_NAME: - raise ValueError( - "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code." - ) - -# Initialize the custom OpenAI async client with the specified BASE_URL and API_KEY. -client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) - -# Disable tracing to avoid using a platform tracing key; adjust as needed. -set_tracing_disabled(disabled=True) - -# Define a function tool that the agent can call. -# The decorator registers this function as a tool in the agents framework. -@function_tool -def get_weather(city: str): - """ - Simulate fetching weather data for a given city. - - Args: - city (str): The name of the city to retrieve weather for. - - Returns: - str: A message with weather information. - """ - print(f"[debug] getting weather for {city}") - return f"The weather in {city} is sunny." - -# Import nest_asyncio to support nested event loops -import nest_asyncio - -# Apply the nest_asyncio patch to enable running asyncio.run() -# even if an event loop is already running. -nest_asyncio.apply() - -async def main(): - """ - Main asynchronous function to set up and run the agent. - - This function creates an Agent with a custom model and function tools, - then runs a query to get the weather in Tokyo. - """ - # Create an Agent instance with: - # - A name ("Assistant") - # - Custom instructions ("Be precise and concise.") - # - A model built from OpenAIChatCompletionsModel using our client and model name. - # - A list of tools; here, only get_weather is provided. - agent = Agent( - name="Assistant", - instructions="Be precise and concise.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - # Execute the agent with the sample query. - result = await Runner.run(agent, "What's the weather in Tokyo?") - - # Print the final output from the agent. - print(result.final_output) - -# Standard boilerplate to run the async main() function. -if __name__ == "__main__": - asyncio.run(main()) -``` - -## 🔍 Code Breakdown - -Let's examine the key components: - -### 1. **Client Configuration** - -```python -client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) -``` - -This creates an async OpenAI client pointed at Perplexity's Sonar API. The client handles all HTTP communication and maintains compatibility with OpenAI's interface. - -### 2. **Function Tools** - -```python -@function_tool -def get_weather(city: str): - """Simulate fetching weather data for a given city.""" - return f"The weather in {city} is sunny." -``` - -Function tools allow your agent to perform actions beyond text generation. In production, you'd replace this with real API calls. - -### 3. **Agent Creation** - -```python -agent = Agent( - name="Assistant", - instructions="Be precise and concise.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], -) -``` - -The agent combines Sonar's language capabilities with your custom tools and instructions. - -## 🏃‍♂️ Running the Example - -1. **Set your environment variables**: - ```bash - export EXAMPLE_API_KEY="your-perplexity-api-key" - ``` - -2. **Save the code** to a file (e.g., `pplx_openai_agent.py`) - -3. **Run the script**: - ```bash - python pplx_openai_agent.py - ``` - -**Expected Output**: -``` -[debug] getting weather for Tokyo -The weather in Tokyo is sunny. -``` - -## 🔧 Customization Options - -### **Different Sonar Models** - -Choose the right model for your use case: - -```python -# For quick, lightweight queries -MODEL_NAME = "sonar" - -# For complex research and analysis (default) -MODEL_NAME = "sonar-pro" - -# For deep reasoning tasks -MODEL_NAME = "sonar-reasoning-pro" -``` - -### **Custom Instructions** - -Tailor the agent's behavior: - -```python -agent = Agent( - name="Research Assistant", - instructions=""" - You are a research assistant specializing in academic literature. - Always provide citations and verify information through multiple sources. - Be thorough but concise in your responses. - """, - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[search_papers, get_citations], -) -``` - -### **Multiple Function Tools** - -Add more capabilities: - -```python -@function_tool -def search_web(query: str): - """Search the web for current information.""" - # Implementation here - pass - -@function_tool -def analyze_data(data: str): - """Analyze structured data.""" - # Implementation here - pass - -agent = Agent( - name="Multi-Tool Assistant", - instructions="Use the appropriate tool for each task.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather, search_web, analyze_data], -) -``` - -## 🚀 Production Considerations - -### **Error Handling** - -```python -async def robust_main(): - try: - agent = Agent( - name="Assistant", - instructions="Be helpful and accurate.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - result = await Runner.run(agent, "What's the weather in Tokyo?") - return result.final_output - - except Exception as e: - print(f"Error running agent: {e}") - return "Sorry, I encountered an error processing your request." -``` - -### **Rate Limiting** - -```python -import aiohttp -from openai import AsyncOpenAI - -# Configure client with custom timeout and retry settings -client = AsyncOpenAI( - base_url=BASE_URL, - api_key=API_KEY, - timeout=30.0, - max_retries=3 -) -``` - -### **Logging and Monitoring** - -```python -import logging - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -@function_tool -def get_weather(city: str): - logger.info(f"Fetching weather for {city}") - # Implementation here -``` - -## 🔗 Advanced Integration Patterns - -### **Streaming Responses** - -For real-time applications: - -```python -async def stream_agent_response(query: str): - agent = Agent( - name="Streaming Assistant", - instructions="Provide detailed, step-by-step responses.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - async for chunk in Runner.stream(agent, query): - print(chunk, end='', flush=True) -``` - -### **Context Management** - -For multi-turn conversations: - -```python -class ConversationManager: - def __init__(self): - self.agent = Agent( - name="Conversational Assistant", - instructions="Maintain context across multiple interactions.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - self.conversation_history = [] - - async def chat(self, message: str): - result = await Runner.run(self.agent, message) - self.conversation_history.append({"user": message, "assistant": result.final_output}) - return result.final_output -``` - -## ⚠️ Important Notes - -- **API Costs**: Monitor your usage as both Perplexity and OpenAI Agents may incur costs -- **Rate Limits**: Respect API rate limits and implement appropriate backoff strategies -- **Error Handling**: Always implement robust error handling for production applications -- **Security**: Keep your API keys secure and never commit them to version control - -## 🎯 Use Cases - -This integration pattern is perfect for: - -- **🔍 Research Assistants** - Combining real-time search with structured responses -- **📊 Data Analysis Tools** - Using Sonar for context and agents for processing -- **🤖 Customer Support** - Grounded responses with function calling capabilities -- **📚 Educational Applications** - Real-time information with interactive features - -## 📚 References - -- [Perplexity Sonar API Documentation](https://docs.perplexity.ai/home) -- [OpenAI Agents SDK Documentation](https://github.com/openai/openai-agents-python) -- [AsyncOpenAI Client Reference](https://platform.openai.com/docs/api-reference) -- [Function Calling Best Practices](https://platform.openai.com/docs/guides/function-calling) - ---- - -**Ready to build?** This integration opens up powerful possibilities for creating intelligent, grounded agents. Start with the basic example and gradually add more sophisticated tools and capabilities! 🚀 \ No newline at end of file diff --git a/docs/articles/openai-agents-integration/README.mdx b/docs/articles/openai-agents-integration/README.mdx deleted file mode 100644 index 73aefd0..0000000 --- a/docs/articles/openai-agents-integration/README.mdx +++ /dev/null @@ -1,384 +0,0 @@ ---- -title: OpenAI Agents Integration -description: Complete guide for integrating Perplexity's Sonar API with the OpenAI Agents SDK -sidebar_position: 1 -keywords: [openai, agents, integration, async, custom-client] ---- - -## 🎯 What You'll Build - -By the end of this guide, you'll have: -- ✅ A custom async OpenAI client configured for Sonar API -- ✅ An intelligent agent with function calling capabilities -- ✅ A working example that fetches real-time information -- ✅ Production-ready integration patterns - -## 🏗️ Architecture Overview - -```mermaid -graph TD - A[Your Application] --> B[OpenAI Agents SDK] - B --> C[Custom AsyncOpenAI Client] - C --> D[Perplexity Sonar API] - B --> E[Function Tools] - E --> F[Weather API, etc.] -``` - -This integration allows you to: -1. **Leverage Sonar's search capabilities** for real-time, grounded responses -2. **Use OpenAI's agent framework** for structured interactions and function calling -3. **Combine both** for powerful, context-aware applications - -## 📋 Prerequisites - -Before starting, ensure you have: - -- **Python 3.7+** installed -- **Perplexity API Key** - [Get one here](https://docs.perplexity.ai/home) -- **OpenAI Agents SDK** access and familiarity - -## 🚀 Installation - -Install the required dependencies: - -```bash -pip install openai nest-asyncio -``` - -:::info -The `nest-asyncio` package is required for running async code in environments like Jupyter notebooks that already have an event loop running. -::: - -## ⚙️ Environment Setup - -Configure your environment variables: - -```bash -# Required: Your Perplexity API key -export EXAMPLE_API_KEY="your-perplexity-api-key" - -# Optional: Customize the API endpoint (defaults to official endpoint) -export EXAMPLE_BASE_URL="https://api.perplexity.ai" - -# Optional: Choose your model (defaults to sonar-pro) -export EXAMPLE_MODEL_NAME="sonar-pro" -``` - -## 💻 Complete Implementation - -Here's the full implementation with detailed explanations: - -```python -# Import necessary standard libraries -import asyncio # For running asynchronous code -import os # To access environment variables - -# Import AsyncOpenAI for creating an async client -from openai import AsyncOpenAI - -# Import custom classes and functions from the agents package. -# These handle agent creation, model interfacing, running agents, and more. -from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled - -# Retrieve configuration from environment variables or use defaults -BASE_URL = os.getenv("EXAMPLE_BASE_URL") or "https://api.perplexity.ai" -API_KEY = os.getenv("EXAMPLE_API_KEY") -MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or "sonar-pro" - -# Validate that all required configuration variables are set -if not BASE_URL or not API_KEY or not MODEL_NAME: - raise ValueError( - "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code." - ) - -# Initialize the custom OpenAI async client with the specified BASE_URL and API_KEY. -client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) - -# Disable tracing to avoid using a platform tracing key; adjust as needed. -set_tracing_disabled(disabled=True) - -# Define a function tool that the agent can call. -# The decorator registers this function as a tool in the agents framework. -@function_tool -def get_weather(city: str): - """ - Simulate fetching weather data for a given city. - - Args: - city (str): The name of the city to retrieve weather for. - - Returns: - str: A message with weather information. - """ - print(f"[debug] getting weather for {city}") - return f"The weather in {city} is sunny." - -# Import nest_asyncio to support nested event loops -import nest_asyncio - -# Apply the nest_asyncio patch to enable running asyncio.run() -# even if an event loop is already running. -nest_asyncio.apply() - -async def main(): - """ - Main asynchronous function to set up and run the agent. - - This function creates an Agent with a custom model and function tools, - then runs a query to get the weather in Tokyo. - """ - # Create an Agent instance with: - # - A name ("Assistant") - # - Custom instructions ("Be precise and concise.") - # - A model built from OpenAIChatCompletionsModel using our client and model name. - # - A list of tools; here, only get_weather is provided. - agent = Agent( - name="Assistant", - instructions="Be precise and concise.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - # Execute the agent with the sample query. - result = await Runner.run(agent, "What's the weather in Tokyo?") - - # Print the final output from the agent. - print(result.final_output) - -# Standard boilerplate to run the async main() function. -if __name__ == "__main__": - asyncio.run(main()) -``` - -## 🔍 Code Breakdown - -Let's examine the key components: - -### 1. **Client Configuration** - -```python -client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) -``` - -This creates an async OpenAI client pointed at Perplexity's Sonar API. The client handles all HTTP communication and maintains compatibility with OpenAI's interface. - -### 2. **Function Tools** - -```python -@function_tool -def get_weather(city: str): - """Simulate fetching weather data for a given city.""" - return f"The weather in {city} is sunny." -``` - -Function tools allow your agent to perform actions beyond text generation. In production, you'd replace this with real API calls. - -### 3. **Agent Creation** - -```python -agent = Agent( - name="Assistant", - instructions="Be precise and concise.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], -) -``` - -The agent combines Sonar's language capabilities with your custom tools and instructions. - -## 🏃‍♂️ Running the Example - -1. **Set your environment variables**: - ```bash - export EXAMPLE_API_KEY="your-perplexity-api-key" - ``` - -2. **Save the code** to a file (e.g., `pplx_openai_agent.py`) - -3. **Run the script**: - ```bash - python pplx_openai_agent.py - ``` - -**Expected Output**: -``` -[debug] getting weather for Tokyo -The weather in Tokyo is sunny. -``` - -## 🔧 Customization Options - -### **Different Sonar Models** - -Choose the right model for your use case: - -```python -# For quick, lightweight queries -MODEL_NAME = "sonar" - -# For complex research and analysis (default) -MODEL_NAME = "sonar-pro" - -# For deep reasoning tasks -MODEL_NAME = "sonar-reasoning-pro" -``` - -### **Custom Instructions** - -Tailor the agent's behavior: - -```python -agent = Agent( - name="Research Assistant", - instructions=""" - You are a research assistant specializing in academic literature. - Always provide citations and verify information through multiple sources. - Be thorough but concise in your responses. - """, - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[search_papers, get_citations], -) -``` - -### **Multiple Function Tools** - -Add more capabilities: - -```python -@function_tool -def search_web(query: str): - """Search the web for current information.""" - # Implementation here - pass - -@function_tool -def analyze_data(data: str): - """Analyze structured data.""" - # Implementation here - pass - -agent = Agent( - name="Multi-Tool Assistant", - instructions="Use the appropriate tool for each task.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather, search_web, analyze_data], -) -``` - -## 🚀 Production Considerations - -### **Error Handling** - -```python -async def robust_main(): - try: - agent = Agent( - name="Assistant", - instructions="Be helpful and accurate.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - result = await Runner.run(agent, "What's the weather in Tokyo?") - return result.final_output - - except Exception as e: - print(f"Error running agent: {e}") - return "Sorry, I encountered an error processing your request." -``` - -### **Rate Limiting** - -```python -import aiohttp -from openai import AsyncOpenAI - -# Configure client with custom timeout and retry settings -client = AsyncOpenAI( - base_url=BASE_URL, - api_key=API_KEY, - timeout=30.0, - max_retries=3 -) -``` - -### **Logging and Monitoring** - -```python -import logging - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -@function_tool -def get_weather(city: str): - logger.info(f"Fetching weather for {city}") - # Implementation here -``` - -## 🔗 Advanced Integration Patterns - -### **Streaming Responses** - -For real-time applications: - -```python -async def stream_agent_response(query: str): - agent = Agent( - name="Streaming Assistant", - instructions="Provide detailed, step-by-step responses.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - async for chunk in Runner.stream(agent, query): - print(chunk, end='', flush=True) -``` - -### **Context Management** - -For multi-turn conversations: - -```python -class ConversationManager: - def __init__(self): - self.agent = Agent( - name="Conversational Assistant", - instructions="Maintain context across multiple interactions.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - self.conversation_history = [] - - async def chat(self, message: str): - result = await Runner.run(self.agent, message) - self.conversation_history.append({"user": message, "assistant": result.final_output}) - return result.final_output -``` - -## ⚠️ Important Notes - -- **API Costs**: Monitor your usage as both Perplexity and OpenAI Agents may incur costs -- **Rate Limits**: Respect API rate limits and implement appropriate backoff strategies -- **Error Handling**: Always implement robust error handling for production applications -- **Security**: Keep your API keys secure and never commit them to version control - -## 🎯 Use Cases - -This integration pattern is perfect for: - -- **🔍 Research Assistants** - Combining real-time search with structured responses -- **📊 Data Analysis Tools** - Using Sonar for context and agents for processing -- **🤖 Customer Support** - Grounded responses with function calling capabilities -- **📚 Educational Applications** - Real-time information with interactive features - -## 📚 References - -- [Perplexity Sonar API Documentation](https://docs.perplexity.ai/home) -- [OpenAI Agents SDK Documentation](https://github.com/openai/openai-agents-python) -- [AsyncOpenAI Client Reference](https://platform.openai.com/docs/api-reference) -- [Function Calling Best Practices](https://platform.openai.com/docs/guides/function-calling) - ---- - -**Ready to build?** This integration opens up powerful possibilities for creating intelligent, grounded agents. Start with the basic example and gradually add more sophisticated tools and capabilities! 🚀 \ No newline at end of file diff --git a/docs/articles/openai-agents-integration/pplx_openai.py b/docs/articles/openai-agents-integration/pplx_openai.py deleted file mode 100644 index 2df5da8..0000000 --- a/docs/articles/openai-agents-integration/pplx_openai.py +++ /dev/null @@ -1,92 +0,0 @@ -# Import necessary standard libraries -import asyncio # For running asynchronous code -import os # To access environment variables - -# Import AsyncOpenAI for creating an async client -from openai import AsyncOpenAI - -# Import custom classes and functions from the agents package. -# These handle agent creation, model interfacing, running agents, and more. -from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled - -# Retrieve configuration from environment variables or use defaults -BASE_URL = os.getenv("EXAMPLE_BASE_URL") or "https://api.perplexity.ai" -API_KEY = os.getenv("EXAMPLE_API_KEY") -MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or "sonar-pro" - -# Validate that all required configuration variables are set -if not BASE_URL or not API_KEY or not MODEL_NAME: - raise ValueError( - "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code." - ) - -""" -This example illustrates how to use a custom provider with a specific agent: -1. We create an asynchronous OpenAI client configured to interact with the Perplexity Sonar API. -2. We define a custom model using this client. -3. We set up an Agent with our custom model and attach function tools. -Note: Tracing is disabled in this example. If you have an OpenAI platform API key, -you can enable tracing by setting the environment variable OPENAI_API_KEY or using set_tracing_export_api_key(). -""" - -# Initialize the custom OpenAI async client with the specified BASE_URL and API_KEY. -client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) - -# Disable tracing to avoid using a platform tracing key; adjust as needed. -set_tracing_disabled(disabled=True) - -# (Alternate approach example, commented out) -# PROVIDER = OpenAIProvider(openai_client=client) -# agent = Agent(..., model="some-custom-model") -# Runner.run(agent, ..., run_config=RunConfig(model_provider=PROVIDER)) - -# Define a function tool that the agent can call. -# The decorator registers this function as a tool in the agents framework. -@function_tool -def get_weather(city: str): - """ - Simulate fetching weather data for a given city. - - Args: - city (str): The name of the city to retrieve weather for. - - Returns: - str: A message with weather information. - """ - print(f"[debug] getting weather for {city}") - return f"The weather in {city} is sunny." - -# Import nest_asyncio to support nested event loops (helpful in interactive environments like Jupyter) -import nest_asyncio - -# Apply the nest_asyncio patch to enable running asyncio.run() even if an event loop is already running. -nest_asyncio.apply() - -async def main(): - """ - Main asynchronous function to set up and run the agent. - - This function creates an Agent with a custom model and function tools, - then runs a query to get the weather in Tokyo. - """ - # Create an Agent instance with: - # - A name ("Assistant") - # - Custom instructions ("Be precise and concise.") - # - A model built from OpenAIChatCompletionsModel using our client and model name. - # - A list of tools; here, only get_weather is provided. - agent = Agent( - name="Assistant", - instructions="Be precise and concise.", - model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), - tools=[get_weather], - ) - - # Execute the agent with the sample query. - result = await Runner.run(agent, "What's the weather in Tokyo?") - - # Print the final output from the agent. - print(result.final_output) - -# Standard boilerplate to run the async main() function. -if __name__ == "__main__": - asyncio.run(main()) diff --git a/docs/articles/search-domain-filtering/README.mdx b/docs/articles/search-domain-filtering/README.mdx new file mode 100644 index 0000000..b819dc1 --- /dev/null +++ b/docs/articles/search-domain-filtering/README.mdx @@ -0,0 +1,588 @@ +--- +title: Search Domain Filtering Patterns +description: Use search_domain_filter for focused search — allowlist patterns for trusted sources, denylist for excluding domains, and practical patterns for news, government, and competitive intelligence +sidebar_position: 9 +keywords: [domain-filter, search-filter, allowlist, denylist, web-search, agent-api] +products: [agent-api] +categories: [search-filtering] +--- + +This guide covers search domain filtering on the Agent API. You will learn how to use allowlists to restrict search to trusted domains, denylists to exclude unwanted sources, and practical patterns for common use cases like news-only search, government data, and competitor exclusion. + + +Domain filtering is configured per-tool under the `tools` array via `tools[].filters.search_domain_filter`. For the full reference, see [Agent API Filters](/docs/agent-api/tools/web-search#filters). + + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## How Domain Filtering Works + +The `search_domain_filter` parameter accepts a list of domain strings: + +- **Allowlist** (no prefix): Include only results from these domains. `["reuters.com", "apnews.com"]` means search only Reuters and AP News. +- **Denylist** (`-` prefix): Exclude results from these domains. `["-reddit.com", "-twitter.com"]` means exclude Reddit and Twitter. + +*You can also add a path to a domain to narrow results to one section of a site — `["nature.com/articles"]` searches only that section, while `["-reddit.com/r/all"]` excludes that section but still searches the rest of the site.* + + +**Never mix allowlist and denylist entries in the same request.** The API does not support combining `"reuters.com"` and `"-reddit.com"` in the same array. Use either all allowlist or all denylist entries. + + +## Basic Domain Filtering + +Domain filters are configured per-tool under the `tools` array. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Allowlist: search only specific domains +response = client.responses.create( + model="openai/gpt-5.4", + input="What are the latest developments in AI regulation?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": ["reuters.com", "apnews.com", "bbc.com"], + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are the latest developments in AI regulation?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: ["reuters.com", "apnews.com", "bbc.com"], + }, + }], +}); +console.log(response.output_text); +``` + + +## Pattern: Denylist Filtering + +Use the `-` prefix to exclude specific domains from search results. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Denylist: exclude social media and user-generated content +response = client.responses.create( + model="openai/gpt-5.4", + input="What are the latest developments in AI regulation?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": ["-reddit.com", "-twitter.com", "-quora.com", "-medium.com"], + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are the latest developments in AI regulation?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: ["-reddit.com", "-twitter.com", "-quora.com", "-medium.com"], + }, + }], +}); +console.log(response.output_text); +``` + + +## Pattern: Path Filtering + +Add a path after a domain to limit results to one section of a site — such as documentation, a subreddit, a blog, or a news vertical. This works in both allowlist and denylist mode. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Allowlist a single section of a site, and exclude a section of another +response = client.responses.create( + model="openai/gpt-5.4", + input="Summarize recent peer-reviewed CRISPR results", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": ["nature.com/articles", "science.org"], + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "Summarize recent peer-reviewed CRISPR results", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: ["nature.com/articles", "science.org"], + }, + }], +}); +console.log(response.output_text); +``` + + + +Paths match on segment boundaries, so `"example.com/docs"` matches `/docs` and `/docs/intro` but not `/documentation`. Subdomains are included (`blog.example.com/docs` matches). Query strings after the boundary are allowed (`/docs?x=1`). + + +## Pattern: News-Only Search + +Restrict results to major news outlets for current events and breaking news. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +NEWS_DOMAINS = [ + "reuters.com", + "apnews.com", + "bbc.com", + "nytimes.com", + "washingtonpost.com", + "theguardian.com", + "bloomberg.com", + "ft.com", +] + +response = client.responses.create( + model="openai/gpt-5.4", + input="What happened in global markets today?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": NEWS_DOMAINS, + "search_recency_filter": "day", + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const NEWS_DOMAINS = [ + "reuters.com", + "apnews.com", + "bbc.com", + "nytimes.com", + "washingtonpost.com", + "theguardian.com", + "bloomberg.com", + "ft.com", +]; + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What happened in global markets today?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: NEWS_DOMAINS, + search_recency_filter: "day", + }, + }], +}); +console.log(response.output_text); +``` + + + +Combine `search_domain_filter` with `search_recency_filter` for time-sensitive queries. Options are `day`, `week`, `month`, and `year`. + + +## Pattern: Government and Official Sources + +Restrict to government domains for policy, regulation, and official statistics. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +GOV_DOMAINS = [ + ".gov", # US federal and state + ".gov.uk", # UK government + ".europa.eu", # EU institutions + "who.int", # World Health Organization + "worldbank.org", # World Bank +] + +response = client.responses.create( + model="openai/gpt-5.4", + input="What are the current US federal guidelines on AI usage in healthcare?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": GOV_DOMAINS, + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const GOV_DOMAINS = [ + ".gov", + ".gov.uk", + ".europa.eu", + "who.int", + "worldbank.org", +]; + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are the current US federal guidelines on AI usage in healthcare?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: GOV_DOMAINS, + }, + }], +}); +console.log(response.output_text); +``` + + +## Pattern: Academic and Research Filtering + +Target educational and research institutions. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +ACADEMIC_DOMAINS = [ + ".edu", + "arxiv.org", + "scholar.google.com", + "pubmed.ncbi.nlm.nih.gov", + "nature.com", + "science.org", + "ieee.org", +] + +response = client.responses.create( + model="openai/gpt-5.4", + input="What are recent advances in protein structure prediction?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": ACADEMIC_DOMAINS, + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const ACADEMIC_DOMAINS = [ + ".edu", + "arxiv.org", + "scholar.google.com", + "pubmed.ncbi.nlm.nih.gov", + "nature.com", + "science.org", + "ieee.org", +]; + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are recent advances in protein structure prediction?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: ACADEMIC_DOMAINS, + }, + }], +}); +console.log(response.output_text); +``` + + +## Pattern: Competitor Exclusion + +Use denylists to exclude competitor websites from search results when building customer-facing content. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Exclude competitor domains from product research +EXCLUDED_DOMAINS = [ + "-competitor-a.com", + "-competitor-b.io", + "-competitor-c.ai", +] + +response = client.responses.create( + model="openai/gpt-5.4", + input="What are the best practices for building real-time data pipelines?", + tools=[{ + "type": "web_search", + "filters": { + "search_domain_filter": EXCLUDED_DOMAINS, + }, + }], +) +print(response.output_text) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const EXCLUDED_DOMAINS = [ + "-competitor-a.com", + "-competitor-b.io", + "-competitor-c.ai", +]; + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are the best practices for building real-time data pipelines?", + tools: [{ + type: "web_search" as const, + filters: { + search_domain_filter: EXCLUDED_DOMAINS, + }, + }], +}); +console.log(response.output_text); +``` + + +## Configurable Filter Builder + +A reusable helper that builds domain filter configurations from named presets. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Named filter presets +FILTER_PRESETS = { + "news": ["reuters.com", "apnews.com", "bbc.com", "bloomberg.com", "ft.com"], + "academic": [".edu", "arxiv.org", "nature.com", "science.org", "pubmed.ncbi.nlm.nih.gov"], + "government": [".gov", ".gov.uk", ".europa.eu", "who.int"], + "tech": ["techcrunch.com", "arstechnica.com", "theverge.com", "wired.com"], + "no_social": ["-reddit.com", "-twitter.com", "-facebook.com", "-tiktok.com", "-quora.com"], + "no_seo_spam": ["-pinterest.com", "-medium.com", "-hubspot.com"], +} + + +def search_with_preset(query: str, preset: str, recency: str = None) -> str: + """Run a search with a named domain filter preset.""" + if preset not in FILTER_PRESETS: + raise ValueError(f"Unknown preset: {preset}. Options: {list(FILTER_PRESETS.keys())}") + + filters = {"search_domain_filter": FILTER_PRESETS[preset]} + if recency: + filters["search_recency_filter"] = recency + + response = client.responses.create( + model="openai/gpt-5.4", + input=query, + tools=[{"type": "web_search", "filters": filters}], + ) + return response.output_text + + +# Usage +print("--- News Search ---") +print(search_with_preset("Latest AI regulation news", "news", recency="week")) + +print("\n--- Academic Search ---") +print(search_with_preset("CRISPR gene editing recent papers", "academic")) + +print("\n--- Clean Search (no social media) ---") +print(search_with_preset("Best Python testing frameworks", "no_social")) +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const FILTER_PRESETS: Record = { + news: ["reuters.com", "apnews.com", "bbc.com", "bloomberg.com", "ft.com"], + academic: [".edu", "arxiv.org", "nature.com", "science.org", "pubmed.ncbi.nlm.nih.gov"], + government: [".gov", ".gov.uk", ".europa.eu", "who.int"], + tech: ["techcrunch.com", "arstechnica.com", "theverge.com", "wired.com"], + no_social: ["-reddit.com", "-twitter.com", "-facebook.com", "-tiktok.com", "-quora.com"], + no_seo_spam: ["-pinterest.com", "-medium.com", "-hubspot.com"], +}; + +async function searchWithPreset(query: string, preset: string, recency?: string): Promise { + if (!(preset in FILTER_PRESETS)) { + throw new Error(`Unknown preset: ${preset}. Options: ${Object.keys(FILTER_PRESETS).join(", ")}`); + } + + const filters: Record = { search_domain_filter: FILTER_PRESETS[preset] }; + if (recency) filters.search_recency_filter = recency; + + const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: query, + tools: [{ type: "web_search" as const, filters }], + }); + return response.output_text; +} + +console.log("--- News Search ---"); +console.log(await searchWithPreset("Latest AI regulation news", "news", "week")); + +console.log("\n--- Academic Search ---"); +console.log(await searchWithPreset("CRISPR gene editing recent papers", "academic")); + +console.log("\n--- Clean Search (no social media) ---"); +console.log(await searchWithPreset("Best Python testing frameworks", "no_social")); +``` + + +## Common Pitfalls + +### Mixing Allowlist and Denylist + +```python +# ❌ WRONG: mixing allowlist and denylist +search_domain_filter=["reuters.com", "-reddit.com"] + +# ✅ CORRECT: use only allowlist +search_domain_filter=["reuters.com", "apnews.com", "bbc.com"] + +# ✅ CORRECT: use only denylist +search_domain_filter=["-reddit.com", "-twitter.com"] +``` + +### Using Wildcards Incorrectly + +```python +# ❌ WRONG: wildcards are not supported +search_domain_filter=["*.gov"] + +# ✅ CORRECT: use the TLD directly +search_domain_filter=[".gov"] + +# ✅ CORRECT: narrow to a section with a path prefix (no wildcards) +search_domain_filter=["example.com/blog"] +``` + +### Empty Filter Arrays + +```python +# ❌ WRONG: empty array has undefined behavior +search_domain_filter=[] + +# ✅ CORRECT: omit the parameter to search all domains +# (simply don't include search_domain_filter) +``` + +## Tips and Best Practices + +1. **Keep allowlists focused.** 5-10 domains is usually sufficient. Too many domains dilutes the filter's purpose. + +2. **Use denylists for broad exclusion.** When you want to exclude a few noisy sources but otherwise search the full web, denylists are more practical than trying to allowlist everything else. + +3. **Combine with recency filters.** For time-sensitive queries, add `search_recency_filter` alongside domain filters. + +4. **Test your filters.** Run the same query with and without filters to verify that results change as expected. + +5. **TLD filters work broadly.** Using `.gov` matches any domain ending in `.gov`, including `whitehouse.gov`, `irs.gov`, and state domains like `ca.gov`. + +6. **Store presets in configuration.** Define filter presets in your app configuration rather than hardcoding them in every request. + +## Next Steps + + + +Full reference for domain, date range, and location filters on the Agent API. + + + +Domain filtering on the raw Search API for result-level control. + + + +Specialized academic search with domain filtering. + + diff --git a/docs/articles/streaming-citations/README.mdx b/docs/articles/streaming-citations/README.mdx new file mode 100644 index 0000000..2f0d6e9 --- /dev/null +++ b/docs/articles/streaming-citations/README.mdx @@ -0,0 +1,690 @@ +--- +title: Streaming Citation Parsing +description: Consume streaming responses from the Agent API and extract, validate, and display citations in real-time as chunks arrive +sidebar_position: 4 +keywords: [streaming, citations, agent-api, real-time, validation, fast, stream-parsing] +products: [agent-api] +categories: [streaming, integrations] +--- + +This guide shows how to consume streaming responses from the Agent API, extract citations as they arrive, validate source URLs, and build a fully cited output. Streaming is essential for responsive UIs and long-running searches — you can display text and sources progressively instead of waiting for the full response. + + +The `fast` preset is optimized for quick, citation-rich answers. The model inserts numbered references like `[1]`, `[2]` in the text, and the corresponding source URLs arrive in the `search_results` output item. See the [Agent API Presets](/docs/agent-api/presets) docs for all available presets. + + +## Prerequisites + +Install the SDKs: + + +```bash Python +pip install perplexityai openai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai openai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## How Streaming Citations Work + +When you stream an Agent API response with a search-enabled preset, the API sends a sequence of server-sent events (SSE). The flow is: + +1. **Search results** arrive via `response.reasoning.search_results` events — one event per search the model runs — containing URLs, titles, and snippets for each source. +2. **Content chunks** arrive incrementally as the model generates text via `response.output_text.delta` events. +3. **Citation references** appear in the text as numbered markers like `[1]`, `[2]`, mapping to the search result `id` field. + +Your client accumulates the text, collects search results, then maps the numbered references to source URLs using the `id` field. + + +**A response can contain more than one batch of search results.** Single-step presets like `fast` typically search once, but multi-step presets (such as deep research) run many searches — one `search_results` event (streaming) or output item (non-streaming) per search, all sharing a single citation `id` space. Always collect the results from **every** event or item. If you keep only the first batch, most `[N]` references in the text won't resolve and citations will look hallucinated. + + +## Basic Streaming with Citations + + +```python Python +import os +from openai import OpenAI + +# The OpenAI SDK supports Agent API streaming via the /v1/responses alias +client = OpenAI( + api_key=os.environ["PERPLEXITY_API_KEY"], + base_url="https://api.perplexity.ai/v1", +) + +stream = client.responses.create( + input="What are the latest breakthroughs in quantum computing?", + stream=True, + extra_body={"preset": "fast"}, +) + +full_content = "" +search_results = [] + +for event in stream: + event_type = event.type + + # Collect search results (one event per search — accumulate, don't overwrite) + if event_type == "response.reasoning.search_results": + search_results.extend(event.results or []) + + # Accumulate content from each delta + if event_type == "response.output_text.delta": + full_content += event.delta + print(event.delta, end="", flush=True) + +print("\n\n--- Citations ---") +for result in search_results: + print(f"[{result['id']}] {result['title']} — {result['url']}") +``` + +```typescript TypeScript +import OpenAI from "openai"; + +// The OpenAI SDK supports Agent API streaming via the /v1/responses alias +const client = new OpenAI({ + apiKey: process.env.PERPLEXITY_API_KEY, + baseURL: "https://api.perplexity.ai/v1", +}); + +const stream = await client.responses.create({ + input: "What are the latest breakthroughs in quantum computing?", + stream: true, + preset: "fast", +} as any); + +let fullContent = ""; +let searchResults: Array<{ id: number; title: string; url: string }> = []; + +for await (const event of stream) { + // Collect search results (one event per search — accumulate, don't overwrite) + if (event.type === "response.reasoning.search_results") { + searchResults.push(...((event as any).results ?? [])); + } + + // Accumulate content from each delta + if (event.type === "response.output_text.delta") { + fullContent += event.delta; + process.stdout.write(event.delta); + } +} + +console.log("\n\n--- Citations ---"); +searchResults.forEach((result) => { + console.log(`[${result.id}] ${result.title} — ${result.url}`); +}); +``` + +```bash curl +curl -N "https://api.perplexity.ai/v1/agent" \ + -H "Authorization: Bearer $PERPLEXITY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "preset": "fast", + "input": "What are the latest breakthroughs in quantum computing?", + "stream": true + }' +``` + + +## Parsing Citation References from Text + +The model inserts numbered references like `[1]`, `[2]` into the generated text. To build a rich output with clickable links, parse these references and map them to source URLs using the search results. + + +```python Python +import re +from perplexity import Perplexity + +client = Perplexity() + + +def extract_citation_refs(text: str) -> list[int]: + """Extract all citation reference numbers from text, e.g. [1], [2].""" + return sorted(set(int(m) for m in re.findall(r"\[(\d+)\]", text))) + + +def build_cited_output(content: str, search_results: list) -> str: + """Replace [N] references with markdown links and append a references section.""" + cited_content = content + + # Build a map from id to URL + url_map = {r.id: r.url for r in search_results} + title_map = {r.id: r.title for r in search_results} + + # Replace inline references with markdown links + for ref_id, url in url_map.items(): + cited_content = cited_content.replace( + f"[{ref_id}]", + f"[[{ref_id}]]({url})" + ) + + # Append a references section with all cited sources + used_refs = extract_citation_refs(content) + if used_refs: + cited_content += "\n\n---\n**References:**\n" + for ref in used_refs: + if ref in url_map: + cited_content += f"- [{ref}] {title_map[ref]} — {url_map[ref]}\n" + + return cited_content + + +# Non-streaming request to get content + search results +response = client.responses.create( + preset="fast", + input="What is CRISPR gene editing and how does it work?", +) + +# Extract search results from the response output. +# Multi-step presets return one search_results item per research step, +# all sharing a single id space — collect the results from every item. +content = response.output_text +search_results = [] +for item in response.output: + if item.type == "search_results": + search_results.extend(item.results or []) + +# Build the final output with linked citations +output = build_cited_output(content, search_results) +print(output) +``` + +```typescript TypeScript +import Perplexity from "@perplexity-ai/perplexity_ai"; + +const client = new Perplexity(); + +function extractCitationRefs(text: string): number[] { + const refs = new Set(); + for (const match of text.matchAll(/\[(\d+)\]/g)) { + refs.add(parseInt(match[1])); + } + return [...refs].sort((a, b) => a - b); +} + +function buildCitedOutput( + content: string, + searchResults: Array<{ id: number; url: string; title: string }> +): string { + let cited = content; + + // Build maps from id to URL and title + const urlMap = new Map(searchResults.map((r) => [r.id, r.url])); + const titleMap = new Map(searchResults.map((r) => [r.id, r.title])); + + // Replace inline references with markdown links + for (const [id, url] of urlMap) { + cited = cited.replaceAll(`[${id}]`, `[[${id}]](${url})`); + } + + // Append a references section + const usedRefs = extractCitationRefs(content); + if (usedRefs.length > 0) { + cited += "\n\n---\n**References:**\n"; + for (const ref of usedRefs) { + if (urlMap.has(ref)) { + cited += `- [${ref}] ${titleMap.get(ref)} — ${urlMap.get(ref)}\n`; + } + } + } + + return cited; +} + +// Non-streaming request to get content + search results +const response = await client.responses.create({ + preset: "fast", + input: "What is CRISPR gene editing and how does it work?", +}); + +// Extract search results from the response output. +// Multi-step presets return one search_results item per research step, +// all sharing a single id space — collect the results from every item. +const content = response.output_text; +const searchResults: Array<{ id: number; url: string; title: string }> = []; +for (const item of response.output) { + if (item.type === "search_results") { + searchResults.push(...((item as any).results ?? [])); + } +} + +const output = buildCitedOutput(content, searchResults); +console.log(output); +``` + + +## Validating Citation URLs + +In production systems, you should validate that citation URLs are well-formed and reachable before presenting them to users. This avoids broken links and improves trust in the output. + + +```python Python +import asyncio +import aiohttp +from urllib.parse import urlparse + + +def is_valid_url(url: str) -> bool: + """Check that a URL has a valid structure.""" + try: + result = urlparse(url) + return all([result.scheme in ("http", "https"), result.netloc]) + except Exception: + return False + + +async def check_url_reachable(url: str, timeout: float = 5.0) -> dict: + """HEAD-request a URL to check if it's reachable.""" + if not is_valid_url(url): + return {"url": url, "valid": False, "reason": "malformed URL"} + + try: + async with aiohttp.ClientSession() as session: + async with session.head(url, timeout=aiohttp.ClientTimeout(total=timeout), allow_redirects=True) as resp: + return { + "url": url, + "valid": resp.status < 400, + "status": resp.status, + } + except asyncio.TimeoutError: + return {"url": url, "valid": False, "reason": "timeout"} + except Exception as e: + return {"url": url, "valid": False, "reason": str(e)} + + +async def validate_citations(search_results: list) -> list[dict]: + """Validate all citation URLs from search results concurrently.""" + tasks = [check_url_reachable(r.url) for r in search_results] + return await asyncio.gather(*tasks) + + +# Usage after getting a response: +# results = asyncio.run(validate_citations(search_results)) +# for r in results: +# status = "OK" if r["valid"] else f"FAILED ({r.get('reason', r.get('status'))})" +# print(f" {r['url']}: {status}") +``` + +```typescript TypeScript +function isValidUrl(url: string): boolean { + try { + const parsed = new URL(url); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +} + +async function checkUrlReachable(url: string, timeoutMs = 5000): Promise<{ url: string; valid: boolean; reason?: string; status?: number }> { + if (!isValidUrl(url)) { + return { url, valid: false, reason: "malformed URL" }; + } + + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + const resp = await fetch(url, { method: "HEAD", signal: controller.signal, redirect: "follow" }); + clearTimeout(timer); + return { url, valid: resp.status < 400, status: resp.status }; + } catch (e: any) { + return { url, valid: false, reason: e.message }; + } +} + +async function validateCitations(searchResults: Array<{ url: string }>): Promise> { + return Promise.all(searchResults.map(r => checkUrlReachable(r.url))); +} + +// Usage after getting a response: +// const results = await validateCitations(searchResults); +// results.forEach(r => { +// const status = r.valid ? "OK" : `FAILED (${r.reason ?? r.status})`; +// console.log(` ${r.url}: ${status}`); +// }); +``` + + + +**Never ask the model to generate source URLs.** Always use the `search_results` output from the API response. Model-generated URLs can be hallucinated. The search results contain verified URLs from real web searches. + + +## Progressive Display with Live Citation Count + +For chat UIs, it's useful to show a live citation counter as text streams in, then render the full reference list once the stream completes. + + +```python Python +import os +import re +import sys +from openai import OpenAI + +client = OpenAI( + api_key=os.environ["PERPLEXITY_API_KEY"], + base_url="https://api.perplexity.ai/v1", +) + + +def stream_with_progress(query: str): + """Stream a response with a live citation counter.""" + stream = client.responses.create( + input=query, + stream=True, + extra_body={"preset": "fast"}, + ) + + full_content = "" + search_results = [] + seen_refs = set() + + for event in stream: + if event.type == "response.reasoning.search_results": + search_results.extend(event.results or []) + + if event.type == "response.output_text.delta": + full_content += event.delta + sys.stdout.write(event.delta) + sys.stdout.flush() + + # Track new citation references against accumulated text + # (individual deltas may split [N] across chunks) + current_refs = set(int(m) for m in re.findall(r"\[(\d+)\]", full_content)) + if current_refs - seen_refs: + seen_refs = current_refs + sys.stdout.write(f" [📚 {len(seen_refs)} sources]") + sys.stdout.flush() + + # Final summary + print(f"\n\n{'='*60}") + print(f"Response complete: {len(search_results)} sources found, {len(seen_refs)} cited") + print(f"{'='*60}") + + # Build URL map from search results + url_map = {r["id"]: r for r in search_results} + for ref_id in sorted(seen_refs): + if ref_id in url_map: + r = url_map[ref_id] + print(f" ✓ [{ref_id}] {r['title']} — {r['url']}") + + return full_content, search_results + + +content, results = stream_with_progress( + "What are the environmental impacts of lithium mining?" +) +``` + +```typescript TypeScript +import OpenAI from "openai"; + +const client = new OpenAI({ + apiKey: process.env.PERPLEXITY_API_KEY, + baseURL: "https://api.perplexity.ai/v1", +}); + +async function streamWithProgress(query: string) { + const stream = await client.responses.create({ + input: query, + stream: true, + preset: "fast", + } as any); + + let fullContent = ""; + let searchResults: Array<{ id: number; title: string; url: string }> = []; + const seenRefs = new Set(); + + for await (const event of stream) { + if (event.type === "response.reasoning.search_results") { + searchResults.push(...((event as any).results ?? [])); + } + + if (event.type === "response.output_text.delta") { + fullContent += event.delta; + process.stdout.write(event.delta); + + // Track new citation references against accumulated text + // (individual deltas may split [N] across chunks) + const prevSize = seenRefs.size; + for (const match of fullContent.matchAll(/\[(\d+)\]/g)) { + seenRefs.add(parseInt(match[1])); + } + if (seenRefs.size > prevSize) { + process.stdout.write(` [📚 ${seenRefs.size} sources]`); + } + } + } + + console.log(`\n\n${"=".repeat(60)}`); + console.log(`Response complete: ${searchResults.length} sources found, ${seenRefs.size} cited`); + console.log("=".repeat(60)); + + const urlMap = new Map(searchResults.map((r) => [r.id, r])); + for (const refId of [...seenRefs].sort((a, b) => a - b)) { + const r = urlMap.get(refId); + if (r) { + console.log(` ✓ [${refId}] ${r.title} — ${r.url}`); + } + } + + return { fullContent, searchResults }; +} + +await streamWithProgress("What are the environmental impacts of lithium mining?"); +``` + + +## Handling Search Results + +The Agent API returns one or more `search_results` output items with rich metadata (id, title, snippet, URL, date) for each source — one item per search the model ran. This is richer than a flat URL list — use it to build source cards, sidebars, or detailed reference sections. + + +```python Python +from perplexity import Perplexity + +client = Perplexity() + +# Non-streaming request to show the full response structure +response = client.responses.create( + preset="fast", + input="What is the current state of fusion energy research?", +) + +content = response.output_text + +# Extract search results from every search_results item in the output +search_results = [] +for item in response.output: + if item.type == "search_results": + search_results.extend(item.results or []) + +print("--- Answer ---") +print(content) + +print("\n--- Search Results (rich metadata) ---") +for result in search_results: + print(f" [{result.id}] {result.title}") + print(f" URL: {result.url}") + print(f" Date: {result.date}") + print(f" Snippet: {result.snippet[:100]}...") + print() +``` + +```typescript TypeScript +import Perplexity from "@perplexity-ai/perplexity_ai"; + +const client = new Perplexity(); + +const response = await client.responses.create({ + preset: "fast", + input: "What is the current state of fusion energy research?", +}); + +const content = response.output_text; + +// Extract search results from every search_results item in the output +const searchResults: any[] = []; +for (const item of response.output) { + if (item.type === "search_results") { + searchResults.push(...((item as any).results ?? [])); + } +} + +console.log("--- Answer ---"); +console.log(content); + +console.log("\n--- Search Results (rich metadata) ---"); +for (const result of searchResults) { + console.log(` [${result.id}] ${result.title}`); + console.log(` URL: ${result.url}`); + console.log(` Date: ${result.date}`); + console.log(` Snippet: ${result.snippet?.slice(0, 100)}...`); + console.log(); +} +``` + + + +Each search result includes `id`, `title`, `url`, `snippet`, and `date`. The `id` maps directly to the `[N]` references in the text. Use this to build rich source cards for your UI. + + +## Complete Example: Streaming Research Assistant + +A self-contained script that streams an Agent API response, extracts citations, validates URLs, and produces a formatted markdown output. + + +```python Python +import os +import re +from urllib.parse import urlparse +from openai import OpenAI + +client = OpenAI( + api_key=os.environ["PERPLEXITY_API_KEY"], + base_url="https://api.perplexity.ai/v1", +) + + +def is_valid_url(url: str) -> bool: + try: + result = urlparse(url) + return all([result.scheme in ("http", "https"), result.netloc]) + except Exception: + return False + + +def stream_and_collect(query: str) -> tuple[str, list[dict]]: + """Stream an Agent API response and return the full content and search results.""" + stream = client.responses.create( + input=query, + stream=True, + extra_body={"preset": "fast"}, + ) + + content = "" + search_results = [] + + for event in stream: + if event.type == "response.reasoning.search_results": + search_results.extend(event.results or []) + + if event.type == "response.output_text.delta": + content += event.delta + print(event.delta, end="", flush=True) + + print() # newline after streaming + return content, search_results + + +def format_markdown_report(query: str, content: str, search_results: list[dict]) -> str: + """Build a markdown report with inline citation links.""" + # Build URL map from search results + url_map = {r["id"]: r["url"] for r in search_results} + title_map = {r["id"]: r["title"] for r in search_results} + + # Replace [N] with markdown links + formatted = content + for ref_id, url in url_map.items(): + if is_valid_url(url): + formatted = formatted.replace(f"[{ref_id}]", f"[\\[{ref_id}\\]]({url})") + + # Build the report + report = f"# {query}\n\n{formatted}\n\n" + + # Append sources + used_refs = sorted(set(int(m) for m in re.findall(r"\[(\d+)\]", content))) + if search_results: + report += "## Sources\n\n" + for result in search_results: + marker = "→" if result["id"] in used_refs else " " + report += f"{marker} **[{result['id']}]** {result['title']} — {result['url']}\n\n" + + return report + + +if __name__ == "__main__": + query = "What are the most promising approaches to carbon capture technology?" + + print(f"Researching: {query}\n") + print("-" * 60) + + content, search_results = stream_and_collect(query) + + print(f"\n{'=' * 60}") + print(f"Collected {len(search_results)} sources\n") + + # Filter out any malformed URLs + valid_results = [r for r in search_results if is_valid_url(r["url"])] + invalid_count = len(search_results) - len(valid_results) + if invalid_count: + print(f"Warning: {invalid_count} sources had malformed URLs and were excluded.\n") + + report = format_markdown_report(query, content, valid_results) + print(report) +``` + + +## Tips and Best Practices + +1. **Use a search-enabled preset** like `fast` or `low` for citation-rich responses. Different presets use different citation formats — `fast` uses `[1]`, while `low` uses `[web:1]`. + +2. **Accumulate search results from every event or item.** A response contains one `search_results` event (streaming) or output item (non-streaming) per search the model ran — multi-step presets run many. Append each batch to a single list; overwriting on each event or reading only the first item silently drops most sources. + +3. **Use the `id` field to map citations.** Each search result has a numeric `id` that corresponds to the `[N]` reference in the text. + +4. **Validate URLs before displaying them.** Use HEAD requests with timeouts to filter out any unreachable sources. + +5. **Never generate your own URLs.** Use only the `search_results` from the API response. Model-generated URLs can be hallucinated. + +6. **Handle missing references gracefully.** If a `[N]` reference in the text has no matching `id` in your collected search results, display the reference number without a link rather than crashing. + +7. **Consider rate limiting for URL validation.** If the response includes many sources, validate them with concurrency limits to avoid overwhelming target servers. + +## Next Steps + + + +Explore all presets and their citation formats. + + + +Get started with the Agent API for multi-provider access and tools. + + + +Streaming patterns and event types for the Agent API. + + diff --git a/docs/articles/structured-output-extraction/README.mdx b/docs/articles/structured-output-extraction/README.mdx new file mode 100644 index 0000000..6a5678b --- /dev/null +++ b/docs/articles/structured-output-extraction/README.mdx @@ -0,0 +1,637 @@ +--- +title: Structured Output Extraction +description: Get typed, schema-validated JSON responses from the Agent API using response_format with JSON schemas for data extraction, pipelines, and structured research +sidebar_position: 5 +keywords: [structured-outputs, json-schema, response-format, data-extraction, agent-api, typed-responses, data-pipelines] +products: [agent-api] +categories: [structured-outputs, function-calling] +--- + +This guide shows how to extract structured, typed JSON from the Agent API using the `response_format` parameter with JSON schemas. You will learn practical patterns for product data extraction, research findings, comparison tables, and building reliable data pipelines — all with guaranteed schema conformance. + + +The Agent API enforces your JSON schema at generation time, so responses always conform to the specified structure. For the full parameter reference, see [Output Control](/docs/agent-api/output-control). + + +## Prerequisites + +Install the Perplexity SDK: + + +```bash Python +pip install perplexityai +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +If you don't have an API key yet: + + +Navigate to the **API Keys** tab in the API Portal and generate a new key. + + +Then export your API key as an environment variable: +```bash +export PERPLEXITY_API_KEY="your-api-key" +``` + +## How Structured Outputs Work + +When you pass `response_format` with `type: "json_schema"`, the Agent API constrains the model's output to match your schema exactly. The response in `output_text` is a valid JSON string you can parse directly. + +The schema format follows [JSON Schema](https://json-schema.org/) with a few constraints specific to the Perplexity API: + +- **No recursive schemas.** The schema cannot reference itself. +- **No unconstrained objects.** Avoid `additionalProperties: true` or bare `object` types without defined properties. +- **Named schemas required.** Each schema needs a `name` field for identification. + +## Basic: Extracting a Single Entity + +Extract structured data about a single topic with web search grounding. + + +```python Python +import json +from perplexity import Perplexity + +client = Perplexity() + +response = client.responses.create( + model="openai/gpt-5.4", + input="What is the current market cap, CEO, and founding year of NVIDIA?", + tools=[{"type": "web_search"}], + response_format={ + "type": "json_schema", + "json_schema": { + "name": "company_profile", + "schema": { + "type": "object", + "properties": { + "company_name": {"type": "string"}, + "ticker": {"type": "string"}, + "ceo": {"type": "string"}, + "founded_year": {"type": "integer"}, + "market_cap_usd": {"type": "string"}, + "sector": {"type": "string"}, + "headquarters": {"type": "string"}, + }, + "required": ["company_name", "ticker", "ceo", "founded_year", "market_cap_usd", "sector", "headquarters"], + "additionalProperties": false, + }, + }, + }, +) + +company = json.loads(response.output_text) +print(f"{company['company_name']} ({company['ticker']})") +print(f" CEO: {company['ceo']}") +print(f" Founded: {company['founded_year']}") +print(f" Market Cap: {company['market_cap_usd']}") +print(f" Sector: {company['sector']}") +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What is the current market cap, CEO, and founding year of NVIDIA?", + tools: [{ type: "web_search" }], + response_format: { + type: "json_schema", + json_schema: { + name: "company_profile", + schema: { + type: "object", + properties: { + company_name: { type: "string" }, + ticker: { type: "string" }, + ceo: { type: "string" }, + founded_year: { type: "integer" }, + market_cap_usd: { type: "string" }, + sector: { type: "string" }, + headquarters: { type: "string" }, + }, + required: ["company_name", "ticker", "ceo", "founded_year", "market_cap_usd", "sector", "headquarters"], + }, + }, + }, +}); + +const company = JSON.parse(response.output_text); +console.log(`${company.company_name} (${company.ticker})`); +console.log(` CEO: ${company.ceo}`); +console.log(` Founded: ${company.founded_year}`); +console.log(` Market Cap: ${company.market_cap_usd}`); +console.log(` Sector: ${company.sector}`); +``` + + +## Extracting Lists: Product Comparisons + +Extract a structured comparison of multiple items from a single query. + + +```python Python +import json +from perplexity import Perplexity + +client = Perplexity() + +response = client.responses.create( + model="openai/gpt-5.4", + input="Compare the top 3 electric vehicles under $40,000 available in the US in 2026", + tools=[{"type": "web_search"}], + response_format={ + "type": "json_schema", + "json_schema": { + "name": "ev_comparison", + "schema": { + "type": "object", + "properties": { + "vehicles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "make": {"type": "string"}, + "model": {"type": "string"}, + "year": {"type": "integer"}, + "starting_price_usd": {"type": "integer"}, + "range_miles": {"type": "integer"}, + "battery_kwh": {"type": "number"}, + "pros": {"type": "array", "items": {"type": "string"}}, + "cons": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["make", "model", "year", "starting_price_usd", "range_miles", "battery_kwh", "pros", "cons"], + "additionalProperties": false, + }, + }, + "comparison_date": {"type": "string"}, + }, + "required": ["vehicles", "comparison_date"], + "additionalProperties": false, + }, + }, + }, +) + +data = json.loads(response.output_text) +print(f"EV Comparison (as of {data['comparison_date']})\n") + +for v in data["vehicles"]: + print(f"{v['year']} {v['make']} {v['model']}") + print(f" Price: ${v['starting_price_usd']:,}") + print(f" Range: {v['range_miles']} mi | Battery: {v['battery_kwh']} kWh") + print(f" Pros: {', '.join(v['pros'])}") + print(f" Cons: {', '.join(v['cons'])}") + print() +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "Compare the top 3 electric vehicles under $40,000 available in the US in 2026", + tools: [{ type: "web_search" }], + response_format: { + type: "json_schema", + json_schema: { + name: "ev_comparison", + schema: { + type: "object", + properties: { + vehicles: { + type: "array", + items: { + type: "object", + properties: { + make: { type: "string" }, + model: { type: "string" }, + year: { type: "integer" }, + starting_price_usd: { type: "integer" }, + range_miles: { type: "integer" }, + battery_kwh: { type: "number" }, + pros: { type: "array", items: { type: "string" } }, + cons: { type: "array", items: { type: "string" } }, + }, + required: ["make", "model", "year", "starting_price_usd", "range_miles", "battery_kwh", "pros", "cons"], + }, + }, + comparison_date: { type: "string" }, + }, + required: ["vehicles", "comparison_date"], + }, + }, + }, +}); + +const data = JSON.parse(response.output_text); +console.log(`EV Comparison (as of ${data.comparison_date})\n`); + +for (const v of data.vehicles) { + console.log(`${v.year} ${v.make} ${v.model}`); + console.log(` Price: $${v.starting_price_usd.toLocaleString()}`); + console.log(` Range: ${v.range_miles} mi | Battery: ${v.battery_kwh} kWh`); + console.log(` Pros: ${v.pros.join(", ")}`); + console.log(` Cons: ${v.cons.join(", ")}`); + console.log(); +} +``` + + +## Research Findings Extraction + +Parse search-grounded research into a structured format suitable for reports or databases. + + +```python Python +import json +from perplexity import Perplexity + +client = Perplexity() + +response = client.responses.create( + model="openai/gpt-5.4", + input="What are the most recent clinical trial results for GLP-1 receptor agonists in treating obesity?", + tools=[{"type": "web_search"}], + instructions="Provide findings from the most recent clinical trials. Include specific numbers and trial names where available.", + response_format={ + "type": "json_schema", + "json_schema": { + "name": "research_findings", + "schema": { + "type": "object", + "properties": { + "topic": {"type": "string"}, + "findings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "trial_name": {"type": "string"}, + "drug": {"type": "string"}, + "phase": {"type": "string"}, + "key_result": {"type": "string"}, + "sample_size": {"type": "string"}, + "publication_year": {"type": "integer"}, + }, + "required": ["trial_name", "drug", "phase", "key_result", "sample_size", "publication_year"], + "additionalProperties": false, + }, + }, + "summary": {"type": "string"}, + }, + "required": ["topic", "findings", "summary"], + "additionalProperties": false, + }, + }, + }, +) + +data = json.loads(response.output_text) +print(f"Topic: {data['topic']}\n") +print(f"Summary: {data['summary']}\n") + +for finding in data["findings"]: + print(f" {finding['trial_name']} ({finding['drug']}, Phase {finding['phase']})") + print(f" Result: {finding['key_result']}") + print(f" N={finding['sample_size']}, Published: {finding['publication_year']}") + print() +``` + +```typescript TypeScript +import Perplexity from '@perplexity-ai/perplexity_ai'; + +const client = new Perplexity(); + +const response = await client.responses.create({ + model: "openai/gpt-5.4", + input: "What are the most recent clinical trial results for GLP-1 receptor agonists in treating obesity?", + tools: [{ type: "web_search" }], + instructions: "Provide findings from the most recent clinical trials. Include specific numbers and trial names where available.", + response_format: { + type: "json_schema", + json_schema: { + name: "research_findings", + schema: { + type: "object", + properties: { + topic: { type: "string" }, + findings: { + type: "array", + items: { + type: "object", + properties: { + trial_name: { type: "string" }, + drug: { type: "string" }, + phase: { type: "string" }, + key_result: { type: "string" }, + sample_size: { type: "string" }, + publication_year: { type: "integer" }, + }, + required: ["trial_name", "drug", "phase", "key_result", "sample_size", "publication_year"], + }, + }, + summary: { type: "string" }, + }, + required: ["topic", "findings", "summary"], + }, + }, + }, +}); + +const data = JSON.parse(response.output_text); +console.log(`Topic: ${data.topic}\n`); +console.log(`Summary: ${data.summary}\n`); + +for (const finding of data.findings) { + console.log(` ${finding.trial_name} (${finding.drug}, Phase ${finding.phase})`); + console.log(` Result: ${finding.key_result}`); + console.log(` N=${finding.sample_size}, Published: ${finding.publication_year}`); + console.log(); +} +``` + + +## Building a Data Pipeline + +Chain structured output extraction into a pipeline that queries, extracts, and stores structured data. + + +```python Python +import json +import csv +import io +from perplexity import Perplexity + +client = Perplexity() + +SCHEMA = { + "type": "json_schema", + "json_schema": { + "name": "startup_funding", + "schema": { + "type": "object", + "properties": { + "companies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "round": {"type": "string"}, + "amount_usd": {"type": "string"}, + "lead_investor": {"type": "string"}, + "sector": {"type": "string"}, + "date": {"type": "string"}, + }, + "required": ["name", "round", "amount_usd", "lead_investor", "sector", "date"], + "additionalProperties": false, + }, + }, + }, + "required": ["companies"], + "additionalProperties": false, + }, + }, +} + + +def extract_funding_rounds(sector: str) -> list[dict]: + """Query the API and return structured funding data for a sector.""" + response = client.responses.create( + model="openai/gpt-5.4", + input=f"List the 5 largest startup funding rounds in {sector} from the past 3 months", + tools=[{"type": "web_search"}], + response_format=SCHEMA, + ) + data = json.loads(response.output_text) + return data["companies"] + + +def pipeline(sectors: list[str]) -> str: + """Run extraction across multiple sectors and produce a CSV.""" + all_rows = [] + for sector in sectors: + print(f"Extracting: {sector}...") + rows = extract_funding_rounds(sector) + for row in rows: + row["query_sector"] = sector + all_rows.append(row) + + # Convert to CSV + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=["query_sector", "name", "round", "amount_usd", "lead_investor", "sector", "date"]) + writer.writeheader() + writer.writerows(all_rows) + return output.getvalue() + + +if __name__ == "__main__": + csv_output = pipeline(["AI infrastructure", "climate tech", "biotech"]) + print(csv_output) +``` + + +## Schema Design Constraints + + +The Agent API enforces these constraints on JSON schemas: + +- **`additionalProperties` must be `false`.** Every `"type": "object"` in the schema must include `"additionalProperties": false`. This applies to the top-level schema and all nested objects. +- **No recursive schemas.** A schema cannot reference itself with `$ref` pointing to its own definition. +- **No unconstrained dicts.** Avoid `"type": "object"` without `properties`. Every object type must have explicitly defined properties. +- **All properties should be `required`.** While optional properties are allowed, making all properties required ensures consistent output structure. +- **No `$ref` to external schemas.** All definitions must be inline. + + +### Patterns That Work + +```json +// ✅ Flat object with typed fields +{ + "type": "object", + "properties": { + "name": { "type": "string" }, + "count": { "type": "integer" }, + "tags": { "type": "array", "items": { "type": "string" } } + }, + "required": ["name", "count", "tags"] +} + +// ✅ Array of typed objects +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "number" } + }, + "required": ["key", "value"] + } +} + +// ✅ Enum for constrained values +{ + "type": "string", + "enum": ["low", "medium", "high"] +} +``` + +### Patterns to Avoid + +```json +// ❌ Recursive schema (self-referencing) +{ + "type": "object", + "properties": { + "children": { "$ref": "#" } + } +} + +// ❌ Unconstrained object +{ + "type": "object", + "additionalProperties": true +} + +// ❌ Bare dict/map type +{ + "type": "object" +} +``` + +## Combining Structured Output with Function Calling + +You can use `response_format` alongside custom tools. The model calls your functions first, then formats the final response according to your schema. + + +```python Python +import json +from perplexity import Perplexity + +client = Perplexity() + +tools = [ + {"type": "web_search"}, + { + "type": "function", + "name": "get_internal_price", + "description": "Look up the internal wholesale price for a product SKU.", + "parameters": { + "type": "object", + "properties": { + "sku": {"type": "string", "description": "Product SKU"} + }, + "required": ["sku"] + }, + }, +] + + +def get_internal_price(sku: str) -> dict: + prices = {"SKU-A100": 8500, "SKU-H100": 25000, "SKU-4090": 1600} + return {"sku": sku, "wholesale_price_usd": prices.get(sku, 0)} + + +response = client.responses.create( + model="openai/gpt-5.4", + tools=tools, + input="Get the current retail price for the NVIDIA H100 GPU from the web, and also look up our internal wholesale price for SKU-H100. Compare them.", + response_format={ + "type": "json_schema", + "json_schema": { + "name": "price_comparison", + "schema": { + "type": "object", + "properties": { + "product": {"type": "string"}, + "retail_price_usd": {"type": "string"}, + "wholesale_price_usd": {"type": "integer"}, + "margin_percent": {"type": "string"}, + "source": {"type": "string"}, + }, + "required": ["product", "retail_price_usd", "wholesale_price_usd", "margin_percent", "source"], + "additionalProperties": false, + }, + }, + }, +) + +# Handle function calls +while any(item.type == "function_call" for item in response.output): + next_input = [item.model_dump() for item in response.output] + for item in response.output: + if item.type == "function_call": + args = json.loads(item.arguments) + result = get_internal_price(**args) + next_input.append({ + "type": "function_call_output", + "call_id": item.call_id, + "output": json.dumps(result), + }) + response = client.responses.create( + model="openai/gpt-5.4", + tools=tools, + input=next_input, + response_format={ + "type": "json_schema", + "json_schema": { + "name": "price_comparison", + "schema": { + "type": "object", + "properties": { + "product": {"type": "string"}, + "retail_price_usd": {"type": "string"}, + "wholesale_price_usd": {"type": "integer"}, + "margin_percent": {"type": "string"}, + "source": {"type": "string"}, + }, + "required": ["product", "retail_price_usd", "wholesale_price_usd", "margin_percent", "source"], + "additionalProperties": false, + }, + }, + }, + ) + +data = json.loads(response.output_text) +print(f"Product: {data['product']}") +print(f"Retail: {data['retail_price_usd']} (from {data['source']})") +print(f"Wholesale: ${data['wholesale_price_usd']:,}") +print(f"Margin: {data['margin_percent']}") +``` + + + +When combining structured outputs with function calling, pass the same `response_format` in every turn of the multi-turn loop. The schema is only enforced on the final text output, not on function call arguments. + + +## Next Steps + + + +Full reference for response_format, streaming, and output shaping. + + + +Combine structured outputs with multi-turn function calling. + + + +Get started with the Agent API in minutes. + + + +Choose the right model for structured extraction tasks. + + diff --git a/docs/examples/README.mdx b/docs/examples/README.mdx index 0e846cb..99717bf 100644 --- a/docs/examples/README.mdx +++ b/docs/examples/README.mdx @@ -1,192 +1,133 @@ --- title: Examples Overview -description: Ready-to-use applications demonstrating Perplexity Sonar API capabilities +description: Runnable projects covering the Agent API, Search API, and Embeddings API sidebar_position: 1 -keywords: [examples, applications, demos, sonar-api] +keywords: [examples, applications, demos, agent-api, search-api, embeddings] --- # Examples Overview -Welcome to the **Perplexity Sonar API Examples** collection! These are production-ready applications that demonstrate real-world use cases of the Sonar API. +Ready-to-run projects that demonstrate real-world use cases across the Perplexity APIs. Each example includes complete setup instructions and working code. + +## Choosing the Right Example + +| If you want to... | Use this example | API | Language | +|---|---|---|---| +| Learn how Agent API presets change a workflow | [Customizing Presets](/docs/cookbook/examples/agent-api-presets/README) | Agent API | Python, TypeScript, cURL | +| Conduct deep web research | [Agent Research Assistant](/docs/cookbook/examples/agent-research-assistant/README) | Agent API | Python, TypeScript | +| Turn research into an interactive report | [Grounded Data Story with Kimi K3](/docs/cookbook/examples/data-story-kimi-k3/README) | Agent API | Python | +| Compare models across providers | [Model Comparison](/docs/cookbook/examples/model-comparison/README) | Agent API | Python | +| Monitor news topics in real time | [Search News Monitor](/docs/cookbook/examples/search-news-monitor/README) | Search API | Python, TypeScript | +| Build a document Q&A system | [Document Q&A](/docs/cookbook/examples/document-qa/README) | Embeddings + Agent API | Python | +| Build a TypeScript CLI agent | [TypeScript Agent CLI](/docs/cookbook/examples/typescript-agent-cli/README) | Agent API | TypeScript | +| Analyze images with web context | [Image Analysis](/docs/cookbook/examples/image-analysis/README) | Agent API | Python, TypeScript | +| Ask questions about uploaded files | [File Attachment Q&A](/docs/cookbook/examples/file-attachment-qa/README) | Agent API | Python | +| Search SEC filings for financial data | [SEC Filing Search](/docs/cookbook/examples/sec-filing-search/README) | Agent API | Python | +| Run code and build a PDF in a sandbox | [Competitor Buzz Tracker](/docs/cookbook/examples/competitor-buzz-tracker/README) | Agent API | Python | +| Chart historical stock prices in a sandbox | [Finance Chart](/docs/cookbook/examples/finance-chart-sandbox/README) | Agent API | Python | +| Generate a public-company research brief | [Equity Research Brief](/docs/cookbook/examples/equity-research-brief/README) | Agent API | Python | +| Analyze a newly listed company | [SpaceX Financial Briefing](/docs/cookbook/examples/spacex-spcx-briefing/README) | Agent API | Python | +| Source candidates for a hiring brief | [Talent Sourcer](/docs/cookbook/examples/talent-sourcer/README) | Agent API | Python | +| Pick an open model via an MCP server | [Model Picker](/docs/cookbook/examples/model-picker/README) | Agent API | Python | -## 🚀 Quick Start +## By API -Navigate to any example directory and follow the instructions in the README.md file. +### Agent API -## 📋 Available Examples + + +Compare Agent API presets, inspect their behavior, and override individual settings. + -### 🔍 [Fact Checker CLI](fact-checker-cli/) + +Deep web research using the `medium` preset with structured report output. + -**Purpose**: Verify claims and articles for factual accuracy -**Type**: Command-line tool -**Use Cases**: Journalism, research, content verification + +Research a topic and turn the findings into a source-linked interactive HTML report. + -**Key Features**: -- Structured claim analysis with ratings -- Source citation and evidence tracking -- JSON output for automation -- Professional fact-checking workflow + +Compare responses from 5 providers side-by-side — quality, latency, and cost. + -**Quick Start**: -```bash -cd fact-checker-cli/ -python fact_checker.py --text "The Earth is flat" -``` - ---- + +Interactive TypeScript CLI with streaming, model selection, and web search. + -### 🤖 [Daily Knowledge Bot](daily-knowledge-bot/) + +Vision + web search for context-enriched image analysis. + -**Purpose**: Automated daily fact delivery system -**Type**: Scheduled Python application -**Use Cases**: Education, newsletters, personal learning + +Upload documents and ask questions about them with optional web search enrichment. + -**Key Features**: -- Topic rotation based on calendar -- Persistent storage of facts -- Configurable scheduling -- Educational content generation + +Two chained agent requests: the sandbox searches and counts each brand's share of voice, then renders a downloadable bar-chart PDF. + -**Quick Start**: -```bash -cd daily-knowledge-bot/ -python daily_knowledge_bot.py -``` + +Fetch historical prices and render CSV and PNG artifacts inside the Agent API sandbox. + ---- + +Generate a structured equity brief with the `finance_search` tool. + -### 🏥 [Disease Information App](disease-qa/) + +Research a newly listed ticker with finance data, web search, and SEC sources. + -**Purpose**: Interactive medical information lookup -**Type**: Web application (HTML/JavaScript) -**Use Cases**: Health education, medical reference, patient information + +Search SEC.gov and EDGAR for financial filings with structured data extraction. + -**Key Features**: -- Interactive browser interface -- Structured medical knowledge cards -- Citation tracking for medical sources -- Standalone deployment ready + +Wide search in a sandbox: source engineers by skill, location, and tenure, verify each, and rank them with their GitHub and profile links into an HTML shortlist. + -**Quick Start**: -```bash -cd disease-qa/ -jupyter notebook disease_qa_tutorial.ipynb -``` + +Connect the Hugging Face MCP server and web search in one request: find open models on the live Hub, verify their downloads and license, and check benchmarks before recommending one. + + ---- +### Search API -### 📊 [Financial News Tracker](financial-news-tracker/) + + +Multi-topic news monitoring with domain filtering and recency control. + + -**Purpose**: Real-time financial news monitoring and market analysis -**Type**: Command-line tool -**Use Cases**: Investment research, market monitoring, financial journalism +### Embeddings API -**Key Features**: -- Real-time financial news aggregation -- Market sentiment analysis (Bullish/Bearish/Neutral) -- Impact assessment and sector analysis -- Investment insights and recommendations + + +Self-contained RAG system with contextualized embeddings and Agent API answer generation. + + + +## API Key Setup + +All examples require a Perplexity API key. Set it as an environment variable: -**Quick Start**: ```bash -cd financial-news-tracker/ -python financial_news_tracker.py "tech stocks" +export PERPLEXITY_API_KEY="your-api-key-here" ``` ---- - -### 📈 [Equity Research Brief](equity-research-brief/) + +Get your API key at [perplexity.ai/account/api](https://perplexity.ai/account/api). + -**Purpose**: Generate institutional-grade equity research briefs for any public ticker -**Type**: Command-line tool -**Use Cases**: Investor workflows, fundamental analysis, earnings prep, peer benchmarking +## Common Requirements -**Key Features**: -- Uses the Agent API's built-in `finance_search` tool for structured fundamentals -- Three preset configurations (live quote, single-company, multi-step research) -- Cites Perplexity finance source URLs alongside the brief -- Reports `finance_search` invocation count and total request cost - -**Quick Start**: -```bash -cd equity-research-brief/ -python equity_research_brief.py NVDA -``` - ---- - -### 📚 [Academic Research Finder](research-finder/) - -**Purpose**: Academic literature discovery and summarization -**Type**: Command-line research tool -**Use Cases**: Academic research, literature reviews, scholarly work - -**Key Features**: -- Academic source prioritization -- Paper citation extraction with DOI links -- Research-focused prompting -- Scholarly workflow integration - -**Quick Start**: -```bash -cd research-finder/ -python research_finder.py "quantum computing advances" -``` - -## 🔑 API Key Setup - -All examples require a Perplexity API key. You can set it up in several ways: - -### Environment Variable (Recommended) -```bash -export PPLX_API_KEY="your-api-key-here" -``` - -### .env File -Create a `.env` file in the example directory: -```bash -PERPLEXITY_API_KEY=your-api-key-here -``` - -### Command Line Argument -```bash -python script.py --api-key your-api-key-here -``` - -## 🛠️ Common Requirements - -All examples require: -- **Python 3.7+** -- **Perplexity API Key** ([Get one here](https://docs.perplexity.ai/guides/getting-started)) +- **Python 3.9+** or **Node.js 18+** (depending on the example) +- **Perplexity API Key** - **Internet connection** for API calls -Additional requirements vary by example and are listed in each `requirements.txt` file. - -## 🎯 Choosing the Right Example - -| **If you want to...** | **Use this example** | -|------------------------|----------------------| -| Verify information accuracy | **Fact Checker CLI** | -| Learn something new daily | **Daily Knowledge Bot** | -| Look up medical information | **Disease Information App** | -| Track financial markets | **Financial News Tracker** | -| Generate an equity research brief | **Equity Research Brief** | -| Research academic topics | **Academic Research Finder** | - -## 🤝 Contributing - -Found a bug or want to improve an example? We welcome contributions! +Additional requirements vary by example and are listed in each project's documentation. -1. **Report Issues**: Open an issue describing the problem -2. **Suggest Features**: Propose new functionality or improvements -3. **Submit Code**: Fork, implement, and submit a pull request - -See our [Contributing Guidelines](https://github.com/ppl-ai/api-cookbook/blob/main/CONTRIBUTING.md) for details. - -## 📄 License - -All examples are licensed under the [MIT License](https://github.com/ppl-ai/api-cookbook/blob/main/LICENSE). - ---- +## Contributing -**Ready to explore?** Pick an example above and start building with Perplexity's Sonar API! 🚀 +Found a bug or want to add an example? See our [Contributing Guidelines](https://github.com/perplexityai/api-cookbook/blob/main/CONTRIBUTING.md). diff --git a/docs/examples/agent-api-presets/README.mdx b/docs/examples/agent-api-presets/README.mdx new file mode 100644 index 0000000..08ad0b6 --- /dev/null +++ b/docs/examples/agent-api-presets/README.mdx @@ -0,0 +1,241 @@ +--- +title: "Customizing Presets" +sidebar_position: 21 +keywords: [agent-api, presets, configuration, tool-options, reproducibility, python] +products: [agent-api] +categories: [] +--- + +Models, tools, and capabilities change quickly. It can be a full time job to keep your agent code updated with the ideal configurations for your use case. That's what Perplexity presets help solve. + +## Start from a preset + +A preset is a Perplexity maintained bundle of Agent API settings that packages together a model, search config, reasoning steps, system prompt, and available tools. Perplexity updates the underlying configurations as evaluations improve, and your calls receive the updates without needing to adjust your code. + +What if the chosen preset doesn't quite meet all of your needs? Imagine, for instance, that you found a preset configuration that almost perfectly meets your needs, with the exception of one or two fields that you'd like to tune. You can pass your preset by name and then modify only the fields that need adjustment. All the other preset fields will continue to use their defaults. + +## Check the prerequisites + +You need Python 3.10 or newer, the `perplexityai` library installed, and an API key exported as `PERPLEXITY_API_KEY`. Create the key at [console.perplexity.ai/group/keys](https://console.perplexity.ai/group/keys). If you have never called the API before, run through the [Perplexity API quickstart](https://docs.perplexity.ai/docs/getting-started/quickstart) first. + +```bash +pip install perplexityai +export PERPLEXITY_API_KEY="pplx-..." +``` + +## Run a basic example + +Every example in this tutorial uses the `low` preset unless noted. Start by calling it with nothing but a prompt. + +```python +from perplexity import Perplexity + +client = Perplexity() + +response = client.responses.create( + preset="low", + input="Summarize the current Perplexity Agent API pricing page.", +) + +print("model:", response.model) +print(response.output_text) +``` + +The `low` preset supplies the model, search config, reasoning steps, system prompt, and available tools. Your request adds only the input. + +## Customize a preset in two moves + +Two techniques cover almost every real customization: override a top-level field, and adjust options for one tool. The last section shows how to inspect what ran. + +### 1. Override one parameter + +Override a parameter when the preset almost fits but one field needs to change. Pass that field on the request; every other field keeps its default value. + +`low` documents a low `max_steps` default (check the current presets documentation for the current value). Raise the ceiling when a task needs more reasoning or tool-use iterations: + +```python +response = client.responses.create( + preset="low", + input="Summarize this week's Agent API changelog.", + max_steps=8, +) + +print("model:", response.model) +print("status:", response.status) +print("tool invocations:", response.usage.tool_calls_details) +``` + +### 2. Adjust options for one tool + +Adjust tool options when the preset's tool set is right for the job but one tool needs tuning. Pass a partial entry for that tool and the preset's other tools stay attached. + +`low` invokes `fetch_url` by default when a prompt names a URL. Pass a partial `web_search` override and `fetch_url` still runs: + +```python +response = client.responses.create( + preset="low", + input=( + "Read https://docs.perplexity.ai/docs/agent-api/presets " + "and list the preset names on that page." + ), + tools=[{ + "type": "web_search", + "max_tokens": 6000, + "max_tokens_per_page": 1200, + }], +) + +print("model:", response.model) +print("tools ran:", list(response.usage.tool_calls_details.keys())) +``` + +Live run on August 19, 2026 with `perplexityai==0.43.3`: + +```text +model: openai/gpt-5.6-luna +tools ran: ['fetch_url'] +``` + +The request adjusted `web_search`, but `fetch_url` is what actually ran because the prompt asked to read a URL and `fetch_url` is the tool for that job. The evidence is `usage.tool_calls_details`: `fetch_url` appears there even though the request never passed a `fetch_url` entry. That is tool merging. To also tune `fetch_url`, add a `fetch_url` entry to `tools` alongside `web_search`. + +## Put both moves together: an evidence-based rollout decision + +Let's tie these concepts together. Suppose the performance lead for a CPU-bound service is deciding whether to pilot Python 3.14's free-threaded build. `low` is a good base, but this task needs more reasoning room (an override) and deeper context from two specific technical pages (a tool merge). + +```python +from perplexity import Perplexity + +client = Perplexity() + +response = client.responses.create( + preset="low", + input=( + "You are the performance lead for a CPU-bound fraud-scoring service. " + "The team proposes piloting Python 3.14's free-threaded build in " + "production. Create an adoption brief of at most 450 words with exactly " + "these Markdown headings: ## Decision, ## Three technical risks, " + "## Five benchmark gates, and ## Two stop conditions. Make each list " + "match the number in its heading. For every risk, separate the " + "documented behavior from its implication for this service. Use only " + "the sources allowed by the web_search tool. Cite factual claims inline " + "using numeric result IDs, one source per bracket, like [1][2]. Never " + "invent citation IDs. Do not include shell commands." + ), + max_steps=8, + tools=[{ + "type": "web_search", + "search_context_size": "high", + "filters": { + "search_domain_filter": [ + "docs.python.org/3.14/whatsnew/3.14.html", + "docs.python.org/3/howto/free-threading-python.html", + ], + }, + }], +) + +print(response.output_text) + +print("\n--- Observed run ---") +print("model: ", response.model) +print("status: ", response.status) +print("tools: ", response.usage.tool_calls_details) +print("total cost: $", response.usage.cost.total_cost) +``` + +Every piece of the call has a job: + +| Concern | Supplied by | +|---|---| +| Maintained model, system prompt, reasoning, defaults, and unlisted tools | `low` preset | +| Application task and output contract | `input` | +| Additional reasoning room | `max_steps=8` override | +| Deeper evidence extraction | `search_context_size="high"` tool merge | +| Trusted-source policy | URL-level `search_domain_filter` tool merge | +| Runtime evidence | Response metadata | + +A representative live run on August 19, 2026 with `perplexityai==0.43.3` produced this brief (`response.output_text`): + +```markdown +## Decision + +Pilot Python 3.14's free-threaded build in production only as a tightly scoped canary, not as the default runtime. Python 3.14 officially supports free-threading, but it remains optional; third-party extension compatibility and workload-specific scaling are still material uncertainties. [1][2] Require a GIL-enabled rollback path, identical capacity controls, and the benchmark gates below before expanding traffic. + +## Three technical risks + +1. **Documented behavior:** Free-threaded execution adds single-thread overhead: approximately 1-8% across `pyperformance`, depending on platform and hardware. [2] **Implication:** A fraud request that is mostly serial Python may become slower or require more CPU, even if multi-threaded throughput improves. + +2. **Documented behavior:** Some C extensions are not ready for free-threaded operation and can automatically re-enable the GIL when imported; a warning is emitted. [2] **Implication:** The service's supposedly parallel workers could silently serialize, making throughput and tail-latency results misleading. Audit every dependency and fail the pilot if the GIL is re-enabled unexpectedly. + +3. **Documented behavior:** Built-in containers provide protections resembling GIL-era behavior, but shared iterators are generally not thread-safe, and concurrent frame access can crash the interpreter. [2] **Implication:** Existing "safe because of the GIL" assumptions in feature extraction, caching, or model plumbing may produce races, corrupted results, or process crashes. Treat shared mutable state as requiring explicit synchronization. + +## Five benchmark gates + +1. **Correctness:** Run production-representative replay against the GIL build; require identical fraud decisions, scores, error classifications, and audit records. + +2. **Throughput:** At the target core allocation and realistic thread count, require at least 1.20x sustained requests/second versus the GIL build. + +3. **Latency:** At p50, p95, and p99 under peak load, require no regression greater than 5%, with zero missed service-level objectives. + +4. **CPU efficiency:** Require at least 10% lower CPU-seconds per scored request at equal traffic; separately measure scaling as workers increase, because free-threading does not automatically benefit every program. [2] + +5. **Operational safety:** Soak-test for 24 hours with production dependency versions; require zero crashes, deadlocks, data races, unexpected GIL re-enablement warnings, or memory growth beyond the GIL build's agreed budget. Free-threaded builds typically use more memory. [2] + +## Two stop conditions + +1. **Immediate rollback:** Stop the pilot and revert to the GIL build if correctness differs, the SLO is breached, a crash/deadlock occurs, or any dependency re-enables the GIL in the production path. + +2. **No-go after pilot:** Do not expand beyond the canary if any benchmark gate fails, especially if throughput gains do not compensate for the documented single-thread overhead, or if memory/capacity cost exceeds the approved budget. +``` + +And this observed-run block: + +```text +model: openai/gpt-5.6-luna +status: completed +tools: {'search_web': ToolCallDetailsOutput(invocation=2)} +total cost: $0.00672 +``` + +The brief came in at 410 words, followed every requested heading and list count, used valid numeric citations, and drew only from the two allowed official Python pages. Factual spot checks confirmed its claims about parallel execution, extension-triggered GIL re-enablement, iterator safety, official Python 3.14 support, and the documented single-thread performance penalty. + +Output will vary between runs. Generated rollout advice is still a draft: validate citation IDs against the response's search results and review consequential recommendations before using them in production. + +## Inspect what actually ran + +You need a way to check what the API actually served. Read `response.model` for the backing model, `response.usage.tool_calls_details` for the tools that ran, and `response.usage.cost.total_cost` for the billed amount. The response does not expose the effective system prompt, `max_steps`, reasoning, or the full inherited tool set, so treat this as inspection of the observed run rather than of the preset's configuration. + +```python +def inspect(preset: str, prompt: str) -> None: + response = client.responses.create(preset=preset, input=prompt) + print(f"preset={preset}") + print(f" model: {response.model}") + print(f" status: {response.status}") + print(f" invocations: {response.usage.tool_calls_details}") + print(f" total_cost: ${response.usage.cost.total_cost}") + +inspect("low", "Summarize the current Perplexity Agent API pricing page.") +``` + +Your numbers will differ. Live run on August 19, 2026 with `perplexityai==0.43.3`: + +```text +preset=low + model: openai/gpt-5.6-luna + status: completed + invocations: {'fetch_url': ToolCallDetailsOutput(invocation=1), 'search_web': ToolCallDetailsOutput(invocation=1)} + total_cost: $0.01812 +``` + +Inspect what ran on any request where correctness or cost matters. It lets you see which model handled the call, which tools ran, and the call's cost. + +## Summary + +Presets give you a maintained Agent API configuration you can call by name. Override one field to change one thing without losing the other defaults. Merge tool options to tune a tool while keeping the preset's other available tools attached. Read `response.model` and `response.usage.tool_calls_details` to inspect your calls. + +## Resources + +- [Agent API presets](https://docs.perplexity.ai/docs/agent-api/presets) +- [Agent API quickstart](https://docs.perplexity.ai/docs/agent-api/quickstart) +- [Web Search](https://docs.perplexity.ai/docs/agent-api/tools/web-search) +- [Perplexity API pricing](https://docs.perplexity.ai/docs/getting-started/pricing) diff --git a/docs/examples/agent-research-assistant/README.mdx b/docs/examples/agent-research-assistant/README.mdx new file mode 100644 index 0000000..897fc85 --- /dev/null +++ b/docs/examples/agent-research-assistant/README.mdx @@ -0,0 +1,353 @@ +--- +title: Agent Research Assistant +description: A CLI tool that uses Perplexity's Agent API with the medium preset to conduct multi-step web research and produce structured reports +sidebar_position: 7 +keywords: [agent-api, medium, structured-output, research, cli, reports] +products: [agent-api] +categories: [deep-research, structured-outputs] +--- + +# Agent Research Assistant + +A command-line research tool that leverages Perplexity's Agent API with the `medium` preset to conduct thorough, multi-step web research on any topic. The tool produces structured reports with sections, cited sources, and confidence scores. + +## Features + +- Multi-step web research powered by the `medium` preset +- Structured JSON output with sections, sources, and confidence scores using `response_format` with `json_schema` +- Configurable model selection (defaults to `openai/gpt-5.2` via the medium preset) +- Clean CLI interface that accepts a topic and outputs a formatted report +- Source tracking with URLs and relevance annotations +- Exportable reports in JSON or plain text + +## Installation + + +```bash Python +pip install perplexityai pydantic +``` + +```bash TypeScript +npm install @perplexity-ai/perplexity_ai +``` + + +## API Key Setup + +Set your Perplexity API key as an environment variable. The SDK reads it automatically: + +```bash +export PERPLEXITY_API_KEY="your_api_key_here" +``` + +## Usage + +```bash +# Python +python research_assistant.py "Impact of microplastics on marine ecosystems" + +# TypeScript +npx ts-node research_assistant.ts "Impact of microplastics on marine ecosystems" + +# Override the default model +python research_assistant.py "Quantum computing breakthroughs" --model openai/gpt-5.4 + +# Export as JSON +python research_assistant.py "CRISPR gene therapy trials" --json > report.json +``` + +## How It Works + +1. The CLI accepts a research topic as input. +2. A structured JSON schema is defined for the report format using Pydantic (Python) or a TypeScript interface. +3. The tool calls the Agent API with `preset="medium"`, which configures the model (`openai/gpt-5.2`), enables `web_search` and `fetch_url` tools, and allows up to 10 reasoning steps. +4. The `response_format` parameter with `json_schema` enforces structured output matching the report schema. +5. The response is parsed and displayed as a formatted research report. + + +The `medium` preset is optimized for complex, in-depth analysis. It uses `openai/gpt-5.2` with up to 10K max tokens and 10 reasoning steps. You can override the model by passing `--model` to the CLI. + + +## Full Code + + +```python Python +import json +import argparse +from typing import List, Optional +from pydantic import BaseModel +from perplexity import Perplexity + + +class ReportSource(BaseModel): + title: str + url: str + relevance: str + + +class ReportSection(BaseModel): + heading: str + content: str + confidence: float + sources: List[ReportSource] + + +class ResearchReport(BaseModel): + title: str + summary: str + sections: List[ReportSection] + conclusion: str + overall_confidence: float + total_sources: int + + +def run_research(topic: str, model: Optional[str] = None) -> ResearchReport: + """Conduct deep research on a topic and return a structured report.""" + client = Perplexity() + + params = { + "preset": "medium", + "input": ( + f"Conduct thorough research on the following topic and produce a " + f"detailed report with multiple sections, cited sources, and " + f"confidence scores for each section.\n\nTopic: {topic}" + ), + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "research_report", + "schema": ResearchReport.model_json_schema(), + }, + }, + } + + if model: + params["model"] = model + + response = client.responses.create(**params) + return ResearchReport.model_validate_json(response.output_text) + + +def format_report(report: ResearchReport) -> str: + """Format a ResearchReport into human-readable text.""" + lines = [f"{'=' * 60}", f"RESEARCH REPORT: {report.title}", f"{'=' * 60}", ""] + lines += [f"SUMMARY:", report.summary, ""] + + for i, section in enumerate(report.sections, 1): + lines.append(f"--- Section {i}: {section.heading} ---") + lines.append(f"Confidence: {section.confidence:.0%}\n") + lines.append(section.content) + if section.sources: + lines.append("\nSources:") + for src in section.sources: + lines.append(f" - {src.title} ({src.relevance})") + lines.append(f" {src.url}") + lines.append("") + + lines += [f"{'=' * 60}", "CONCLUSION:", report.conclusion, ""] + lines += [f"Overall Confidence: {report.overall_confidence:.0%}"] + lines += [f"Total Sources: {report.total_sources}", f"{'=' * 60}"] + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Agent Research Assistant") + parser.add_argument("topic", help="The research topic") + parser.add_argument("--model", help="Override the default model", default=None) + parser.add_argument("--json", action="store_true", help="Output raw JSON") + args = parser.parse_args() + + print(f"Researching: {args.topic}") + print("This may take a moment (deep research uses multi-step reasoning)...\n") + + report = run_research(args.topic, model=args.model) + + if args.json: + print(json.dumps(report.model_dump(), indent=2)) + else: + print(format_report(report)) + + +if __name__ == "__main__": + main() +``` + +```typescript TypeScript +import Perplexity from "@perplexity-ai/perplexity_ai"; + +interface ReportSource { + title: string; + url: string; + relevance: string; +} + +interface ReportSection { + heading: string; + content: string; + confidence: number; + sources: ReportSource[]; +} + +interface ResearchReport { + title: string; + summary: string; + sections: ReportSection[]; + conclusion: string; + overall_confidence: number; + total_sources: number; +} + +const reportSchema = { + type: "object" as const, + properties: { + title: { type: "string" }, + summary: { type: "string" }, + sections: { + type: "array", + items: { + type: "object", + properties: { + heading: { type: "string" }, + content: { type: "string" }, + confidence: { type: "number" }, + sources: { + type: "array", + items: { + type: "object", + properties: { + title: { type: "string" }, + url: { type: "string" }, + relevance: { type: "string" }, + }, + required: ["title", "url", "relevance"], + }, + }, + }, + required: ["heading", "content", "confidence", "sources"], + }, + }, + conclusion: { type: "string" }, + overall_confidence: { type: "number" }, + total_sources: { type: "number" }, + }, + required: ["title", "summary", "sections", "conclusion", "overall_confidence", "total_sources"], +}; + +async function runResearch(topic: string, model?: string): Promise { + const client = new Perplexity(); + + const params: Record = { + preset: "medium", + input: + `Conduct thorough research on the following topic and produce a ` + + `detailed report with multiple sections, cited sources, and ` + + `confidence scores for each section.\n\nTopic: ${topic}`, + response_format: { + type: "json_schema", + json_schema: { name: "research_report", schema: reportSchema }, + }, + }; + + if (model) params.model = model; + + const response = await client.responses.create(params as any); + return JSON.parse(response.output_text) as ResearchReport; +} + +async function main() { + const topic = process.argv[2]; + if (!topic) { + console.error("Usage: ts-node research_assistant.ts [--model ] [--json]"); + process.exit(1); + } + + const modelIdx = process.argv.indexOf("--model"); + const model = modelIdx !== -1 ? process.argv[modelIdx + 1] : undefined; + const outputJson = process.argv.includes("--json"); + + console.log(`Researching: ${topic}`); + console.log("This may take a moment (deep research uses multi-step reasoning)...\n"); + + const report = await runResearch(topic, model); + + if (outputJson) { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(`RESEARCH REPORT: ${report.title}\n`); + console.log(`SUMMARY: ${report.summary}\n`); + report.sections.forEach((s, i) => { + console.log(`--- Section ${i + 1}: ${s.heading} (${(s.confidence * 100).toFixed(0)}%) ---`); + console.log(s.content); + s.sources.forEach((src) => console.log(` - ${src.title}: ${src.url}`)); + console.log(); + }); + console.log(`CONCLUSION: ${report.conclusion}`); + console.log(`Overall Confidence: ${(report.overall_confidence * 100).toFixed(0)}%`); + } +} + +main(); +``` + + +## Example Output + +```bash +python research_assistant.py "Impact of microplastics on marine ecosystems" +``` + +``` +Researching: Impact of microplastics on marine ecosystems +This may take a moment (deep research uses multi-step reasoning)... + +============================================================ +RESEARCH REPORT: Impact of Microplastics on Marine Ecosystems +============================================================ + +SUMMARY: +Microplastics have become a pervasive pollutant in marine environments +worldwide, affecting organisms from plankton to large marine mammals. + +--- Section 1: Sources and Distribution --- +Confidence: 92% + +Microplastics originate from the degradation of larger plastic debris, +synthetic textiles, industrial processes, and cosmetic products... + +Sources: + - NOAA Marine Debris Program (high) + https://marinedebris.noaa.gov/... + +--- Section 2: Biological Effects on Marine Organisms --- +Confidence: 88% + +Research demonstrates that microplastics affect marine life at multiple +trophic levels... + +Sources: + - Environmental Science & Technology (high) + https://pubs.acs.org/... + +============================================================ +CONCLUSION: +Microplastics pose a significant and growing threat to marine ecosystems. + +Overall Confidence: 89% +Total Sources: 12 +============================================================ +``` + + +For shorter, faster research tasks, consider using the `low` preset instead. It uses `openai/gpt-5.4` with up to 3 reasoning steps -- a good balance of speed and thoroughness. + + + +The first request with a new JSON Schema may take 10 to 30 seconds to prepare. Subsequent requests with the same schema will not see this delay. See the [structured outputs guide](/docs/agent-api/output-control#structured-outputs) for details. + + +## Limitations + +- Deep research requests consume more tokens and cost more than standard requests due to multi-step reasoning and tool usage. +- Structured output with JSON schema requires the model to adhere to the schema. Very complex schemas may reduce output quality. +- Confidence scores are model-generated estimates and should be treated as relative indicators, not absolute measures. +- The quality of research depends on the availability and quality of web sources for the given topic. diff --git a/docs/examples/competitor-buzz-tracker/README.mdx b/docs/examples/competitor-buzz-tracker/README.mdx new file mode 100644 index 0000000..9a94e44 --- /dev/null +++ b/docs/examples/competitor-buzz-tracker/README.mdx @@ -0,0 +1,661 @@ +--- +title: Competitor Buzz Tracker +description: Turn a basket of searches and keyword rules into a one-page share-of-voice chart (PDF) with two chained Agent API requests. The first searches and counts inside the sandbox and returns a structured-output JSON contract. The second renders the bar chart and shares it as a downloadable file. +sidebar_position: 16 +keywords: [agent-api, sandbox, structured-outputs, code-execution, share-of-voice, competitive-analysis, news, pdf, background] +products: [agent-api] +categories: [sandbox, structured-outputs] +--- + +# Competitor Buzz Tracker + +A command-line example that turns a product and its competitors into a one-page +competitive news report (PDF): how many of the articles in the news right now +mention each brand, and each brand's share of the total. You hand the tool a **basket** — a few +searches plus keyword rules — and the model does the rest. + +It does this by writing the code itself. Driving the +[`sandbox`](https://docs.perplexity.ai/docs/agent-api/tools/sandbox) tool, the +model writes Python, runs it in the sandbox, and loops — searching the web, +deduplicating and classifying the results, fixing its own errors, and re-running +— all server-side. You never run any analysis or charting code locally: the +script just submits the requests, polls the background responses, and downloads +the finished PDF. Every number on the chart is computed, not guessed. + + + Competitor Buzz Tracker PDF: horizontal bar chart of news mentions for Galaxy, iPhone, Pixel, and Other, each labeled with its total and share of voice. + Competitor Buzz Tracker PDF: horizontal bar chart of news mentions for Galaxy, iPhone, Pixel, and Other, each labeled with its total and share of voice. + + +## What the sandbox does here + +- **Runs the analysis as code, not from memory.** Like a code interpreter, the + sandbox lets the model solve a quantitative task by writing and running Python + instead of guessing. The mention counts and share-of-voice percentages come + from code it actually executed over the search results — so the numbers are + real, not plausible-sounding. The script enforces this: it checks the response + contains a `sandbox_results` item and refuses the result otherwise, so the + model can't skip the tool and return invented counts. +- **Searches the web in the same run.** The sandbox can reach Perplexity search + from inside the run, so the model pulls the articles itself and classifies them + in the same request — no separate scraping step, no glue code, no extra tool to + wire up. +- **Returns a real file with zero setup on your side.** matplotlib and the + runtime live in the sandbox; the model renders the chart, shares it with + `share_file`, and you download the `.pdf` from the response by id. One request + in, one file out — nothing to install or host locally. A plain chat completion + would only return text. + +## Without the sandbox + +To build the same report yourself, you'd stand up a runtime: a machine with +Python and matplotlib, the search and classification code, and somewhere to +execute it and capture the file. With the `sandbox` tool the model writes and +runs that code server-side and hands back the finished PDF — nothing to install, +host, or keep running — and it adapts the code to whatever the search returns +instead of you maintaining a rigid pipeline. + +## Installation + +Keep the project files in the same directory: +`competitor_buzz_tracker.py`, `observability.py` (imported by the script), +`requirements.txt`, and your `basket.yaml`. + +1. Install the dependencies — the [Perplexity Python SDK](https://github.com/ppl-ai/perplexity-python), + PyYAML (to read the basket config), and Pydantic (for the response schema). + They're pinned in `requirements.txt`: + +```text requirements.txt +perplexityai==0.38.0 +PyYAML==6.0.2 +pydantic==2.13.4 +``` + +```bash +pip install -r requirements.txt +``` + +2. Set your Perplexity API key: + +```bash +export PERPLEXITY_API_KEY="your-api-key-here" +``` + +The SDK reads the key from this environment variable. + + +This example uses the Agent API `sandbox` tool. See the +[Sandbox docs](https://docs.perplexity.ai/docs/agent-api/tools/sandbox) for +setup and usage details. + + +## Usage + +You describe the job in a small YAML basket: a chart title, the search +queries to run, and the keyword rules that classify each result. One article can +match several keywords — a story that mentions both Pixel and Galaxy counts for +both; one that matches none counts under "Other". More queries mean broader +coverage. + +```yaml basket.yaml +title: "iPhone vs Pixel vs Galaxy — market buzz" + +queries: + - "smartphone news today" + - "latest phone news" + - "new phone launch" + - "smartphone announcements" + - "flagship smartphone news" + - "Android phone news" + - "new phone releases" + - "phone review roundup" + - "best new phones" + - "upcoming smartphones" + - "foldable phone news" + - "budget phone news" + - "phone camera comparison" + - "smartphone deals this week" + - "mobile phone industry news" + +keywords: + - name: iPhone + regex: "iphone|apple phone" + - name: Pixel + regex: "pixel" + - name: Galaxy + regex: "galaxy|samsung" +``` + +Each keyword's `regex` is a single case-insensitive pattern — use `|` for +alternatives (e.g. `"galaxy|samsung"`). Save it as `basket.yaml`, then run: + +```bash +python competitor_buzz_tracker.py --config basket.yaml [--output FILE] [--show-code] +``` + +This writes `competitor-buzz-_