From 5691acf79b35dabcba23d066f72ee24d23913f36 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 9 May 2026 09:05:27 +0000 Subject: [PATCH 1/2] Add Deep Agents Launch Intelligence cookbook (LangChain + Perplexity Search) Adds a multi-agent cookbook under docs/articles/ that uses LangChain Deep Agents for orchestration, the Perplexity Search API as the only external tool, and the langchain-perplexity chat integration for the model. The agent investigates a product launch and produces a citation-rich brief covering the official announcement, independent reception, the competitive landscape, and execution risks. Four narrowly-scoped sub-agents each write notes to a virtual workpaper file; the orchestrator synthesizes the final report from those workpapers so the brief is grounded in saved evidence rather than the model's parametric memory. Includes a runnable CLI (with a streaming progress mode for notebooks), a programmatic entry point, and an mdx article walking through the architecture, prompts, citation rules, observability, and how to adapt the four-sub-agent scaffold to other intelligence scenarios. --- README.md | 1 + .../README.mdx | 300 ++++++++++ .../launch_intelligence_agent.py | 519 ++++++++++++++++++ .../requirements.txt | 5 + 4 files changed, 825 insertions(+) create mode 100644 docs/articles/deep-agents-launch-intelligence/README.mdx create mode 100644 docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py create mode 100644 docs/articles/deep-agents-launch-intelligence/requirements.txt diff --git a/README.md b/README.md index decd6de..154c573 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Community-built applications including: In-depth tutorials for advanced implementations: - Memory management patterns - OpenAI agents integration +- [Deep Agents Launch Intelligence](docs/articles/deep-agents-launch-intelligence/) — multi-agent product-launch briefs with LangChain Deep Agents + Perplexity Search - Multi-modal implementations ## Quick Start diff --git a/docs/articles/deep-agents-launch-intelligence/README.mdx b/docs/articles/deep-agents-launch-intelligence/README.mdx new file mode 100644 index 0000000..7f7180f --- /dev/null +++ b/docs/articles/deep-agents-launch-intelligence/README.mdx @@ -0,0 +1,300 @@ +--- +title: Deep Agents Launch Intelligence +description: Build a LangChain Deep Agent that investigates a product launch end-to-end using Perplexity Search and the langchain-perplexity chat model. +sidebar_position: 3 +keywords: [langchain, deep-agents, deepagents, perplexity-search, search-api, agents, multi-agent, subagents, virtual-filesystem, workpapers] +--- + +# Deep Agents Launch Intelligence + +This guide walks through building a multi-agent **Product Launch Intelligence Agent** on top of [LangChain Deep Agents](https://github.com/langchain-ai/deepagents) and Perplexity. The agent takes a launch topic ("NVIDIA Rubin GPU announcement", "Apple Vision Pro 2", "OpenAI Sora 2 GA") and produces a structured, citation-rich brief covering the official announcement, independent reception, the competitive landscape, and execution risks. + +The cookbook is intentionally Perplexity-native: + +- The **Perplexity Search API** (via the `perplexityai` Python SDK) is the only outside-world tool. It returns ranked web results with stable URLs the model can cite verbatim. +- The chat model is **`langchain-perplexity`** running `sonar-pro`. The integration plugs straight into Deep Agents — no custom adapter required. +- The orchestrator and sub-agents share a **virtual filesystem of workpapers** so the final report is synthesized from saved evidence, not from the model's parametric memory. + +> This example was inspired by the broader Deep Agents pattern (planner + sub-agents + virtual filesystem) but the scenario, prompts, sub-agent breakdown, and code are written from scratch for a Perplexity stack. Pick a different scenario by swapping the four sub-agent prompts. + +## 🎯 What you'll build + +By the end of this article you will have: + +- ✅ A reusable `perplexity_search` tool wrapping the Search API (multi-query, citation-friendly output) +- ✅ Four narrowly-scoped sub-agents: announcement scout, reception analyst, competitor mapper, risk auditor +- ✅ An orchestrator that plans, delegates, reads the workpapers, and writes a final report +- ✅ A CLI and a streaming progress mode for notebooks + +## 🏗️ Architecture + +```mermaid +graph TD + User[User: "Investigate launch X"] --> Orch[Orchestrator Deep Agent] + Orch -->|plan + delegate| A1[announcement-scout] + Orch -->|plan + delegate| A2[reception-analyst] + Orch -->|plan + delegate| A3[competitor-mapper] + Orch -->|plan + delegate| A4[risk-auditor] + A1 --> Search[(Perplexity Search API)] + A2 --> Search + A3 --> Search + A4 --> Search + A1 -->|write_file| FS[(Virtual workpapers)] + A2 -->|write_file| FS + A3 -->|write_file| FS + A4 -->|write_file| FS + Orch -->|read_file + synthesize| Report[final_report.md] +``` + +The orchestrator never calls the search tool itself — it only plans, delegates, reads workpaper files back, and synthesizes. That separation is what keeps the final report grounded in the saved evidence. + +## 📋 Prerequisites + +- **Python 3.10+** (Deep Agents pulls in LangGraph, which targets 3.10 and up) +- A **Perplexity API key** with Search API access — [get one](https://docs.perplexity.ai/guides/getting-started) +- Familiarity with [LangChain](https://python.langchain.com/) is helpful but not required + +## 🚀 Installation + +```bash +cd docs/articles/deep-agents-launch-intelligence +pip install -r requirements.txt +``` + +The `requirements.txt` pins: + +``` +deepagents>=0.0.5 +langchain-core>=0.3.0 +langchain-perplexity>=0.1.0 +langchain-openai>=0.3.0 # fallback chat model only +perplexityai>=0.6.0 +``` + +## ⚙️ Environment setup + +```bash +export PERPLEXITY_API_KEY="your-perplexity-api-key" + +# Optional: pick a different chat model. Defaults to sonar-pro. +export PPLX_MODEL="sonar-pro" +``` + +## 🧩 Building block 1 — the Perplexity Search tool + +The Perplexity Search API accepts up to five queries per call and returns ranked, snippeted, dated results. Wrap it in a small LangChain `@tool` so the sub-agents can invoke it directly: + +```python +from langchain_core.tools import tool +from perplexity import Perplexity + +COOKBOOK_SLUG = "cookbook/deep-agents-launch-intelligence/0.1.0" + +client = Perplexity(default_headers={"X-Pplx-Integration": COOKBOOK_SLUG}) + +@tool("perplexity_search", return_direct=False) +def perplexity_search( + queries: list[str], + max_results: int = 8, + max_tokens_per_page: int = 512, +) -> str: + """Run 1-5 grounded web searches via the Perplexity Search API.""" + response = client.search.create( + query=queries[:5], + max_results=max_results, + max_tokens_per_page=max_tokens_per_page, + ) + return _format_search_results(response) +``` + +Key points: + +- The tool returns **Markdown bullets with explicit URLs** so the model has a stable surface to cite from. Asking the model for `[1]`-style citations keyed to those URLs is what enforces grounding. +- Multi-query support (`queries[:5]`) lets a sub-agent fan out related searches in one round-trip — much cheaper than running five sequential calls. +- The cookbook attaches an `X-Pplx-Integration` header to every Search call. Use a slug that identifies your integration so the Perplexity team can attribute traffic when you ship to production. + +The full helper that flattens, de-duplicates, and formats results lives in [`launch_intelligence_agent.py`](./launch_intelligence_agent.py). + +## 🧩 Building block 2 — sub-agent prompts + +Each sub-agent owns one slice of the brief. Narrow scopes are what make the orchestrator's plan reliable. + +```python +ANNOUNCEMENT_PROMPT = """You investigate the official announcement of a product launch. + +1. Use perplexity_search to find the canonical first-party announcement + (vendor blog, press release, keynote recap, spec page). +2. Extract: launch date, headline positioning, named features, regions/SKUs, + pricing, availability windows. +3. Save raw bullet notes to announcement.md via write_file. +4. Return a 6-10 line summary with inline citations [1], [2] mapped to the + URLs you actually used. +""" +``` + +The other three follow the same shape: research → write workpaper file → return a citation-rich summary. The full prompt set is in `launch_intelligence_agent.py`. Edit them in place to change the scenario — for example, swap the four sub-agents for `policy-tracker`, `litigation-watch`, `vendor-impact`, `mitigation-options` to repurpose this scaffold as an AI-policy monitoring agent. + +## 🧩 Building block 3 — the orchestrator + +The orchestrator prompt prescribes the plan and the final report shape, and explicitly forbids inventing URLs: + +```text +1. Delegate to announcement-scout +2. Delegate to reception-analyst +3. Delegate to competitor-mapper +4. Delegate to risk-auditor +5. Read workpapers back; write final_report.md. +6. Never cite a URL that did not appear in a Perplexity Search result. +``` + +Putting these rules in the orchestrator (rather than relying on the model to remember them across calls) is what makes the brief reproducible. + +## 🧩 Building block 4 — wiring it together + +Deep Agents accepts a chat model, a list of tools, an `instructions` string, and a `subagents` list. That's the whole graph: + +```python +from deepagents import create_deep_agent +from langchain_perplexity import ChatPerplexity + +chat_model = ChatPerplexity(model="sonar-pro", temperature=0.0) +search_tool = make_perplexity_search_tool() # the @tool from above + +agent = create_deep_agent( + model=chat_model, + tools=[search_tool], + instructions=ORCHESTRATOR_PROMPT, + subagents=SUBAGENTS, # the four prompts wrapped in dicts +) +``` + +`create_deep_agent` returns a compiled LangGraph runnable with `read_file`, `write_file`, `ls`, and a planning tool already in scope — so each sub-agent can open `announcement.md`, write to it, and the orchestrator can pull it back later. + +## 🏃 Running it + +### As a CLI + +```bash +python launch_intelligence_agent.py "NVIDIA Rubin GPU announcement" +``` + +The script prints `final_report.md` to stdout when the graph terminates. + +### Streaming progress (notebooks, long-running runs) + +```bash +python launch_intelligence_agent.py "Apple Vision Pro 2" --stream +``` + +`--stream` uses LangGraph's `stream_mode="updates"` and prints one line to stderr per node tick — useful for watching the four sub-agents fire in sequence: + +``` +[ 3.4s] announcement-scout +[ 18.1s] reception-analyst +[ 41.7s] competitor-mapper +[ 62.0s] risk-auditor +[ 74.2s] orchestrator +``` + +### Programmatically + +```python +from launch_intelligence_agent import investigate_launch, AgentConfig + +result = investigate_launch( + "OpenAI Sora 2 general availability", + AgentConfig(model="sonar-pro"), +) +print(result["files"]["final_report.md"]) +``` + +`result["files"]` exposes every workpaper the sub-agents wrote — handy for downstream pipelines that want to keep the raw evidence alongside the synthesized report. + +## 📤 Expected output shape + +```text +# OpenAI Sora 2 general availability — Launch Intelligence Brief + +## 1. Headline +OpenAI announced Sora 2 GA on 2026-04-22 ... + +## 2. Official announcement +- New 60-second clip ceiling, ... +- Tiered pricing: $20/mo Plus, $200/mo Pro [1] + +## 3. Independent reception +Reviewers consistently praised motion coherence vs. Sora 1 [3][4], +but flagged hallucinated text-in-image artefacts [5]. + +## 4. Competitive landscape +| Competitor | Equivalent offering | Price | Differentiator | +|------------|---------------------|-------|----------------| +| Runway | Gen-4 | $35/mo | Stronger editor [6] | +| Google | Veo 3 | bundled with Gemini Ultra | Native audio [7] | + +## 5. Risks and open questions +- C2PA disclosure rules tightening in the EU [8] +- ... + +## 6. Sources +1. https://openai.com/blog/sora-2-ga +2. ... +``` + +## 🔍 Grounding and citation best practices + +- **Cite only URLs returned by the search tool.** The orchestrator prompt has this rule, and the sub-agent prompts repeat it. Models will happily invent plausible-looking URLs otherwise. +- **Don't paraphrase numbers.** Each sub-agent attributes numbers to a date and a source. The orchestrator forwards that attribution into the final report. +- **Treat `write_file` as a commitment.** Once a sub-agent saves notes, the orchestrator reads them back. That round-trip is the difference between "the model said it" and "the model said it and we still have the receipt". + +## 🛰️ Observability + +Deep Agents is a LangGraph runnable, so it works with the standard LangChain tracing stack. To trace a run: + +```bash +export LANGSMITH_TRACING=true +export LANGSMITH_API_KEY="ls__..." +export LANGSMITH_PROJECT="launch-intelligence" +python launch_intelligence_agent.py "Anthropic Claude Opus 4.7 release" +``` + +Each sub-agent call shows up as its own span, with the search-tool inputs and outputs, the workpaper writes, and the orchestrator's final synthesis. That makes it cheap to debug "why did the brief skip pricing?" by inspecting the failing sub-agent in isolation. + +## 🛠️ Troubleshooting + +- **`RuntimeError: Set PERPLEXITY_API_KEY`** — the chat model could not find a key. Export `PERPLEXITY_API_KEY` or pass `--api-key`. +- **`ImportError: deepagents`** — install the package: `pip install deepagents`. If you are pinning to an older `langgraph`, upgrade it too: `pip install -U langgraph`. +- **`langchain_perplexity` not found** — the script falls back to `langchain-openai` pointed at `https://api.perplexity.ai`. To remove the warning, install the official integration: `pip install langchain-perplexity`. +- **Sub-agent never writes a workpaper** — usually the sub-agent prompt is too vague. The four prompts in this cookbook always end with "save raw notes to `.md`" — keep that line when you adapt them. +- **The brief cites a URL that doesn't open** — confirm the URL appears in the workpaper. If it does, the search result was stale; lower `max_tokens_per_page` and re-run, or filter results by `date`. + +## 🔁 Adapting the scaffold + +The same four-sub-agent skeleton handles other intelligence scenarios with only prompt edits: + +| Scenario | Sub-agents | +|----------|------------| +| Product launch (this cookbook) | announcement-scout, reception-analyst, competitor-mapper, risk-auditor | +| AI policy monitoring | policy-tracker, litigation-watch, vendor-impact, mitigation-options | +| Earnings preview | guidance-tracker, sell-side-consensus, peer-watch, risk-auditor | +| Open-source release | changelog-scout, community-reception, downstream-adopters, security-auditor | + +Swap the four `SUBAGENTS` entries and update the orchestrator's section list — the rest of the scaffold (search tool, virtual filesystem, streaming, CLI) stays the same. + +## ⚠️ Limitations + +- The Search API's `max_results` is a *total* across multi-query calls, so a four-sub-agent run with 8 results each easily fans out to 32+ pages. Keep `max_tokens_per_page` modest unless you need long snippets. +- Deep Agents' virtual filesystem is per-invocation — workpapers are not persisted between runs by default. To keep them, dump `result["files"]` to disk after the call. +- `langchain-perplexity` does not currently expose a `default_headers` hook on the chat model, so the `X-Pplx-Integration` slug only attaches to Search API traffic. The fallback `ChatOpenAI` path does support headers. + +## 📚 Resources + +- [Perplexity Search API](https://docs.perplexity.ai/docs/search-api) +- [Perplexity API quickstart](https://docs.perplexity.ai/guides/getting-started) +- [LangChain Perplexity chat integration](https://python.langchain.com/docs/integrations/chat/perplexity) +- [Deep Agents (langchain-ai/deepagents)](https://github.com/langchain-ai/deepagents) +- [LangGraph streaming reference](https://langchain-ai.github.io/langgraph/concepts/streaming/) + +--- + +**Source code:** [`launch_intelligence_agent.py`](./launch_intelligence_agent.py) diff --git a/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py b/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py new file mode 100644 index 0000000..43e4826 --- /dev/null +++ b/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python3 +""" +Product Launch Intelligence Agent +================================= + +A LangChain Deep Agent that produces a structured launch-intelligence brief on +any product, feature, or hardware release. Built on: + +* ``deepagents.create_deep_agent`` for orchestration, planning, sub-agents, + and a virtual filesystem (workpapers). +* The Perplexity Search API (via the official ``perplexityai`` Python SDK) + for grounded web search with citations. +* ``langchain-perplexity`` for the chat model that powers each agent. + +This is a Perplexity-native take on the Deep Agents pattern. The orchestrator +delegates focused queries to specialist sub-agents, each of which calls the +Perplexity Search API directly, drops findings into named workpaper files, and +returns a citation-rich summary back to the orchestrator. The final report is +synthesized from those workpapers — never invented from the model's parametric +memory. + +Run ``python launch_intelligence_agent.py ""`` to use it as a CLI, +or import :func:`investigate_launch` and call it from your own code/notebook. + +Docs: +- Perplexity Search API: https://docs.perplexity.ai/docs/search-api +- LangChain Perplexity: https://python.langchain.com/docs/integrations/chat/perplexity +- deepagents: https://github.com/langchain-ai/deepagents +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Optional, Tuple + +# --------------------------------------------------------------------------- +# Cookbook attribution. The Perplexity SDK forwards extra HTTP headers through +# ``default_headers``; ``X-Pplx-Integration`` lets the API team identify +# integration traffic. For the LangChain chat model we cannot easily inject +# request headers, so attribution is best-effort. +# --------------------------------------------------------------------------- +COOKBOOK_SLUG = "cookbook/deep-agents-launch-intelligence/0.1.0" +PPLX_INTEGRATION_HEADER = {"X-Pplx-Integration": COOKBOOK_SLUG} + +# Default Perplexity chat model surfaced through ``langchain-perplexity``. +# Override with --model or PPLX_MODEL. +DEFAULT_MODEL = "sonar-pro" + + +# --------------------------------------------------------------------------- +# Perplexity Search API tool (the only "external" tool the sub-agents use) +# --------------------------------------------------------------------------- +def _build_search_client(api_key: Optional[str]) -> Any: + """Construct an SDK client targeting the Perplexity Search API.""" + try: + from perplexity import Perplexity + except ImportError as exc: # pragma: no cover + raise RuntimeError( + "perplexityai SDK is required. Install with `pip install perplexityai`." + ) from exc + + return Perplexity( + api_key=api_key or os.environ.get("PERPLEXITY_API_KEY"), + default_headers=PPLX_INTEGRATION_HEADER, + ) + + +def _format_search_results(payload: Any) -> str: + """Render Perplexity Search API results as a citation-friendly Markdown list. + + The Search API returns one result list per submitted query. We flatten and + de-duplicate so the model sees a single set of (title, url, snippet) + triples. The agent should cite the exact URLs returned here rather than + inventing links. + """ + results: List[Dict[str, Any]] = [] + raw_results = ( + payload.get("results") if isinstance(payload, dict) else getattr(payload, "results", []) + ) or [] + for item in raw_results: + if not isinstance(item, dict): + item = item.model_dump() if hasattr(item, "model_dump") else dict(item) + results.append(item) + + seen: set[str] = set() + lines: List[str] = [] + for r in results: + url = r.get("url") or "" + if not url or url in seen: + continue + seen.add(url) + title = r.get("title") or url + snippet = r.get("snippet") or r.get("content") or "" + date = r.get("date") or "" + meta = f" ({date})" if date else "" + lines.append(f"- **{title}**{meta}\n {url}\n {snippet.strip()}") + if not lines: + return "No results returned by Perplexity Search." + return "\n".join(lines) + + +def make_perplexity_search_tool(api_key: Optional[str] = None) -> Any: + """Return a LangChain ``@tool`` wrapping ``client.search.create``. + + The Perplexity Search API accepts up to five queries in a single call, + which the agents exploit to fan out related searches cheaply. We expose + that as the tool's ``queries`` argument. + """ + from langchain_core.tools import tool + + client = _build_search_client(api_key) + + @tool("perplexity_search", return_direct=False) + def perplexity_search( + queries: List[str], + max_results: int = 8, + max_tokens_per_page: int = 512, + ) -> str: + """Run one or more grounded web searches via the Perplexity Search API. + + Args: + queries: 1-5 short, focused web queries. Phrase each like a search + bar query, not a chat message. + max_results: Total results to keep across all queries (1-20). + max_tokens_per_page: Per-page snippet budget. Lower is faster. + + Returns: + A Markdown bullet list of unique (title, url, snippet) results. + The agent MUST cite the URLs exactly as returned. + """ + if not queries: + return "No queries provided." + # Cap to API limits. + queries = [q.strip() for q in queries if q and q.strip()][:5] + max_results = max(1, min(int(max_results), 20)) + max_tokens_per_page = max(64, min(int(max_tokens_per_page), 1024)) + + response = client.search.create( + query=queries, + max_results=max_results, + max_tokens_per_page=max_tokens_per_page, + ) + return _format_search_results(response) + + return perplexity_search + + +# --------------------------------------------------------------------------- +# Sub-agent prompts. Each sub-agent owns one slice of the brief. Keeping the +# prompts narrow is what makes the orchestrator's plan reliable. +# --------------------------------------------------------------------------- +ANNOUNCEMENT_PROMPT = """You investigate the official announcement of a +product launch. + +Your job: +1. Use ``perplexity_search`` (run up to 5 queries) to find the canonical first- + party announcement: vendor blog post, press release, keynote recap, spec + page, or developer changelog. Prefer first-party sources over reblogs. +2. Extract: launch date, headline positioning, named features, supported + regions or SKUs, pricing and availability windows. +3. Save the raw bullet notes to ``announcement.md`` using ``write_file``. +4. Return a 6-10 line summary back to the orchestrator with inline citation + markers like [1], [2] mapped to the source URLs you used. Never cite a URL + that did not appear in the search tool output. +""" + +RECEPTION_PROMPT = """You investigate independent reception of a product launch: +press coverage, expert reviews, social commentary, and benchmark/quality +reports. + +Your job: +1. Use ``perplexity_search`` to find at least three independent outlets + (publications, hands-on reviews, analyst notes, developer forums). Avoid + reblogs of the vendor press release. +2. Capture concrete praise, concrete criticism, and any quantitative + benchmarks or measurable claims. +3. Save the raw bullet notes to ``reception.md``. +4. Return a 6-10 line synthesis to the orchestrator with inline citations + ([1], [2], ...) mapped to the URLs you actually used. +""" + +COMPETITOR_PROMPT = """You map the competitive landscape around a product +launch. + +Your job: +1. Identify 2-4 directly comparable products that ship today (do not invent + competitors). Use ``perplexity_search`` for each. +2. For each competitor capture: vendor, equivalent feature/SKU, pricing if + public, and one differentiator vs. the launched product. +3. Save the raw bullet notes to ``competitors.md``. +4. Return a Markdown table to the orchestrator with columns: Competitor, + Equivalent offering, Price, Differentiator. Include inline citations. +""" + +RISK_PROMPT = """You assess execution and adoption risks around a product +launch. + +Your job: +1. Use ``perplexity_search`` to find regulatory, security, supply, ecosystem, + or messaging risks raised by credible sources after the announcement. +2. Skip generic boilerplate risks. Each risk you keep must point to a specific + article, filing, or report. +3. Save the raw bullet notes to ``risks.md``. +4. Return 3-5 specific risks to the orchestrator, each with one sentence of + evidence and an inline citation. +""" + + +SUBAGENTS: List[Dict[str, Any]] = [ + { + "name": "announcement-scout", + "description": ( + "Pulls the canonical first-party announcement and headline facts " + "(launch date, features, pricing, availability)." + ), + "prompt": ANNOUNCEMENT_PROMPT, + }, + { + "name": "reception-analyst", + "description": ( + "Surveys independent reception: reviews, press coverage, expert " + "reactions, and any benchmarks." + ), + "prompt": RECEPTION_PROMPT, + }, + { + "name": "competitor-mapper", + "description": ( + "Maps 2-4 direct competitors with equivalent SKUs, pricing, and a " + "single differentiator each." + ), + "prompt": COMPETITOR_PROMPT, + }, + { + "name": "risk-auditor", + "description": ( + "Surfaces concrete regulatory, security, supply, ecosystem, or " + "messaging risks raised by credible sources." + ), + "prompt": RISK_PROMPT, + }, +] + + +# --------------------------------------------------------------------------- +# Orchestrator prompt +# --------------------------------------------------------------------------- +ORCHESTRATOR_PROMPT = """You are the lead analyst on a product-launch +intelligence team. You coordinate four specialist sub-agents and produce one +final brief. + +Operating procedure: + +1. Read the user's launch topic. If it is ambiguous, pick the most likely + recent launch and proceed — do not stall asking for clarification. +2. Plan the work using the planning tool. The standard plan is: + a. Delegate to ``announcement-scout`` to capture official launch facts. + b. Delegate to ``reception-analyst`` for independent coverage. + c. Delegate to ``competitor-mapper`` for the comparable landscape. + d. Delegate to ``risk-auditor`` for execution and adoption risks. +3. Each sub-agent writes its raw notes to a workpaper file + (``announcement.md``, ``reception.md``, ``competitors.md``, ``risks.md``) + and returns a short citation-rich summary to you. +4. Read the workpapers back with ``read_file`` if you need detail beyond the + summaries. +5. Synthesize the final report into ``final_report.md`` using ``write_file``. + Use this exact section order: + + # — Launch Intelligence Brief + + ## 1. Headline + One paragraph: what shipped, when, by whom, at what price. + + ## 2. Official announcement + Key features and availability, distilled from announcement.md. + + ## 3. Independent reception + What reviewers and analysts actually said. Quote sparingly, attribute + always. + + ## 4. Competitive landscape + Markdown table from competitors.md. + + ## 5. Risks and open questions + 3-5 specific risks from risks.md. + + ## 6. Sources + Numbered list of unique URLs cited above. Use only URLs that appeared in + the sub-agents' summaries or workpapers — never invent a URL. + +6. After writing ``final_report.md`` return its full contents as your final + answer. + +Hard rules: + +* Never cite a URL that did not appear in a Perplexity Search result. If the + sub-agents did not produce a fact, omit it rather than guessing. +* Numbers must be attributed (date, source). If a number is not in the + workpapers, write "not disclosed". +* Keep the final report under ~700 words. +""" + + +# --------------------------------------------------------------------------- +# Agent construction +# --------------------------------------------------------------------------- +@dataclass +class AgentConfig: + """Knobs the CLI and ``investigate_launch`` share.""" + + model: str = DEFAULT_MODEL + api_key: Optional[str] = None + temperature: float = 0.0 + + +def _build_chat_model(cfg: AgentConfig) -> Any: + """Return a LangChain chat model backed by Perplexity. + + Uses the official ``langchain-perplexity`` integration when available, and + falls back to ``ChatOpenAI`` pointed at ``api.perplexity.ai`` otherwise so + the example still runs in environments that have not pinned the new + package yet. + """ + api_key = cfg.api_key or os.environ.get("PERPLEXITY_API_KEY") + if not api_key: + raise RuntimeError( + "Set PERPLEXITY_API_KEY (or pass --api-key) before running." + ) + + try: + from langchain_perplexity import ChatPerplexity + + return ChatPerplexity( + model=cfg.model, + temperature=cfg.temperature, + pplx_api_key=api_key, + ) + except ImportError: + pass + + # Fallback: OpenAI-compatible endpoint. Header injection works here. + from langchain_openai import ChatOpenAI + + return ChatOpenAI( + model=cfg.model, + temperature=cfg.temperature, + api_key=api_key, + base_url="https://api.perplexity.ai", + default_headers=PPLX_INTEGRATION_HEADER, + ) + + +def build_agent(cfg: Optional[AgentConfig] = None) -> Any: + """Construct the deep agent: orchestrator + 4 sub-agents + search tool.""" + cfg = cfg or AgentConfig() + + try: + from deepagents import create_deep_agent + except ImportError as exc: # pragma: no cover + raise RuntimeError( + "deepagents is required. Install with `pip install deepagents`." + ) from exc + + chat_model = _build_chat_model(cfg) + search_tool = make_perplexity_search_tool(cfg.api_key) + + return create_deep_agent( + model=chat_model, + tools=[search_tool], + instructions=ORCHESTRATOR_PROMPT, + subagents=SUBAGENTS, + ) + + +# --------------------------------------------------------------------------- +# Programmatic entry point +# --------------------------------------------------------------------------- +def investigate_launch( + topic: str, + cfg: Optional[AgentConfig] = None, + recursion_limit: int = 60, +) -> Dict[str, Any]: + """Run the agent end-to-end and return the final state. + + The returned dict contains ``messages`` (the full LangGraph trace) and + ``files`` (the virtual workpapers written by the sub-agents). Callers + typically want ``files["final_report.md"]``. + """ + agent = build_agent(cfg) + result = agent.invoke( + {"messages": [{"role": "user", "content": topic}]}, + {"recursion_limit": recursion_limit}, + ) + return result + + +# --------------------------------------------------------------------------- +# Streaming progress (handy for notebooks / long-running runs) +# --------------------------------------------------------------------------- +def stream_launch( + topic: str, + cfg: Optional[AgentConfig] = None, + recursion_limit: int = 60, +) -> Iterable[Tuple[str, Any]]: + """Yield ``(node_name, state_update)`` tuples as the graph runs. + + Useful for live progress UIs — print one line per yielded tuple. + """ + agent = build_agent(cfg) + for chunk in agent.stream( + {"messages": [{"role": "user", "content": topic}]}, + {"recursion_limit": recursion_limit}, + stream_mode="updates", + ): + for node, update in chunk.items(): + yield node, update + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- +def _print_final_report(result: Dict[str, Any]) -> None: + files = result.get("files", {}) or {} + if "final_report.md" in files: + print(files["final_report.md"]) + return + # No workpaper written — fall back to the last assistant message. + messages = result.get("messages", []) or [] + if messages: + last = messages[-1] + text = getattr(last, "content", None) or ( + last.get("content") if isinstance(last, dict) else "" + ) + if text: + print(text) + return + print("(no final_report.md produced)") + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description=( + "Generate a launch-intelligence brief with a Perplexity-powered " + "LangChain Deep Agent." + ) + ) + parser.add_argument( + "topic", + help="Launch to investigate, e.g. 'NVIDIA Rubin GPU announcement'.", + ) + parser.add_argument( + "--model", + default=os.environ.get("PPLX_MODEL", DEFAULT_MODEL), + help=f"Perplexity chat model (default: {DEFAULT_MODEL}).", + ) + parser.add_argument( + "--api-key", + default=None, + help="Perplexity API key (defaults to PERPLEXITY_API_KEY env var).", + ) + parser.add_argument( + "--stream", + action="store_true", + help="Stream sub-agent progress to stderr while the graph runs.", + ) + parser.add_argument( + "--json", + action="store_true", + help="Emit the full final state as JSON instead of the report.", + ) + parser.add_argument( + "--recursion-limit", + type=int, + default=60, + help="LangGraph recursion limit (default: 60).", + ) + args = parser.parse_args(argv) + + cfg = AgentConfig(model=args.model, api_key=args.api_key) + started = time.time() + + if args.stream: + # Stream mode: print a one-line progress marker per node update, + # then print the final report from the last accumulated state. + last_state: Dict[str, Any] = {} + try: + for node, update in stream_launch(args.topic, cfg, args.recursion_limit): + print(f"[{time.time() - started:6.1f}s] {node}", file=sys.stderr) + if isinstance(update, dict): + last_state.update(update) + except Exception as err: # noqa: BLE001 + print(f"Agent error: {err}", file=sys.stderr) + return 2 + if args.json: + print(json.dumps(last_state, indent=2, default=str)) + else: + _print_final_report(last_state) + return 0 + + try: + result = investigate_launch(args.topic, cfg, args.recursion_limit) + except Exception as err: # noqa: BLE001 + print(f"Agent error: {err}", file=sys.stderr) + return 2 + + if args.json: + print(json.dumps(result, indent=2, default=str)) + else: + _print_final_report(result) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/articles/deep-agents-launch-intelligence/requirements.txt b/docs/articles/deep-agents-launch-intelligence/requirements.txt new file mode 100644 index 0000000..b96595d --- /dev/null +++ b/docs/articles/deep-agents-launch-intelligence/requirements.txt @@ -0,0 +1,5 @@ +deepagents>=0.0.5 +langchain-core>=0.3.0 +langchain-perplexity>=0.1.0 +langchain-openai>=0.3.0 +perplexityai>=0.6.0 From 1992378373b79d7dd1c2a62eeb32cdc2ff627e25 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 9 May 2026 11:59:27 +0000 Subject: [PATCH 2/2] Rebuild Deep Agents Launch Intelligence on the Perplexity Agent API Switches the cookbook to use only the Perplexity Agent API. Removes the sonar-pro chat-completions framing, the langchain-perplexity / langchain-openai dependencies, and the standalone Search API tool. Keeps the Deep Agents pattern (planner + narrowly-scoped sub-agents + virtual workpaper filesystem) and implements every stage as a client.responses.create call with the pro-search preset and the built-in web_search + fetch_url tools. - Sub-agents are Agent API calls with web_search + fetch_url enabled and write Markdown workpapers (announcement.md, reception.md, competitors.md, risks.md). - Orchestrator is a final Agent API call with no tools that synthesizes the brief from the four workpapers. - requirements.txt reduces to just perplexityai>=0.6.0. - CLI swaps --model sonar-pro for --preset pro-search / --orchestrator-preset. - X-Pplx-Integration cookbook attribution header preserved on every Agent API call via the SDK's default_headers hook. --- README.md | 2 +- .../README.mdx | 253 ++++--- .../launch_intelligence_agent.py | 693 ++++++++++-------- .../requirements.txt | 4 - 4 files changed, 506 insertions(+), 446 deletions(-) diff --git a/README.md b/README.md index 154c573..b5d45f3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Community-built applications including: In-depth tutorials for advanced implementations: - Memory management patterns - OpenAI agents integration -- [Deep Agents Launch Intelligence](docs/articles/deep-agents-launch-intelligence/) — multi-agent product-launch briefs with LangChain Deep Agents + Perplexity Search +- [Deep Agents Launch Intelligence](docs/articles/deep-agents-launch-intelligence/) — multi-agent product-launch briefs built on the Perplexity Agent API (Deep Agents pattern: planner + sub-agents + workpapers) - Multi-modal implementations ## Quick Start diff --git a/docs/articles/deep-agents-launch-intelligence/README.mdx b/docs/articles/deep-agents-launch-intelligence/README.mdx index 7f7180f..cc500c2 100644 --- a/docs/articles/deep-agents-launch-intelligence/README.mdx +++ b/docs/articles/deep-agents-launch-intelligence/README.mdx @@ -1,58 +1,62 @@ --- title: Deep Agents Launch Intelligence -description: Build a LangChain Deep Agent that investigates a product launch end-to-end using Perplexity Search and the langchain-perplexity chat model. +description: A multi-agent product-launch brief built on the Perplexity Agent API using the Deep Agents pattern (planner + sub-agents + workpapers). sidebar_position: 3 -keywords: [langchain, deep-agents, deepagents, perplexity-search, search-api, agents, multi-agent, subagents, virtual-filesystem, workpapers] +keywords: [agent-api, deep-agents, multi-agent, subagents, web-search, fetch-url, workpapers, pro-search] --- # Deep Agents Launch Intelligence -This guide walks through building a multi-agent **Product Launch Intelligence Agent** on top of [LangChain Deep Agents](https://github.com/langchain-ai/deepagents) and Perplexity. The agent takes a launch topic ("NVIDIA Rubin GPU announcement", "Apple Vision Pro 2", "OpenAI Sora 2 GA") and produces a structured, citation-rich brief covering the official announcement, independent reception, the competitive landscape, and execution risks. +This guide walks through building a multi-agent **Product Launch Intelligence Agent** entirely on top of the Perplexity [Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart). The agent takes a launch topic ("NVIDIA Rubin GPU announcement", "Apple Vision Pro 2", "OpenAI Sora 2 GA") and produces a structured, citation-rich brief covering the official announcement, independent reception, the competitive landscape, and execution risks. -The cookbook is intentionally Perplexity-native: +The cookbook reproduces the Deep Agents pattern (planner + narrowly-scoped sub-agents + a shared virtual filesystem of workpapers), but every model call is made through the Perplexity Agent API. There is no Sonar chat-completions code path. -- The **Perplexity Search API** (via the `perplexityai` Python SDK) is the only outside-world tool. It returns ranked web results with stable URLs the model can cite verbatim. -- The chat model is **`langchain-perplexity`** running `sonar-pro`. The integration plugs straight into Deep Agents — no custom adapter required. -- The orchestrator and sub-agents share a **virtual filesystem of workpapers** so the final report is synthesized from saved evidence, not from the model's parametric memory. +- Each **sub-agent is a single Agent API call** with the `pro-search` preset and the built-in `web_search` + `fetch_url` tools enabled. The Agent API plans its own tool steps, returns inline citations, and writes its findings as a workpaper. +- The **orchestrator is a final Agent API call** that synthesizes the four workpapers into the published brief. It runs without web tools, so the brief is grounded strictly in evidence the sub-agents already saved. +- **Workpapers** (`announcement.md`, `reception.md`, `competitors.md`, `risks.md`, `final_report.md`) are persisted in a Python dict the script passes between stages — the same semantics Deep Agents' virtual filesystem uses. -> This example was inspired by the broader Deep Agents pattern (planner + sub-agents + virtual filesystem) but the scenario, prompts, sub-agent breakdown, and code are written from scratch for a Perplexity stack. Pick a different scenario by swapping the four sub-agent prompts. +> This example was inspired by the broader Deep Agents pattern (planner + sub-agents + virtual filesystem) but the scenario, prompts, sub-agent breakdown, and code are written from scratch for the Perplexity Agent API. Pick a different scenario by swapping the four sub-agent prompts. ## 🎯 What you'll build By the end of this article you will have: -- ✅ A reusable `perplexity_search` tool wrapping the Search API (multi-query, citation-friendly output) -- ✅ Four narrowly-scoped sub-agents: announcement scout, reception analyst, competitor mapper, risk auditor -- ✅ An orchestrator that plans, delegates, reads the workpapers, and writes a final report +- ✅ Four narrowly-scoped Agent API sub-agents: announcement scout, reception analyst, competitor mapper, risk auditor — each backed by `pro-search` with `web_search` + `fetch_url` +- ✅ An orchestrator Agent API call that reads the four workpapers and writes the final report +- ✅ A virtual workpaper filesystem that mirrors the Deep Agents pattern - ✅ A CLI and a streaming progress mode for notebooks ## 🏗️ Architecture ```mermaid graph TD - User[User: "Investigate launch X"] --> Orch[Orchestrator Deep Agent] - Orch -->|plan + delegate| A1[announcement-scout] - Orch -->|plan + delegate| A2[reception-analyst] - Orch -->|plan + delegate| A3[competitor-mapper] - Orch -->|plan + delegate| A4[risk-auditor] - A1 --> Search[(Perplexity Search API)] - A2 --> Search - A3 --> Search - A4 --> Search - A1 -->|write_file| FS[(Virtual workpapers)] - A2 -->|write_file| FS - A3 -->|write_file| FS - A4 -->|write_file| FS - Orch -->|read_file + synthesize| Report[final_report.md] + User[User: "Investigate launch X"] --> Driver[Python driver] + Driver -->|Agent API call 1| A1[announcement-scout] + Driver -->|Agent API call 2| A2[reception-analyst] + Driver -->|Agent API call 3| A3[competitor-mapper] + Driver -->|Agent API call 4| A4[risk-auditor] + A1 --> Tools[(web_search + fetch_url)] + A2 --> Tools + A3 --> Tools + A4 --> Tools + A1 -->|workpaper| FS[(announcement.md)] + A2 -->|workpaper| FS2[(reception.md)] + A3 -->|workpaper| FS3[(competitors.md)] + A4 -->|workpaper| FS4[(risks.md)] + Driver -->|Agent API call 5| Orch[orchestrator] + FS --> Orch + FS2 --> Orch + FS3 --> Orch + FS4 --> Orch + Orch --> Report[final_report.md] ``` -The orchestrator never calls the search tool itself — it only plans, delegates, reads workpaper files back, and synthesizes. That separation is what keeps the final report grounded in the saved evidence. +The orchestrator never calls `web_search` itself — it only synthesizes from the workpapers the sub-agents already wrote. That separation is what keeps the final report grounded in saved evidence. ## 📋 Prerequisites -- **Python 3.10+** (Deep Agents pulls in LangGraph, which targets 3.10 and up) -- A **Perplexity API key** with Search API access — [get one](https://docs.perplexity.ai/guides/getting-started) -- Familiarity with [LangChain](https://python.langchain.com/) is helpful but not required +- **Python 3.9+** +- A **Perplexity API key** with [Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart) access — [get one](https://docs.perplexity.ai/guides/getting-started) ## 🚀 Installation @@ -61,13 +65,9 @@ cd docs/articles/deep-agents-launch-intelligence pip install -r requirements.txt ``` -The `requirements.txt` pins: +The `requirements.txt` pins only the official Perplexity SDK: ``` -deepagents>=0.0.5 -langchain-core>=0.3.0 -langchain-perplexity>=0.1.0 -langchain-openai>=0.3.0 # fallback chat model only perplexityai>=0.6.0 ``` @@ -76,99 +76,109 @@ perplexityai>=0.6.0 ```bash export PERPLEXITY_API_KEY="your-perplexity-api-key" -# Optional: pick a different chat model. Defaults to sonar-pro. -export PPLX_MODEL="sonar-pro" +# Optional: pick different Agent API presets. Defaults to pro-search. +export PPLX_PRESET="pro-search" +export PPLX_ORCHESTRATOR_PRESET="pro-search" ``` -## 🧩 Building block 1 — the Perplexity Search tool +## 🧩 Building block 1 — sub-agent prompts -The Perplexity Search API accepts up to five queries per call and returns ranked, snippeted, dated results. Wrap it in a small LangChain `@tool` so the sub-agents can invoke it directly: +Each sub-agent owns one slice of the brief. Narrow scopes are what make the multi-agent plan reliable. + +```python +ANNOUNCEMENT_PROMPT = """You investigate the official announcement of a product launch. + +1. Use web_search and fetch_url to find the canonical first-party announcement + (vendor blog, press release, keynote recap, spec page). +2. Extract: launch date, headline positioning, named features, regions/SKUs, + pricing, availability windows. +3. Return a Markdown document with two sections: + - ## Bullet notes — raw extracted facts as bullets, each with a URL. + - ## Summary — a 6-10 line synthesis with inline citations [1], [2] + mapped to the URLs you actually used. +""" +``` + +The other three follow the same shape: research → write workpaper Markdown → return a citation-rich summary. The full prompt set is in `launch_intelligence_agent.py`. Edit them in place to change the scenario — for example, swap the four sub-agents for `policy-tracker`, `litigation-watch`, `vendor-impact`, `mitigation-options` to repurpose this scaffold as an AI-policy monitoring agent. + +## 🧩 Building block 2 — running a sub-agent through the Agent API + +Each sub-agent is a single `client.responses.create` call: ```python -from langchain_core.tools import tool from perplexity import Perplexity COOKBOOK_SLUG = "cookbook/deep-agents-launch-intelligence/0.1.0" client = Perplexity(default_headers={"X-Pplx-Integration": COOKBOOK_SLUG}) -@tool("perplexity_search", return_direct=False) -def perplexity_search( - queries: list[str], - max_results: int = 8, - max_tokens_per_page: int = 512, -) -> str: - """Run 1-5 grounded web searches via the Perplexity Search API.""" - response = client.search.create( - query=queries[:5], - max_results=max_results, - max_tokens_per_page=max_tokens_per_page, - ) - return _format_search_results(response) +response = client.responses.create( + preset="pro-search", + instructions=ANNOUNCEMENT_PROMPT, + input=f"Launch topic: {topic}\n\nProduce the workpaper described in your instructions.", + tools=[ + {"type": "web_search"}, + {"type": "fetch_url"}, + ], + max_steps=8, + max_output_tokens=2048, +) ``` Key points: -- The tool returns **Markdown bullets with explicit URLs** so the model has a stable surface to cite from. Asking the model for `[1]`-style citations keyed to those URLs is what enforces grounding. -- Multi-query support (`queries[:5]`) lets a sub-agent fan out related searches in one round-trip — much cheaper than running five sequential calls. -- The cookbook attaches an `X-Pplx-Integration` header to every Search call. Use a slug that identifies your integration so the Perplexity team can attribute traffic when you ship to production. - -The full helper that flattens, de-duplicates, and formats results lives in [`launch_intelligence_agent.py`](./launch_intelligence_agent.py). +- The Agent API plans its own tool steps and returns inline citations from the URLs it actually retrieved. Asking the model for `[1]`-style citations keyed to those URLs is what enforces grounding. +- `pro-search` is the recommended preset for grounded multi-tool research over the open web. Use a different preset (e.g. a domain-specific one) by exporting `PPLX_PRESET` or passing `--preset` on the CLI. +- The cookbook attaches an `X-Pplx-Integration` header to every Agent API call. Use a slug that identifies your integration so the Perplexity team can attribute traffic when you ship to production. -## 🧩 Building block 2 — sub-agent prompts +## 🧩 Building block 3 — the orchestrator -Each sub-agent owns one slice of the brief. Narrow scopes are what make the orchestrator's plan reliable. +The orchestrator is one final Agent API call with **no tools** — it composes the brief strictly from the four workpapers: ```python -ANNOUNCEMENT_PROMPT = """You investigate the official announcement of a product launch. - -1. Use perplexity_search to find the canonical first-party announcement - (vendor blog, press release, keynote recap, spec page). -2. Extract: launch date, headline positioning, named features, regions/SKUs, - pricing, availability windows. -3. Save raw bullet notes to announcement.md via write_file. -4. Return a 6-10 line summary with inline citations [1], [2] mapped to the - URLs you actually used. +ORCHESTRATOR_PROMPT = """You are the lead analyst on a product-launch intelligence team. + +You are given four workpapers prepared by specialist sub-agents: +- announcement.md +- reception.md +- competitors.md +- risks.md + +Synthesize them into a single brief using exactly this section order: +1. Headline 2. Official announcement 3. Independent reception +4. Competitive landscape 5. Risks and open questions 6. Sources + +Hard rules: +* Never cite a URL that did not appear in the workpapers. +* Numbers must be attributed (date, source). If not in the workpapers, write "not disclosed". +* Keep the final report under ~700 words. """ -``` - -The other three follow the same shape: research → write workpaper file → return a citation-rich summary. The full prompt set is in `launch_intelligence_agent.py`. Edit them in place to change the scenario — for example, swap the four sub-agents for `policy-tracker`, `litigation-watch`, `vendor-impact`, `mitigation-options` to repurpose this scaffold as an AI-policy monitoring agent. - -## 🧩 Building block 3 — the orchestrator -The orchestrator prompt prescribes the plan and the final report shape, and explicitly forbids inventing URLs: - -```text -1. Delegate to announcement-scout -2. Delegate to reception-analyst -3. Delegate to competitor-mapper -4. Delegate to risk-auditor -5. Read workpapers back; write final_report.md. -6. Never cite a URL that did not appear in a Perplexity Search result. +response = client.responses.create( + preset="pro-search", + instructions=ORCHESTRATOR_PROMPT, + input=f"Launch topic: {topic}\n\nWorkpapers:\n\n{workpaper_blob}\n\nWrite final_report.md.", + max_output_tokens=2048, +) ``` Putting these rules in the orchestrator (rather than relying on the model to remember them across calls) is what makes the brief reproducible. -## 🧩 Building block 4 — wiring it together +## 🧩 Building block 4 — the workpaper filesystem -Deep Agents accepts a chat model, a list of tools, an `instructions` string, and a `subagents` list. That's the whole graph: +Workpapers live in a Python dict that the driver passes between stages, mirroring the Deep Agents virtual filesystem semantics: ```python -from deepagents import create_deep_agent -from langchain_perplexity import ChatPerplexity +files: Dict[str, str] = {} -chat_model = ChatPerplexity(model="sonar-pro", temperature=0.0) -search_tool = make_perplexity_search_tool() # the @tool from above +for spec in SUBAGENTS: + text, _urls = run_subagent(client, spec, topic, cfg) + files[spec.workpaper] = text # announcement.md, reception.md, ... -agent = create_deep_agent( - model=chat_model, - tools=[search_tool], - instructions=ORCHESTRATOR_PROMPT, - subagents=SUBAGENTS, # the four prompts wrapped in dicts -) +files["final_report.md"] = run_orchestrator(client, topic, files, cfg) ``` -`create_deep_agent` returns a compiled LangGraph runnable with `read_file`, `write_file`, `ls`, and a planning tool already in scope — so each sub-agent can open `announcement.md`, write to it, and the orchestrator can pull it back later. +Every workpaper a sub-agent produced is available downstream — handy for pipelines that want to keep the raw evidence alongside the synthesized report. ## 🏃 Running it @@ -178,7 +188,7 @@ agent = create_deep_agent( python launch_intelligence_agent.py "NVIDIA Rubin GPU announcement" ``` -The script prints `final_report.md` to stdout when the graph terminates. +The script prints a one-line progress marker per stage to stderr, then `final_report.md` to stdout when the pipeline terminates. ### Streaming progress (notebooks, long-running runs) @@ -186,7 +196,7 @@ The script prints `final_report.md` to stdout when the graph terminates. python launch_intelligence_agent.py "Apple Vision Pro 2" --stream ``` -`--stream` uses LangGraph's `stream_mode="updates"` and prints one line to stderr per node tick — useful for watching the four sub-agents fire in sequence: +`--stream` prints one line to stderr per stage as it finishes: ``` [ 3.4s] announcement-scout @@ -203,12 +213,13 @@ from launch_intelligence_agent import investigate_launch, AgentConfig result = investigate_launch( "OpenAI Sora 2 general availability", - AgentConfig(model="sonar-pro"), + AgentConfig(subagent_preset="pro-search"), ) -print(result["files"]["final_report.md"]) +print(result.final_report) +print(result.files["announcement.md"]) # raw workpaper from sub-agent 1 ``` -`result["files"]` exposes every workpaper the sub-agents wrote — handy for downstream pipelines that want to keep the raw evidence alongside the synthesized report. +`result.files` exposes every workpaper the sub-agents wrote — handy for downstream pipelines that want to keep the raw evidence alongside the synthesized report. `result.sources` maps each sub-agent name to the URLs the Agent API retrieved during that call. ## 📤 Expected output shape @@ -243,30 +254,28 @@ but flagged hallucinated text-in-image artefacts [5]. ## 🔍 Grounding and citation best practices -- **Cite only URLs returned by the search tool.** The orchestrator prompt has this rule, and the sub-agent prompts repeat it. Models will happily invent plausible-looking URLs otherwise. +- **Cite only URLs the Agent API retrieved.** The orchestrator prompt has this rule, and the sub-agent prompts repeat it. Models will happily invent plausible-looking URLs otherwise. - **Don't paraphrase numbers.** Each sub-agent attributes numbers to a date and a source. The orchestrator forwards that attribution into the final report. -- **Treat `write_file` as a commitment.** Once a sub-agent saves notes, the orchestrator reads them back. That round-trip is the difference between "the model said it" and "the model said it and we still have the receipt". +- **Treat workpapers as a commitment.** Once a sub-agent saves notes, the orchestrator reads them back. That round-trip is the difference between "the model said it" and "the model said it and we still have the receipt". ## 🛰️ Observability -Deep Agents is a LangGraph runnable, so it works with the standard LangChain tracing stack. To trace a run: +Every Agent API call returns its full tool-step trace (`web_search_results`, `fetch_url_results`) on the response object. The driver collects URLs into `result.sources[]` so you can audit which sources each sub-agent actually used: -```bash -export LANGSMITH_TRACING=true -export LANGSMITH_API_KEY="ls__..." -export LANGSMITH_PROJECT="launch-intelligence" -python launch_intelligence_agent.py "Anthropic Claude Opus 4.7 release" +```python +for name, urls in result.sources.items(): + print(name, len(urls), "urls") ``` -Each sub-agent call shows up as its own span, with the search-tool inputs and outputs, the workpaper writes, and the orchestrator's final synthesis. That makes it cheap to debug "why did the brief skip pricing?" by inspecting the failing sub-agent in isolation. +Cost and step counts are available on `response.usage` per call (see the [Agent API quickstart](https://docs.perplexity.ai/docs/agent-api/quickstart)). ## 🛠️ Troubleshooting -- **`RuntimeError: Set PERPLEXITY_API_KEY`** — the chat model could not find a key. Export `PERPLEXITY_API_KEY` or pass `--api-key`. -- **`ImportError: deepagents`** — install the package: `pip install deepagents`. If you are pinning to an older `langgraph`, upgrade it too: `pip install -U langgraph`. -- **`langchain_perplexity` not found** — the script falls back to `langchain-openai` pointed at `https://api.perplexity.ai`. To remove the warning, install the official integration: `pip install langchain-perplexity`. -- **Sub-agent never writes a workpaper** — usually the sub-agent prompt is too vague. The four prompts in this cookbook always end with "save raw notes to `.md`" — keep that line when you adapt them. -- **The brief cites a URL that doesn't open** — confirm the URL appears in the workpaper. If it does, the search result was stale; lower `max_tokens_per_page` and re-run, or filter results by `date`. +- **`RuntimeError: Set PERPLEXITY_API_KEY`** — the SDK could not find a key. Export `PERPLEXITY_API_KEY` or pass `--api-key`. +- **`ImportError: perplexity`** — install the SDK: `pip install perplexityai`. +- **Sub-agent returns no workpaper** — usually the prompt is too vague. The four prompts in this cookbook always end with explicit `## Bullet notes` and `## Summary` sections — keep that structure when you adapt them. +- **The brief cites a URL that doesn't open** — confirm the URL appears in the workpaper. If it does, the search result was stale; lower `--max-steps` and re-run, or rephrase the sub-agent prompt to request more recent sources. +- **Hit `max_steps` before the sub-agent finished** — bump `--max-steps` (default 8). Most sub-agents converge in 3-6 steps. ## 🔁 Adapting the scaffold @@ -279,21 +288,21 @@ The same four-sub-agent skeleton handles other intelligence scenarios with only | Earnings preview | guidance-tracker, sell-side-consensus, peer-watch, risk-auditor | | Open-source release | changelog-scout, community-reception, downstream-adopters, security-auditor | -Swap the four `SUBAGENTS` entries and update the orchestrator's section list — the rest of the scaffold (search tool, virtual filesystem, streaming, CLI) stays the same. +Swap the four `SUBAGENTS` entries and update the orchestrator's section list — the rest of the scaffold (Agent API client, workpaper filesystem, streaming, CLI) stays the same. ## ⚠️ Limitations -- The Search API's `max_results` is a *total* across multi-query calls, so a four-sub-agent run with 8 results each easily fans out to 32+ pages. Keep `max_tokens_per_page` modest unless you need long snippets. -- Deep Agents' virtual filesystem is per-invocation — workpapers are not persisted between runs by default. To keep them, dump `result["files"]` to disk after the call. -- `langchain-perplexity` does not currently expose a `default_headers` hook on the chat model, so the `X-Pplx-Integration` slug only attaches to Search API traffic. The fallback `ChatOpenAI` path does support headers. +- The Agent API is invoked sequentially per sub-agent in this cookbook. Drop in `concurrent.futures.ThreadPoolExecutor` to fan the four sub-agents out in parallel if latency matters more than rate-limit headroom. +- Workpapers are per-invocation — they are not persisted between runs. Dump `result.files` to disk after the call to keep them. +- The cookbook treats `pro-search` as the default preset for both sub-agents and orchestrator. If your account has access to other presets, swap them in via `PPLX_PRESET` / `PPLX_ORCHESTRATOR_PRESET` or the CLI flags. ## 📚 Resources -- [Perplexity Search API](https://docs.perplexity.ai/docs/search-api) +- [Perplexity Agent API quickstart](https://docs.perplexity.ai/docs/agent-api/quickstart) +- [Agent API web_search tool](https://docs.perplexity.ai/docs/agent-api/web-search) +- [Agent API fetch_url tool](https://docs.perplexity.ai/docs/agent-api/fetch-url) - [Perplexity API quickstart](https://docs.perplexity.ai/guides/getting-started) -- [LangChain Perplexity chat integration](https://python.langchain.com/docs/integrations/chat/perplexity) -- [Deep Agents (langchain-ai/deepagents)](https://github.com/langchain-ai/deepagents) -- [LangGraph streaming reference](https://langchain-ai.github.io/langgraph/concepts/streaming/) +- [Deep Agents pattern (langchain-ai/deepagents)](https://github.com/langchain-ai/deepagents) — the orchestration pattern this cookbook reproduces on top of the Agent API. --- diff --git a/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py b/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py index 43e4826..7a0858f 100644 --- a/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py +++ b/docs/articles/deep-agents-launch-intelligence/launch_intelligence_agent.py @@ -3,29 +3,37 @@ Product Launch Intelligence Agent ================================= -A LangChain Deep Agent that produces a structured launch-intelligence brief on -any product, feature, or hardware release. Built on: - -* ``deepagents.create_deep_agent`` for orchestration, planning, sub-agents, - and a virtual filesystem (workpapers). -* The Perplexity Search API (via the official ``perplexityai`` Python SDK) - for grounded web search with citations. -* ``langchain-perplexity`` for the chat model that powers each agent. - -This is a Perplexity-native take on the Deep Agents pattern. The orchestrator -delegates focused queries to specialist sub-agents, each of which calls the -Perplexity Search API directly, drops findings into named workpaper files, and -returns a citation-rich summary back to the orchestrator. The final report is -synthesized from those workpapers — never invented from the model's parametric -memory. - -Run ``python launch_intelligence_agent.py ""`` to use it as a CLI, -or import :func:`investigate_launch` and call it from your own code/notebook. +A Deep-Agents-style multi-agent that produces a structured launch-intelligence +brief on any product, feature, or hardware release. Built on top of the +Perplexity **Agent API** (``client.responses.create``). + +Architecture +------------ + +The cookbook reproduces the Deep Agents pattern (orchestrator + narrowly-scoped +sub-agents + a shared virtual filesystem of workpapers) but every model call is +made through the Perplexity Agent API. There is no Sonar chat-completions +dependency. Each sub-agent is a single Agent API call with its own preset, +instructions, and tool list (``web_search`` + ``fetch_url``). The orchestrator +is also an Agent API call: it receives the four sub-agent summaries plus the +saved workpapers and synthesizes the final brief. + +* Sub-agents call the Agent API with ``web_search`` enabled, which performs + grounded retrieval and returns inline citations the model can quote verbatim. +* The orchestrator runs without web tools — it composes the brief strictly + from the workpapers the sub-agents wrote, which is what keeps the report + reproducible and grounded. +* Workpapers live in a Python dict that the script passes between sub-agents, + exactly mirroring the Deep Agents virtual filesystem semantics. + +Run ``python launch_intelligence_agent.py ""`` to use it as a +CLI, or import :func:`investigate_launch` and call it from your own +code/notebook. Docs: -- Perplexity Search API: https://docs.perplexity.ai/docs/search-api -- LangChain Perplexity: https://python.langchain.com/docs/integrations/chat/perplexity -- deepagents: https://github.com/langchain-ai/deepagents +- Agent API quickstart: https://docs.perplexity.ai/docs/agent-api/quickstart +- web_search tool: https://docs.perplexity.ai/docs/agent-api/web-search +- fetch_url tool: https://docs.perplexity.ai/docs/agent-api/fetch-url """ from __future__ import annotations @@ -35,28 +43,31 @@ import os import sys import time -from dataclasses import dataclass -from typing import Any, Dict, Iterable, List, Optional, Tuple +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple # --------------------------------------------------------------------------- # Cookbook attribution. The Perplexity SDK forwards extra HTTP headers through -# ``default_headers``; ``X-Pplx-Integration`` lets the API team identify -# integration traffic. For the LangChain chat model we cannot easily inject -# request headers, so attribution is best-effort. +# ``default_headers``; ``X-Pplx-Integration`` lets the Perplexity team +# attribute integration traffic. # --------------------------------------------------------------------------- COOKBOOK_SLUG = "cookbook/deep-agents-launch-intelligence/0.1.0" PPLX_INTEGRATION_HEADER = {"X-Pplx-Integration": COOKBOOK_SLUG} -# Default Perplexity chat model surfaced through ``langchain-perplexity``. -# Override with --model or PPLX_MODEL. -DEFAULT_MODEL = "sonar-pro" +# Default Agent API preset for sub-agents. ``pro-search`` is the recommended +# preset for grounded multi-tool research over the open web. +DEFAULT_SUBAGENT_PRESET = "pro-search" + +# The orchestrator does not need web tools — it only synthesizes from +# workpapers — so a lighter preset is fine. +DEFAULT_ORCHESTRATOR_PRESET = "pro-search" # --------------------------------------------------------------------------- -# Perplexity Search API tool (the only "external" tool the sub-agents use) +# Agent API client # --------------------------------------------------------------------------- -def _build_search_client(api_key: Optional[str]) -> Any: - """Construct an SDK client targeting the Perplexity Search API.""" +def _build_client(api_key: Optional[str]) -> Any: + """Construct the Perplexity SDK client with the cookbook attribution slug.""" try: from perplexity import Perplexity except ImportError as exc: # pragma: no cover @@ -70,383 +81,401 @@ def _build_search_client(api_key: Optional[str]) -> Any: ) -def _format_search_results(payload: Any) -> str: - """Render Perplexity Search API results as a citation-friendly Markdown list. - - The Search API returns one result list per submitted query. We flatten and - de-duplicate so the model sees a single set of (title, url, snippet) - triples. The agent should cite the exact URLs returned here rather than - inventing links. - """ - results: List[Dict[str, Any]] = [] - raw_results = ( - payload.get("results") if isinstance(payload, dict) else getattr(payload, "results", []) - ) or [] - for item in raw_results: - if not isinstance(item, dict): - item = item.model_dump() if hasattr(item, "model_dump") else dict(item) - results.append(item) - - seen: set[str] = set() - lines: List[str] = [] - for r in results: - url = r.get("url") or "" - if not url or url in seen: - continue - seen.add(url) - title = r.get("title") or url - snippet = r.get("snippet") or r.get("content") or "" - date = r.get("date") or "" - meta = f" ({date})" if date else "" - lines.append(f"- **{title}**{meta}\n {url}\n {snippet.strip()}") - if not lines: - return "No results returned by Perplexity Search." - return "\n".join(lines) - - -def make_perplexity_search_tool(api_key: Optional[str] = None) -> Any: - """Return a LangChain ``@tool`` wrapping ``client.search.create``. - - The Perplexity Search API accepts up to five queries in a single call, - which the agents exploit to fan out related searches cheaply. We expose - that as the tool's ``queries`` argument. - """ - from langchain_core.tools import tool - - client = _build_search_client(api_key) - - @tool("perplexity_search", return_direct=False) - def perplexity_search( - queries: List[str], - max_results: int = 8, - max_tokens_per_page: int = 512, - ) -> str: - """Run one or more grounded web searches via the Perplexity Search API. - - Args: - queries: 1-5 short, focused web queries. Phrase each like a search - bar query, not a chat message. - max_results: Total results to keep across all queries (1-20). - max_tokens_per_page: Per-page snippet budget. Lower is faster. - - Returns: - A Markdown bullet list of unique (title, url, snippet) results. - The agent MUST cite the URLs exactly as returned. - """ - if not queries: - return "No queries provided." - # Cap to API limits. - queries = [q.strip() for q in queries if q and q.strip()][:5] - max_results = max(1, min(int(max_results), 20)) - max_tokens_per_page = max(64, min(int(max_tokens_per_page), 1024)) - - response = client.search.create( - query=queries, - max_results=max_results, - max_tokens_per_page=max_tokens_per_page, - ) - return _format_search_results(response) - - return perplexity_search - - # --------------------------------------------------------------------------- -# Sub-agent prompts. Each sub-agent owns one slice of the brief. Keeping the -# prompts narrow is what makes the orchestrator's plan reliable. +# Sub-agent prompts. Each sub-agent owns one slice of the brief. Narrow scopes +# are what make the orchestrator's plan reliable. # --------------------------------------------------------------------------- -ANNOUNCEMENT_PROMPT = """You investigate the official announcement of a -product launch. +ANNOUNCEMENT_PROMPT = """You investigate the official announcement of a product launch. Your job: -1. Use ``perplexity_search`` (run up to 5 queries) to find the canonical first- - party announcement: vendor blog post, press release, keynote recap, spec - page, or developer changelog. Prefer first-party sources over reblogs. +1. Use the web_search and fetch_url tools to find the canonical first-party + announcement: vendor blog post, press release, keynote recap, spec page, + or developer changelog. Prefer first-party sources over reblogs. 2. Extract: launch date, headline positioning, named features, supported regions or SKUs, pricing and availability windows. -3. Save the raw bullet notes to ``announcement.md`` using ``write_file``. -4. Return a 6-10 line summary back to the orchestrator with inline citation - markers like [1], [2] mapped to the source URLs you used. Never cite a URL - that did not appear in the search tool output. +3. Return a Markdown document with two sections: + - ``## Bullet notes`` — raw extracted facts as bullets, each with a URL. + - ``## Summary`` — a 6-10 line synthesis with inline citation markers + ``[1]``, ``[2]`` mapped to the source URLs you actually used. +Never cite a URL that did not appear in your search/fetch results. """ RECEPTION_PROMPT = """You investigate independent reception of a product launch: -press coverage, expert reviews, social commentary, and benchmark/quality -reports. +press coverage, expert reviews, social commentary, and benchmark/quality reports. Your job: -1. Use ``perplexity_search`` to find at least three independent outlets +1. Use web_search and fetch_url to find at least three independent outlets (publications, hands-on reviews, analyst notes, developer forums). Avoid reblogs of the vendor press release. -2. Capture concrete praise, concrete criticism, and any quantitative - benchmarks or measurable claims. -3. Save the raw bullet notes to ``reception.md``. -4. Return a 6-10 line synthesis to the orchestrator with inline citations - ([1], [2], ...) mapped to the URLs you actually used. +2. Capture concrete praise, concrete criticism, and any quantitative benchmarks + or measurable claims. +3. Return a Markdown document with two sections: + - ``## Bullet notes`` — raw bullets, each tagged with its source URL. + - ``## Summary`` — a 6-10 line synthesis with inline citations. """ -COMPETITOR_PROMPT = """You map the competitive landscape around a product -launch. +COMPETITOR_PROMPT = """You map the competitive landscape around a product launch. Your job: 1. Identify 2-4 directly comparable products that ship today (do not invent - competitors). Use ``perplexity_search`` for each. + competitors). Use web_search and fetch_url for each. 2. For each competitor capture: vendor, equivalent feature/SKU, pricing if public, and one differentiator vs. the launched product. -3. Save the raw bullet notes to ``competitors.md``. -4. Return a Markdown table to the orchestrator with columns: Competitor, - Equivalent offering, Price, Differentiator. Include inline citations. +3. Return a Markdown document with two sections: + - ``## Bullet notes`` — raw bullets per competitor, each with a URL. + - ``## Summary`` — a Markdown table with columns: Competitor, Equivalent + offering, Price, Differentiator. Include inline citations. """ -RISK_PROMPT = """You assess execution and adoption risks around a product -launch. +RISK_PROMPT = """You assess execution and adoption risks around a product launch. Your job: -1. Use ``perplexity_search`` to find regulatory, security, supply, ecosystem, - or messaging risks raised by credible sources after the announcement. +1. Use web_search and fetch_url to find regulatory, security, supply, + ecosystem, or messaging risks raised by credible sources after the + announcement. 2. Skip generic boilerplate risks. Each risk you keep must point to a specific article, filing, or report. -3. Save the raw bullet notes to ``risks.md``. -4. Return 3-5 specific risks to the orchestrator, each with one sentence of - evidence and an inline citation. +3. Return a Markdown document with two sections: + - ``## Bullet notes`` — raw bullets, each with a URL. + - ``## Summary`` — 3-5 specific risks, each with one sentence of evidence + and an inline citation. """ -SUBAGENTS: List[Dict[str, Any]] = [ - { - "name": "announcement-scout", - "description": ( +@dataclass +class SubAgentSpec: + """A single Deep-Agents-style sub-agent backed by one Agent API call.""" + + name: str + description: str + prompt: str + workpaper: str + + +SUBAGENTS: List[SubAgentSpec] = [ + SubAgentSpec( + name="announcement-scout", + description=( "Pulls the canonical first-party announcement and headline facts " "(launch date, features, pricing, availability)." ), - "prompt": ANNOUNCEMENT_PROMPT, - }, - { - "name": "reception-analyst", - "description": ( + prompt=ANNOUNCEMENT_PROMPT, + workpaper="announcement.md", + ), + SubAgentSpec( + name="reception-analyst", + description=( "Surveys independent reception: reviews, press coverage, expert " "reactions, and any benchmarks." ), - "prompt": RECEPTION_PROMPT, - }, - { - "name": "competitor-mapper", - "description": ( + prompt=RECEPTION_PROMPT, + workpaper="reception.md", + ), + SubAgentSpec( + name="competitor-mapper", + description=( "Maps 2-4 direct competitors with equivalent SKUs, pricing, and a " "single differentiator each." ), - "prompt": COMPETITOR_PROMPT, - }, - { - "name": "risk-auditor", - "description": ( + prompt=COMPETITOR_PROMPT, + workpaper="competitors.md", + ), + SubAgentSpec( + name="risk-auditor", + description=( "Surfaces concrete regulatory, security, supply, ecosystem, or " "messaging risks raised by credible sources." ), - "prompt": RISK_PROMPT, - }, + prompt=RISK_PROMPT, + workpaper="risks.md", + ), ] # --------------------------------------------------------------------------- -# Orchestrator prompt +# Orchestrator prompt. The orchestrator only synthesizes from workpapers. # --------------------------------------------------------------------------- -ORCHESTRATOR_PROMPT = """You are the lead analyst on a product-launch -intelligence team. You coordinate four specialist sub-agents and produce one -final brief. +ORCHESTRATOR_PROMPT = """You are the lead analyst on a product-launch intelligence team. -Operating procedure: +You are given four workpapers prepared by specialist sub-agents: -1. Read the user's launch topic. If it is ambiguous, pick the most likely - recent launch and proceed — do not stall asking for clarification. -2. Plan the work using the planning tool. The standard plan is: - a. Delegate to ``announcement-scout`` to capture official launch facts. - b. Delegate to ``reception-analyst`` for independent coverage. - c. Delegate to ``competitor-mapper`` for the comparable landscape. - d. Delegate to ``risk-auditor`` for execution and adoption risks. -3. Each sub-agent writes its raw notes to a workpaper file - (``announcement.md``, ``reception.md``, ``competitors.md``, ``risks.md``) - and returns a short citation-rich summary to you. -4. Read the workpapers back with ``read_file`` if you need detail beyond the - summaries. -5. Synthesize the final report into ``final_report.md`` using ``write_file``. - Use this exact section order: +- ``announcement.md`` — official launch facts +- ``reception.md`` — independent reception +- ``competitors.md`` — competitive landscape +- ``risks.md`` — execution and adoption risks - # — Launch Intelligence Brief +Synthesize them into a single brief using exactly this section order: - ## 1. Headline - One paragraph: what shipped, when, by whom, at what price. +# — Launch Intelligence Brief - ## 2. Official announcement - Key features and availability, distilled from announcement.md. +## 1. Headline +One paragraph: what shipped, when, by whom, at what price. - ## 3. Independent reception - What reviewers and analysts actually said. Quote sparingly, attribute - always. +## 2. Official announcement +Key features and availability, distilled from announcement.md. - ## 4. Competitive landscape - Markdown table from competitors.md. +## 3. Independent reception +What reviewers and analysts actually said. Quote sparingly, attribute always. - ## 5. Risks and open questions - 3-5 specific risks from risks.md. +## 4. Competitive landscape +Markdown table from competitors.md. - ## 6. Sources - Numbered list of unique URLs cited above. Use only URLs that appeared in - the sub-agents' summaries or workpapers — never invent a URL. +## 5. Risks and open questions +3-5 specific risks from risks.md. -6. After writing ``final_report.md`` return its full contents as your final - answer. +## 6. Sources +Numbered list of unique URLs cited above. Hard rules: -* Never cite a URL that did not appear in a Perplexity Search result. If the - sub-agents did not produce a fact, omit it rather than guessing. +* Never cite a URL that did not appear in the workpapers. If a fact is missing + from the workpapers, omit it rather than guessing. * Numbers must be attributed (date, source). If a number is not in the workpapers, write "not disclosed". * Keep the final report under ~700 words. + +Return only the final report Markdown. Do not call any tools. """ # --------------------------------------------------------------------------- -# Agent construction +# Agent API call helpers # --------------------------------------------------------------------------- +def _safe_output_text(response: Any) -> str: + """Concatenate every assistant text block in an Agent API response. + + The SDK exposes ``response.output_text`` but only when every output item is + a message; tool-call items break that helper, so walk the list defensively. + """ + chunks: List[str] = [] + for item in getattr(response, "output", []) or []: + item_type = getattr(item, "type", None) or ( + item.get("type") if isinstance(item, dict) else None + ) + if item_type != "message": + continue + content = ( + getattr(item, "content", None) + if not isinstance(item, dict) + else item.get("content") + ) + for block in content or []: + block_type = getattr(block, "type", None) or ( + block.get("type") if isinstance(block, dict) else None + ) + if block_type != "output_text": + continue + text = ( + getattr(block, "text", None) + if not isinstance(block, dict) + else block.get("text") + ) + if text: + chunks.append(text) + return "\n\n".join(chunks) + + +def _collect_search_urls(response: Any) -> List[str]: + """Pull URLs out of ``web_search_results`` items in the Agent API output.""" + urls: List[str] = [] + seen: set[str] = set() + for item in getattr(response, "output", []) or []: + item_type = getattr(item, "type", None) or ( + item.get("type") if isinstance(item, dict) else None + ) + if item_type not in ("web_search_results", "fetch_url_results"): + continue + nested = ( + getattr(item, "results", None) + if not isinstance(item, dict) + else item.get("results", []) + ) or [] + for r in nested: + url = ( + getattr(r, "url", None) + if not isinstance(r, dict) + else r.get("url") + ) + if url and url not in seen: + seen.add(url) + urls.append(url) + return urls + + @dataclass class AgentConfig: """Knobs the CLI and ``investigate_launch`` share.""" - model: str = DEFAULT_MODEL + subagent_preset: str = DEFAULT_SUBAGENT_PRESET + orchestrator_preset: str = DEFAULT_ORCHESTRATOR_PRESET api_key: Optional[str] = None - temperature: float = 0.0 + subagent_max_steps: int = 8 + subagent_max_output_tokens: int = 2048 + orchestrator_max_output_tokens: int = 2048 -def _build_chat_model(cfg: AgentConfig) -> Any: - """Return a LangChain chat model backed by Perplexity. +def run_subagent( + client: Any, + spec: SubAgentSpec, + topic: str, + cfg: AgentConfig, +) -> Tuple[str, List[str]]: + """Run a single sub-agent through the Agent API. - Uses the official ``langchain-perplexity`` integration when available, and - falls back to ``ChatOpenAI`` pointed at ``api.perplexity.ai`` otherwise so - the example still runs in environments that have not pinned the new - package yet. + Returns ``(workpaper_markdown, source_urls)``. """ - api_key = cfg.api_key or os.environ.get("PERPLEXITY_API_KEY") - if not api_key: - raise RuntimeError( - "Set PERPLEXITY_API_KEY (or pass --api-key) before running." - ) - - try: - from langchain_perplexity import ChatPerplexity - - return ChatPerplexity( - model=cfg.model, - temperature=cfg.temperature, - pplx_api_key=api_key, - ) - except ImportError: - pass - - # Fallback: OpenAI-compatible endpoint. Header injection works here. - from langchain_openai import ChatOpenAI - - return ChatOpenAI( - model=cfg.model, - temperature=cfg.temperature, - api_key=api_key, - base_url="https://api.perplexity.ai", - default_headers=PPLX_INTEGRATION_HEADER, + user_input = ( + f"Launch topic: {topic}\n\n" + f"Produce the workpaper described in your instructions." ) + response = client.responses.create( + preset=cfg.subagent_preset, + instructions=spec.prompt, + input=user_input, + tools=[ + {"type": "web_search"}, + {"type": "fetch_url"}, + ], + max_steps=cfg.subagent_max_steps, + max_output_tokens=cfg.subagent_max_output_tokens, + ) + text = _safe_output_text(response) + urls = _collect_search_urls(response) + return text, urls -def build_agent(cfg: Optional[AgentConfig] = None) -> Any: - """Construct the deep agent: orchestrator + 4 sub-agents + search tool.""" - cfg = cfg or AgentConfig() - try: - from deepagents import create_deep_agent - except ImportError as exc: # pragma: no cover - raise RuntimeError( - "deepagents is required. Install with `pip install deepagents`." - ) from exc - - chat_model = _build_chat_model(cfg) - search_tool = make_perplexity_search_tool(cfg.api_key) +def run_orchestrator( + client: Any, + topic: str, + files: Dict[str, str], + cfg: AgentConfig, +) -> str: + """Synthesize the final brief from the sub-agent workpapers.""" + workpaper_blob = "\n\n".join( + f"--- {name} ---\n{content}" for name, content in files.items() + ) + user_input = ( + f"Launch topic: {topic}\n\n" + "Workpapers:\n\n" + f"{workpaper_blob}\n\n" + "Write final_report.md following the section order in your " + "instructions." + ) - return create_deep_agent( - model=chat_model, - tools=[search_tool], + response = client.responses.create( + preset=cfg.orchestrator_preset, instructions=ORCHESTRATOR_PROMPT, - subagents=SUBAGENTS, + input=user_input, + max_output_tokens=cfg.orchestrator_max_output_tokens, ) + return _safe_output_text(response) # --------------------------------------------------------------------------- -# Programmatic entry point +# Public entry points # --------------------------------------------------------------------------- +@dataclass +class InvestigationResult: + """Bundles the orchestrator output, raw workpapers, and source URLs.""" + + final_report: str + files: Dict[str, str] = field(default_factory=dict) + sources: Dict[str, List[str]] = field(default_factory=dict) + + def investigate_launch( topic: str, cfg: Optional[AgentConfig] = None, - recursion_limit: int = 60, -) -> Dict[str, Any]: - """Run the agent end-to-end and return the final state. - - The returned dict contains ``messages`` (the full LangGraph trace) and - ``files`` (the virtual workpapers written by the sub-agents). Callers - typically want ``files["final_report.md"]``. + progress: Optional[Callable[[str, float], None]] = None, +) -> InvestigationResult: + """Run the full pipeline: 4 sub-agents → orchestrator. + + Args: + topic: Launch to investigate. + cfg: Tunable knobs (presets, max steps, etc.). + progress: Optional callback ``(stage_name, elapsed_seconds)`` invoked + after each sub-agent and the orchestrator finish. Useful for + CLIs and notebooks that want a live progress line. """ - agent = build_agent(cfg) - result = agent.invoke( - {"messages": [{"role": "user", "content": topic}]}, - {"recursion_limit": recursion_limit}, + cfg = cfg or AgentConfig() + client = _build_client(cfg.api_key) + + files: Dict[str, str] = {} + sources: Dict[str, List[str]] = {} + started = time.time() + + for spec in SUBAGENTS: + text, urls = run_subagent(client, spec, topic, cfg) + files[spec.workpaper] = text + sources[spec.name] = urls + if progress: + progress(spec.name, time.time() - started) + + final_report = run_orchestrator(client, topic, files, cfg) + files["final_report.md"] = final_report + if progress: + progress("orchestrator", time.time() - started) + + return InvestigationResult( + final_report=final_report, + files=files, + sources=sources, ) - return result -# --------------------------------------------------------------------------- -# Streaming progress (handy for notebooks / long-running runs) -# --------------------------------------------------------------------------- def stream_launch( topic: str, cfg: Optional[AgentConfig] = None, - recursion_limit: int = 60, -) -> Iterable[Tuple[str, Any]]: - """Yield ``(node_name, state_update)`` tuples as the graph runs. +) -> Iterable[Tuple[str, float, Dict[str, str]]]: + """Yield ``(stage_name, elapsed_seconds, files_so_far)`` after each stage. - Useful for live progress UIs — print one line per yielded tuple. + Equivalent to ``investigate_launch`` with a progress callback, but exposes + the partial state at each tick so callers can inspect intermediate + workpapers. """ - agent = build_agent(cfg) - for chunk in agent.stream( - {"messages": [{"role": "user", "content": topic}]}, - {"recursion_limit": recursion_limit}, - stream_mode="updates", - ): - for node, update in chunk.items(): - yield node, update + cfg = cfg or AgentConfig() + client = _build_client(cfg.api_key) + + files: Dict[str, str] = {} + started = time.time() + + for spec in SUBAGENTS: + text, _urls = run_subagent(client, spec, topic, cfg) + files[spec.workpaper] = text + yield spec.name, time.time() - started, dict(files) + + final_report = run_orchestrator(client, topic, files, cfg) + files["final_report.md"] = final_report + yield "orchestrator", time.time() - started, dict(files) # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- -def _print_final_report(result: Dict[str, Any]) -> None: - files = result.get("files", {}) or {} - if "final_report.md" in files: - print(files["final_report.md"]) +def _print_final_report(result: InvestigationResult) -> None: + if result.final_report: + print(result.final_report) + return + if "final_report.md" in result.files: + print(result.files["final_report.md"]) return - # No workpaper written — fall back to the last assistant message. - messages = result.get("messages", []) or [] - if messages: - last = messages[-1] - text = getattr(last, "content", None) or ( - last.get("content") if isinstance(last, dict) else "" - ) - if text: - print(text) - return print("(no final_report.md produced)") +def _serialize_result(result: InvestigationResult) -> Dict[str, Any]: + return { + "final_report": result.final_report, + "files": result.files, + "sources": result.sources, + } + + def main(argv: Optional[List[str]] = None) -> int: parser = argparse.ArgumentParser( description=( - "Generate a launch-intelligence brief with a Perplexity-powered " - "LangChain Deep Agent." + "Generate a launch-intelligence brief with a Deep-Agents-style " + "multi-agent built on the Perplexity Agent API." ) ) parser.add_argument( @@ -454,9 +483,22 @@ def main(argv: Optional[List[str]] = None) -> int: help="Launch to investigate, e.g. 'NVIDIA Rubin GPU announcement'.", ) parser.add_argument( - "--model", - default=os.environ.get("PPLX_MODEL", DEFAULT_MODEL), - help=f"Perplexity chat model (default: {DEFAULT_MODEL}).", + "--preset", + default=os.environ.get("PPLX_PRESET", DEFAULT_SUBAGENT_PRESET), + help=( + f"Agent API preset for the four sub-agents " + f"(default: {DEFAULT_SUBAGENT_PRESET})." + ), + ) + parser.add_argument( + "--orchestrator-preset", + default=os.environ.get( + "PPLX_ORCHESTRATOR_PRESET", DEFAULT_ORCHESTRATOR_PRESET + ), + help=( + f"Agent API preset for the orchestrator " + f"(default: {DEFAULT_ORCHESTRATOR_PRESET})." + ), ) parser.add_argument( "--api-key", @@ -466,50 +508,63 @@ def main(argv: Optional[List[str]] = None) -> int: parser.add_argument( "--stream", action="store_true", - help="Stream sub-agent progress to stderr while the graph runs.", + help="Stream sub-agent progress to stderr while the run executes.", ) parser.add_argument( "--json", action="store_true", - help="Emit the full final state as JSON instead of the report.", + help="Emit the full result (workpapers + sources) as JSON.", ) parser.add_argument( - "--recursion-limit", + "--max-steps", type=int, - default=60, - help="LangGraph recursion limit (default: 60).", + default=8, + help="Max tool-call steps per sub-agent (default: 8).", ) args = parser.parse_args(argv) - cfg = AgentConfig(model=args.model, api_key=args.api_key) - started = time.time() + cfg = AgentConfig( + subagent_preset=args.preset, + orchestrator_preset=args.orchestrator_preset, + api_key=args.api_key, + subagent_max_steps=args.max_steps, + ) if args.stream: - # Stream mode: print a one-line progress marker per node update, - # then print the final report from the last accumulated state. - last_state: Dict[str, Any] = {} + last: Optional[InvestigationResult] = None + files_acc: Dict[str, str] = {} try: - for node, update in stream_launch(args.topic, cfg, args.recursion_limit): - print(f"[{time.time() - started:6.1f}s] {node}", file=sys.stderr) - if isinstance(update, dict): - last_state.update(update) + for stage, elapsed, files in stream_launch(args.topic, cfg): + print(f"[{elapsed:6.1f}s] {stage}", file=sys.stderr) + files_acc = files + last = InvestigationResult( + final_report=files_acc.get("final_report.md", ""), + files=files_acc, + ) except Exception as err: # noqa: BLE001 print(f"Agent error: {err}", file=sys.stderr) return 2 if args.json: - print(json.dumps(last_state, indent=2, default=str)) + print(json.dumps(_serialize_result(last), indent=2, default=str)) else: - _print_final_report(last_state) + _print_final_report(last) return 0 + started = time.time() + + def _progress(stage: str, elapsed: float) -> None: + print(f"[{elapsed:6.1f}s] {stage}", file=sys.stderr) + try: - result = investigate_launch(args.topic, cfg, args.recursion_limit) + result = investigate_launch(args.topic, cfg, progress=_progress) except Exception as err: # noqa: BLE001 print(f"Agent error: {err}", file=sys.stderr) return 2 + print(f"Total elapsed: {time.time() - started:.1f}s", file=sys.stderr) + if args.json: - print(json.dumps(result, indent=2, default=str)) + print(json.dumps(_serialize_result(result), indent=2, default=str)) else: _print_final_report(result) return 0 diff --git a/docs/articles/deep-agents-launch-intelligence/requirements.txt b/docs/articles/deep-agents-launch-intelligence/requirements.txt index b96595d..8d6a26c 100644 --- a/docs/articles/deep-agents-launch-intelligence/requirements.txt +++ b/docs/articles/deep-agents-launch-intelligence/requirements.txt @@ -1,5 +1 @@ -deepagents>=0.0.5 -langchain-core>=0.3.0 -langchain-perplexity>=0.1.0 -langchain-openai>=0.3.0 perplexityai>=0.6.0