chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:17 +08:00
commit 4ed4e9ff99
1368 changed files with 334957 additions and 0 deletions
@@ -0,0 +1,38 @@
# Financial Research Agent Example
This example shows how you might compose a richer financial research agent using the Agents SDK. The pattern is similar to the `research_bot` example, but with more specialized subagents and a verification step.
The flow is:
1. **Planning**: A planner agent turns the end users request into a list of search terms relevant to financial analysis recent news, earnings calls, corporate filings, industry commentary, etc.
2. **Search**: A search agent uses the builtin `WebSearchTool` to retrieve terse summaries for each search term. (You could also add `FileSearchTool` if you have indexed PDFs or 10Ks.)
3. **Subanalysts**: Additional agents (e.g. a fundamentals analyst and a risk analyst) are exposed as tools so the writer can call them inline and incorporate their outputs.
4. **Writing**: A senior writer agent brings together the search snippets and any subanalyst summaries into a longform markdown report plus a short executive summary.
5. **Verification**: A final verifier agent audits the report for obvious inconsistencies or missing sourcing.
You can run the example with:
```bash
python -m examples.financial_research_agent.main
```
and enter a query like:
```
Write up an analysis of Apple Inc.'s most recent quarter.
```
### Starter prompt
The writer agent is seeded with instructions similar to:
```
You are a senior financial analyst. You will be provided with the original query
and a set of raw search summaries. Your job is to synthesize these into a
longform markdown report (at least several paragraphs) with a short executive
summary. You also have access to tools like `fundamentals_analysis` and
`risk_analysis` to get short specialist writeups if you want to incorporate them.
Add a few followup questions for further research.
```
You can tweak these prompts and subagents to suit your own data sources and preferred report structure.
@@ -0,0 +1,23 @@
from pydantic import BaseModel
from agents import Agent
# A subagent focused on analyzing a company's fundamentals.
FINANCIALS_PROMPT = (
"You are a financial analyst focused on company fundamentals such as revenue, "
"profit, margins and growth trajectory. Given a collection of web (and optional file) "
"search results about a company, write a concise analysis of its recent financial "
"performance. Pull out key metrics or quotes. Keep it under 2 paragraphs."
)
class AnalysisSummary(BaseModel):
summary: str
"""Short text summary for this aspect of the analysis."""
financials_agent = Agent(
name="FundamentalsAnalystAgent",
instructions=FINANCIALS_PROMPT,
output_type=AnalysisSummary,
)
@@ -0,0 +1,35 @@
from pydantic import BaseModel
from agents import Agent
# Generate a plan of searches to ground the financial analysis.
# For a given financial question or company, we want to search for
# recent news, official filings, analyst commentary, and other
# relevant background.
PROMPT = (
"You are a financial research planner. Given a request for financial analysis, "
"produce a set of web searches to gather the context needed. Aim for recent "
"headlines, earnings calls or 10K snippets, analyst commentary, and industry background. "
"Output between 5 and 15 search terms to query for."
)
class FinancialSearchItem(BaseModel):
reason: str
"""Your reasoning for why this search is relevant."""
query: str
"""The search term to feed into a web (or file) search."""
class FinancialSearchPlan(BaseModel):
searches: list[FinancialSearchItem]
"""A list of searches to perform."""
planner_agent = Agent(
name="FinancialPlannerAgent",
instructions=PROMPT,
model="o3-mini",
output_type=FinancialSearchPlan,
)
@@ -0,0 +1,22 @@
from pydantic import BaseModel
from agents import Agent
# A subagent specializing in identifying risk factors or concerns.
RISK_PROMPT = (
"You are a risk analyst looking for potential red flags in a company's outlook. "
"Given background research, produce a short analysis of risks such as competitive threats, "
"regulatory issues, supply chain problems, or slowing growth. Keep it under 2 paragraphs."
)
class AnalysisSummary(BaseModel):
summary: str
"""Short text summary for this aspect of the analysis."""
risk_agent = Agent(
name="RiskAnalystAgent",
instructions=RISK_PROMPT,
output_type=AnalysisSummary,
)
@@ -0,0 +1,27 @@
from pydantic import BaseModel
from agents import Agent, ModelSettings, WebSearchTool
# Given a search term, use web search to pull back a brief summary.
# Summaries should be concise but capture the main financial points.
INSTRUCTIONS = (
"You are a research assistant specializing in financial topics. "
"Given a search term, use web search to retrieve uptodate context and "
"produce a short summary of at most 300 words. Focus on key numbers, events, "
"or quotes that will be useful to a financial analyst."
)
class FinancialSearchSummary(BaseModel):
summary: str
"""A concise summary of the search findings."""
search_agent = Agent(
name="FinancialSearchAgent",
model="gpt-5.6-sol",
instructions=INSTRUCTIONS,
tools=[WebSearchTool()],
model_settings=ModelSettings(response_include=["web_search_call.action.sources"]),
output_type=FinancialSearchSummary,
)
@@ -0,0 +1,48 @@
from typing import Literal
from pydantic import BaseModel
from agents import Agent
# Agent to sanitycheck a synthesized report for consistency and recall.
# This can be used to flag potential gaps or obvious mistakes.
VERIFIER_PROMPT = (
"You are a meticulous evidence auditor. You will receive an original request, an explicit "
"research cutoff date, a financial report, and structured web research evidence with source "
"URLs. Judge the report only against that supplied evidence; do not reject or approve claims "
"based on your own memory. Check that material numeric and time-sensitive claims are supported "
"by the evidence, that citations use supplied URLs, that the report is internally consistent, "
"and that uncertainty is appropriately caveated. Treat information published on or before the "
"research cutoff as potentially available. Mark unsupported claims separately from claims that "
"the evidence directly contradicts."
)
class VerificationIssue(BaseModel):
claim: str
"""The report claim that needs attention."""
category: Literal["unsupported", "contradicted", "stale_or_unreleased", "other"]
"""The evidence problem associated with the claim."""
explanation: str
"""Why the evidence does not support the claim."""
source_urls: list[str]
"""Relevant supplied source URLs, if any."""
class VerificationResult(BaseModel):
verified: bool
"""Whether the report is coherent and supported by the supplied evidence."""
issues: list[VerificationIssue]
"""Evidence-based issues that must be corrected before publication."""
verifier_agent = Agent(
name="VerificationAgent",
instructions=VERIFIER_PROMPT,
model="gpt-5.6-sol",
output_type=VerificationResult,
)
@@ -0,0 +1,42 @@
from pydantic import BaseModel
from agents import Agent
# Writer agent brings together the raw search results and optionally calls out
# to subanalyst tools for specialized commentary, then returns a cohesive markdown report.
WRITER_PROMPT = (
"You are a senior financial analyst. You will be provided with the original query and "
"a set of raw search summaries. Your task is to synthesize these into a longform markdown "
"report (at least several paragraphs) including a short executive summary and followup "
"questions. If needed, you can call the available analysis tools (e.g. fundamentals_analysis, "
"risk_analysis) to get short specialist writeups to incorporate. Every material numeric or "
"time-sensitive claim must include an inline Markdown citation using a URL supplied in the "
"research evidence. Never invent or alter a source URL."
)
REVISION_PROMPT = (
f"{WRITER_PROMPT} You are revising an existing report after evidence verification. Address "
"every verification issue, remove claims that cannot be supported, preserve valid analysis, "
"and return a complete replacement report rather than a patch or commentary."
)
class FinancialReportData(BaseModel):
short_summary: str
"""A short 23 sentence executive summary."""
markdown_report: str
"""The full markdown report."""
follow_up_questions: list[str]
"""Suggested followup questions for further research."""
# Note: We will attach handoffs to specialist analyst agents at runtime in the manager.
# This shows how an agent can use handoffs to delegate to specialized subagents.
writer_agent = Agent(
name="FinancialWriterAgent",
instructions=WRITER_PROMPT,
model="gpt-5.6-sol",
output_type=FinancialReportData,
)
+23
View File
@@ -0,0 +1,23 @@
import asyncio
from examples.auto_mode import input_with_fallback
from .manager import FinancialResearchManager
# Entrypoint for the financial bot example.
# Run this as `python -m examples.financial_research_agent.main` and enter a
# financial research query, for example:
# "Write up an analysis of Apple Inc.'s most recent quarter."
async def main() -> None:
query = input_with_fallback(
"Enter a financial research query: ",
"Write a short analysis of Apple's long-term revenue drivers and key risks. "
"Avoid making claims about unreleased quarterly results.",
)
mgr = FinancialResearchManager()
await mgr.run(query)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,265 @@
from __future__ import annotations
import asyncio
import json
import time
from collections.abc import Sequence
from datetime import datetime, timezone
from pydantic import BaseModel
from rich.console import Console
from agents import Runner, RunResult, RunResultStreaming, custom_span, gen_trace_id, trace
from examples.web_search_utils import extract_url_citations, extract_web_search_source_urls
from .agents.financials_agent import financials_agent
from .agents.planner_agent import FinancialSearchItem, FinancialSearchPlan, planner_agent
from .agents.risk_agent import risk_agent
from .agents.search_agent import FinancialSearchSummary, search_agent
from .agents.verifier_agent import VerificationResult, verifier_agent
from .agents.writer_agent import REVISION_PROMPT, FinancialReportData, writer_agent
from .printer import Printer
class FinancialSource(BaseModel):
title: str
url: str
class FinancialSearchEvidence(BaseModel):
query: str
reason: str
summary: str
sources: list[FinancialSource]
retrieved_at: str
def _extract_financial_sources(items: Sequence[object]) -> list[FinancialSource]:
sources: list[FinancialSource] = []
seen: set[str] = set()
for citation in extract_url_citations(items):
if citation.url in seen:
continue
seen.add(citation.url)
sources.append(FinancialSource(title=citation.title, url=citation.url))
for url in extract_web_search_source_urls(items):
if url in seen:
continue
seen.add(url)
sources.append(FinancialSource(title=url, url=url))
return sources
async def _summary_extractor(run_result: RunResult | RunResultStreaming) -> str:
"""Custom output extractor for subagents that return an AnalysisSummary."""
# The financial/risk analyst agents emit an AnalysisSummary with a `summary` field.
# We want the tool call to return just that summary text so the writer can drop it inline.
return str(run_result.final_output.summary)
class FinancialResearchManager:
"""
Orchestrates the full flow: planning, searching, subanalysis, writing, and verification.
"""
def __init__(self) -> None:
self.console = Console()
self.printer = Printer(self.console)
self.research_cutoff = datetime.now(timezone.utc).date().isoformat()
async def run(self, query: str) -> None:
trace_id = gen_trace_id()
try:
with trace("Financial research trace", trace_id=trace_id):
self.printer.update_item(
"trace_id",
f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}",
is_done=True,
hide_checkmark=True,
)
self.printer.update_item("start", "Starting financial research...", is_done=True)
search_plan = await self._plan_searches(query)
search_results = await self._perform_searches(search_plan)
report, verification = await self._produce_verified_report(query, search_results)
final_report = f"Report summary\n\n{report.short_summary}"
self.printer.update_item("final_report", final_report, is_done=True)
finally:
self.printer.end()
# Print to stdout
print("\n\n=====REPORT=====\n\n")
print(f"Report:\n{report.markdown_report}")
print("\n\n=====FOLLOW UP QUESTIONS=====\n\n")
print("\n".join(report.follow_up_questions))
print("\n\n=====VERIFICATION=====\n\n")
print(verification)
async def _produce_verified_report(
self,
query: str,
search_results: Sequence[FinancialSearchEvidence],
) -> tuple[FinancialReportData, VerificationResult]:
report = await self._write_report(query, search_results)
verification = await self._verify_report(query, report, search_results)
if verification.verified:
return report, verification
report = await self._revise_report(query, report, search_results, verification)
verification = await self._verify_report(query, report, search_results)
if not verification.verified:
raise RuntimeError(
"Financial report failed evidence verification after one revision: "
f"{verification.model_dump_json()}"
)
return report, verification
async def _plan_searches(self, query: str) -> FinancialSearchPlan:
self.printer.update_item("planning", "Planning searches...")
result = await Runner.run(planner_agent, f"Query: {query}")
self.printer.update_item(
"planning",
f"Will perform {len(result.final_output.searches)} searches",
is_done=True,
)
return result.final_output_as(FinancialSearchPlan)
async def _perform_searches(
self, search_plan: FinancialSearchPlan
) -> Sequence[FinancialSearchEvidence]:
with custom_span("Search the web"):
self.printer.update_item("searching", "Searching...")
tasks = [asyncio.create_task(self._search(item)) for item in search_plan.searches]
results: list[FinancialSearchEvidence] = []
num_completed = 0
num_succeeded = 0
num_failed = 0
for task in asyncio.as_completed(tasks):
result = await task
if result is not None:
results.append(result)
num_succeeded += 1
else:
num_failed += 1
num_completed += 1
status = f"Searching... {num_completed}/{len(tasks)} finished"
if num_failed:
status += f" ({num_succeeded} succeeded, {num_failed} failed)"
self.printer.update_item(
"searching",
status,
)
summary = f"Searches finished: {num_succeeded}/{len(tasks)} succeeded"
if num_failed:
summary += f", {num_failed} failed"
self.printer.update_item("searching", summary, is_done=True)
return results
async def _search(self, item: FinancialSearchItem) -> FinancialSearchEvidence | None:
input_data = f"Search term: {item.query}\nReason: {item.reason}"
try:
result = await Runner.run(search_agent, input_data)
search_summary = result.final_output_as(FinancialSearchSummary)
sources = _extract_financial_sources(result.new_items)
if not sources:
return None
return FinancialSearchEvidence(
query=item.query,
reason=item.reason,
summary=search_summary.summary,
sources=sources,
retrieved_at=self.research_cutoff,
)
except Exception:
return None
async def _write_report(
self,
query: str,
search_results: Sequence[FinancialSearchEvidence],
) -> FinancialReportData:
# Expose the specialist analysts as tools so the writer can invoke them inline
# and still produce the final FinancialReportData output.
fundamentals_tool = financials_agent.as_tool(
tool_name="fundamentals_analysis",
tool_description="Use to get a short writeup of key financial metrics",
custom_output_extractor=_summary_extractor,
)
risk_tool = risk_agent.as_tool(
tool_name="risk_analysis",
tool_description="Use to get a short writeup of potential red flags",
custom_output_extractor=_summary_extractor,
)
writer_with_tools = writer_agent.clone(tools=[fundamentals_tool, risk_tool])
self.printer.update_item("writing", "Thinking about report...")
input_data = self._report_input(query, search_results)
result = Runner.run_streamed(writer_with_tools, input_data)
update_messages = [
"Planning report structure...",
"Writing sections...",
"Finalizing report...",
]
last_update = time.time()
next_message = 0
async for _ in result.stream_events():
if time.time() - last_update > 5 and next_message < len(update_messages):
self.printer.update_item("writing", update_messages[next_message])
next_message += 1
last_update = time.time()
self.printer.mark_item_done("writing")
return result.final_output_as(FinancialReportData)
async def _revise_report(
self,
query: str,
report: FinancialReportData,
search_results: Sequence[FinancialSearchEvidence],
verification: VerificationResult,
) -> FinancialReportData:
self.printer.update_item("revising", "Revising report from verification feedback...")
revision_agent = writer_agent.clone(instructions=REVISION_PROMPT)
input_data = (
f"{self._report_input(query, search_results)}\n"
f"Existing report:\n{report.model_dump_json()}\n"
f"Verification feedback:\n{verification.model_dump_json()}"
)
result = await Runner.run(revision_agent, input_data)
self.printer.mark_item_done("revising")
return result.final_output_as(FinancialReportData)
async def _verify_report(
self,
query: str,
report: FinancialReportData,
search_results: Sequence[FinancialSearchEvidence],
) -> VerificationResult:
self.printer.update_item("verifying", "Verifying report...")
input_data = json.dumps(
{
"original_query": query,
"research_cutoff": self.research_cutoff,
"report": report.model_dump(mode="json"),
"evidence": [item.model_dump(mode="json") for item in search_results],
},
ensure_ascii=False,
)
result = await Runner.run(verifier_agent, input_data)
self.printer.mark_item_done("verifying")
return result.final_output_as(VerificationResult)
def _report_input(
self,
query: str,
search_results: Sequence[FinancialSearchEvidence],
) -> str:
return json.dumps(
{
"original_query": query,
"research_cutoff": self.research_cutoff,
"evidence": [item.model_dump(mode="json") for item in search_results],
},
ensure_ascii=False,
)
@@ -0,0 +1,46 @@
from typing import Any
from rich.console import Console, Group
from rich.live import Live
from rich.spinner import Spinner
class Printer:
"""
Simple wrapper to stream status updates. Used by the financial bot
manager as it orchestrates planning, search and writing.
"""
def __init__(self, console: Console) -> None:
self.live = Live(console=console)
self.items: dict[str, tuple[str, bool]] = {}
self.hide_done_ids: set[str] = set()
self.live.start()
def end(self) -> None:
self.live.stop()
def hide_done_checkmark(self, item_id: str) -> None:
self.hide_done_ids.add(item_id)
def update_item(
self, item_id: str, content: str, is_done: bool = False, hide_checkmark: bool = False
) -> None:
self.items[item_id] = (content, is_done)
if hide_checkmark:
self.hide_done_ids.add(item_id)
self.flush()
def mark_item_done(self, item_id: str) -> None:
self.items[item_id] = (self.items[item_id][0], True)
self.flush()
def flush(self) -> None:
renderables: list[Any] = []
for item_id, (content, is_done) in self.items.items():
if is_done:
prefix = "" if item_id not in self.hide_done_ids else ""
renderables.append(prefix + content)
else:
renderables.append(Spinner("dots", text=content))
self.live.update(Group(*renderables))