Skip to content

Commit 03dc8f7

Browse files
authored
Adds example for financial agent (#255)
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 sub‑agents and a verification step. The flow is: 1. **Planning**: A planner agent turns the end user’s 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 built‑in `WebSearchTool` to retrieve terse summaries for each search term. (You could also add `FileSearchTool` if you have indexed PDFs or 10‑Ks.) 3. **Sub‑analysts**: 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 writer agent brings together the search snippets and any sub‑analyst summaries into a long‑form markdown report plus a short executive summary. 5. **Verification**: A final verifier agent audits the report for obvious inconsistencies or missing sourcing.
2 parents cef3d53 + 0dec571 commit 03dc8f7

12 files changed

+399
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Financial Research Agent Example
2+
3+
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 sub‑agents and a verification step.
4+
5+
The flow is:
6+
7+
1. **Planning**: A planner agent turns the end user’s request into a list of search terms relevant to financial analysis – recent news, earnings calls, corporate filings, industry commentary, etc.
8+
2. **Search**: A search agent uses the built‑in `WebSearchTool` to retrieve terse summaries for each search term. (You could also add `FileSearchTool` if you have indexed PDFs or 10‑Ks.)
9+
3. **Sub‑analysts**: 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.
10+
4. **Writing**: A senior writer agent brings together the search snippets and any sub‑analyst summaries into a long‑form markdown report plus a short executive summary.
11+
5. **Verification**: A final verifier agent audits the report for obvious inconsistencies or missing sourcing.
12+
13+
You can run the example with:
14+
15+
```bash
16+
python -m examples.financial_research_agent.main
17+
```
18+
19+
and enter a query like:
20+
21+
```
22+
Write up an analysis of Apple Inc.'s most recent quarter.
23+
```
24+
25+
### Starter prompt
26+
27+
The writer agent is seeded with instructions similar to:
28+
29+
```
30+
You are a senior financial analyst. You will be provided with the original query
31+
and a set of raw search summaries. Your job is to synthesize these into a
32+
long‑form markdown report (at least several paragraphs) with a short executive
33+
summary. You also have access to tools like `fundamentals_analysis` and
34+
`risk_analysis` to get short specialist write‑ups if you want to incorporate them.
35+
Add a few follow‑up questions for further research.
36+
```
37+
38+
You can tweak these prompts and sub‑agents to suit your own data sources and preferred report structure.

examples/financial_research_agent/__init__.py

Whitespace-only changes.

examples/financial_research_agent/agents/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent
4+
5+
# A sub‑agent focused on analyzing a company's fundamentals.
6+
FINANCIALS_PROMPT = (
7+
"You are a financial analyst focused on company fundamentals such as revenue, "
8+
"profit, margins and growth trajectory. Given a collection of web (and optional file) "
9+
"search results about a company, write a concise analysis of its recent financial "
10+
"performance. Pull out key metrics or quotes. Keep it under 2 paragraphs."
11+
)
12+
13+
14+
class AnalysisSummary(BaseModel):
15+
summary: str
16+
"""Short text summary for this aspect of the analysis."""
17+
18+
19+
financials_agent = Agent(
20+
name="FundamentalsAnalystAgent",
21+
instructions=FINANCIALS_PROMPT,
22+
output_type=AnalysisSummary,
23+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent
4+
5+
# Generate a plan of searches to ground the financial analysis.
6+
# For a given financial question or company, we want to search for
7+
# recent news, official filings, analyst commentary, and other
8+
# relevant background.
9+
PROMPT = (
10+
"You are a financial research planner. Given a request for financial analysis, "
11+
"produce a set of web searches to gather the context needed. Aim for recent "
12+
"headlines, earnings calls or 10‑K snippets, analyst commentary, and industry background. "
13+
"Output between 5 and 15 search terms to query for."
14+
)
15+
16+
17+
class FinancialSearchItem(BaseModel):
18+
reason: str
19+
"""Your reasoning for why this search is relevant."""
20+
21+
query: str
22+
"""The search term to feed into a web (or file) search."""
23+
24+
25+
class FinancialSearchPlan(BaseModel):
26+
searches: list[FinancialSearchItem]
27+
"""A list of searches to perform."""
28+
29+
30+
planner_agent = Agent(
31+
name="FinancialPlannerAgent",
32+
instructions=PROMPT,
33+
model="o3-mini",
34+
output_type=FinancialSearchPlan,
35+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent
4+
5+
# A sub‑agent specializing in identifying risk factors or concerns.
6+
RISK_PROMPT = (
7+
"You are a risk analyst looking for potential red flags in a company's outlook. "
8+
"Given background research, produce a short analysis of risks such as competitive threats, "
9+
"regulatory issues, supply chain problems, or slowing growth. Keep it under 2 paragraphs."
10+
)
11+
12+
13+
class AnalysisSummary(BaseModel):
14+
summary: str
15+
"""Short text summary for this aspect of the analysis."""
16+
17+
18+
risk_agent = Agent(
19+
name="RiskAnalystAgent",
20+
instructions=RISK_PROMPT,
21+
output_type=AnalysisSummary,
22+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from agents import Agent, WebSearchTool
2+
from agents.model_settings import ModelSettings
3+
4+
# Given a search term, use web search to pull back a brief summary.
5+
# Summaries should be concise but capture the main financial points.
6+
INSTRUCTIONS = (
7+
"You are a research assistant specializing in financial topics. "
8+
"Given a search term, use web search to retrieve up‑to‑date context and "
9+
"produce a short summary of at most 300 words. Focus on key numbers, events, "
10+
"or quotes that will be useful to a financial analyst."
11+
)
12+
13+
search_agent = Agent(
14+
name="FinancialSearchAgent",
15+
instructions=INSTRUCTIONS,
16+
tools=[WebSearchTool()],
17+
model_settings=ModelSettings(tool_choice="required"),
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent
4+
5+
# Agent to sanity‑check a synthesized report for consistency and recall.
6+
# This can be used to flag potential gaps or obvious mistakes.
7+
VERIFIER_PROMPT = (
8+
"You are a meticulous auditor. You have been handed a financial analysis report. "
9+
"Your job is to verify the report is internally consistent, clearly sourced, and makes "
10+
"no unsupported claims. Point out any issues or uncertainties."
11+
)
12+
13+
14+
class VerificationResult(BaseModel):
15+
verified: bool
16+
"""Whether the report seems coherent and plausible."""
17+
18+
issues: str
19+
"""If not verified, describe the main issues or concerns."""
20+
21+
22+
verifier_agent = Agent(
23+
name="VerificationAgent",
24+
instructions=VERIFIER_PROMPT,
25+
model="gpt-4o",
26+
output_type=VerificationResult,
27+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pydantic import BaseModel
2+
3+
from agents import Agent
4+
5+
# Writer agent brings together the raw search results and optionally calls out
6+
# to sub‑analyst tools for specialized commentary, then returns a cohesive markdown report.
7+
WRITER_PROMPT = (
8+
"You are a senior financial analyst. You will be provided with the original query and "
9+
"a set of raw search summaries. Your task is to synthesize these into a long‑form markdown "
10+
"report (at least several paragraphs) including a short executive summary and follow‑up "
11+
"questions. If needed, you can call the available analysis tools (e.g. fundamentals_analysis, "
12+
"risk_analysis) to get short specialist write‑ups to incorporate."
13+
)
14+
15+
16+
class FinancialReportData(BaseModel):
17+
short_summary: str
18+
"""A short 2‑3 sentence executive summary."""
19+
20+
markdown_report: str
21+
"""The full markdown report."""
22+
23+
follow_up_questions: list[str]
24+
"""Suggested follow‑up questions for further research."""
25+
26+
27+
# Note: We will attach handoffs to specialist analyst agents at runtime in the manager.
28+
# This shows how an agent can use handoffs to delegate to specialized subagents.
29+
writer_agent = Agent(
30+
name="FinancialWriterAgent",
31+
instructions=WRITER_PROMPT,
32+
model="gpt-4.5-preview-2025-02-27",
33+
output_type=FinancialReportData,
34+
)
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import asyncio
2+
3+
from .manager import FinancialResearchManager
4+
5+
6+
# Entrypoint for the financial bot example.
7+
# Run this as `python -m examples.financial_bot.main` and enter a
8+
# financial research query, for example:
9+
# "Write up an analysis of Apple Inc.'s most recent quarter."
10+
async def main() -> None:
11+
query = input("Enter a financial research query: ")
12+
mgr = FinancialResearchManager()
13+
await mgr.run(query)
14+
15+
16+
if __name__ == "__main__":
17+
asyncio.run(main())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import time
5+
from collections.abc import Sequence
6+
7+
from rich.console import Console
8+
9+
from agents import Runner, RunResult, custom_span, gen_trace_id, trace
10+
11+
from .agents.financials_agent import financials_agent
12+
from .agents.planner_agent import FinancialSearchItem, FinancialSearchPlan, planner_agent
13+
from .agents.risk_agent import risk_agent
14+
from .agents.search_agent import search_agent
15+
from .agents.verifier_agent import VerificationResult, verifier_agent
16+
from .agents.writer_agent import FinancialReportData, writer_agent
17+
from .printer import Printer
18+
19+
20+
async def _summary_extractor(run_result: RunResult) -> str:
21+
"""Custom output extractor for sub‑agents that return an AnalysisSummary."""
22+
# The financial/risk analyst agents emit an AnalysisSummary with a `summary` field.
23+
# We want the tool call to return just that summary text so the writer can drop it inline.
24+
return str(run_result.final_output.summary)
25+
26+
27+
class FinancialResearchManager:
28+
"""
29+
Orchestrates the full flow: planning, searching, sub‑analysis, writing, and verification.
30+
"""
31+
32+
def __init__(self) -> None:
33+
self.console = Console()
34+
self.printer = Printer(self.console)
35+
36+
async def run(self, query: str) -> None:
37+
trace_id = gen_trace_id()
38+
with trace("Financial research trace", trace_id=trace_id):
39+
self.printer.update_item(
40+
"trace_id",
41+
f"View trace: https://platform.openai.com/traces/{trace_id}",
42+
is_done=True,
43+
hide_checkmark=True,
44+
)
45+
self.printer.update_item(
46+
"start", "Starting financial research...", is_done=True)
47+
search_plan = await self._plan_searches(query)
48+
search_results = await self._perform_searches(search_plan)
49+
report = await self._write_report(query, search_results)
50+
verification = await self._verify_report(report)
51+
52+
final_report = f"Report summary\n\n{report.short_summary}"
53+
self.printer.update_item(
54+
"final_report", final_report, is_done=True)
55+
56+
self.printer.end()
57+
58+
# Print to stdout
59+
print("\n\n=====REPORT=====\n\n")
60+
print(f"Report:\n{report.markdown_report}")
61+
print("\n\n=====FOLLOW UP QUESTIONS=====\n\n")
62+
print("\n".join(report.follow_up_questions))
63+
print("\n\n=====VERIFICATION=====\n\n")
64+
print(verification)
65+
66+
async def _plan_searches(self, query: str) -> FinancialSearchPlan:
67+
self.printer.update_item("planning", "Planning searches...")
68+
result = await Runner.run(planner_agent, f"Query: {query}")
69+
self.printer.update_item(
70+
"planning",
71+
f"Will perform {len(result.final_output.searches)} searches",
72+
is_done=True,
73+
)
74+
return result.final_output_as(FinancialSearchPlan)
75+
76+
async def _perform_searches(self, search_plan: FinancialSearchPlan) -> Sequence[str]:
77+
with custom_span("Search the web"):
78+
self.printer.update_item("searching", "Searching...")
79+
tasks = [asyncio.create_task(self._search(item))
80+
for item in search_plan.searches]
81+
results: list[str] = []
82+
num_completed = 0
83+
for task in asyncio.as_completed(tasks):
84+
result = await task
85+
if result is not None:
86+
results.append(result)
87+
num_completed += 1
88+
self.printer.update_item(
89+
"searching", f"Searching... {num_completed}/{len(tasks)} completed"
90+
)
91+
self.printer.mark_item_done("searching")
92+
return results
93+
94+
async def _search(self, item: FinancialSearchItem) -> str | None:
95+
input_data = f"Search term: {item.query}\nReason: {item.reason}"
96+
try:
97+
result = await Runner.run(search_agent, input_data)
98+
return str(result.final_output)
99+
except Exception:
100+
return None
101+
102+
async def _write_report(self, query: str, search_results: Sequence[str]) -> FinancialReportData:
103+
# Expose the specialist analysts as tools so the writer can invoke them inline
104+
# and still produce the final FinancialReportData output.
105+
fundamentals_tool = financials_agent.as_tool(
106+
tool_name="fundamentals_analysis",
107+
tool_description="Use to get a short write‑up of key financial metrics",
108+
custom_output_extractor=_summary_extractor,
109+
)
110+
risk_tool = risk_agent.as_tool(
111+
tool_name="risk_analysis",
112+
tool_description="Use to get a short write‑up of potential red flags",
113+
custom_output_extractor=_summary_extractor,
114+
)
115+
writer_with_tools = writer_agent.clone(
116+
tools=[fundamentals_tool, risk_tool])
117+
self.printer.update_item("writing", "Thinking about report...")
118+
input_data = f"Original query: {query}\nSummarized search results: {search_results}"
119+
result = Runner.run_streamed(writer_with_tools, input_data)
120+
update_messages = [
121+
"Planning report structure...",
122+
"Writing sections...",
123+
"Finalizing report...",
124+
]
125+
last_update = time.time()
126+
next_message = 0
127+
async for _ in result.stream_events():
128+
if time.time() - last_update > 5 and next_message < len(update_messages):
129+
self.printer.update_item(
130+
"writing", update_messages[next_message])
131+
next_message += 1
132+
last_update = time.time()
133+
self.printer.mark_item_done("writing")
134+
return result.final_output_as(FinancialReportData)
135+
136+
async def _verify_report(self, report: FinancialReportData) -> VerificationResult:
137+
self.printer.update_item("verifying", "Verifying report...")
138+
result = await Runner.run(verifier_agent, report.markdown_report)
139+
self.printer.mark_item_done("verifying")
140+
return result.final_output_as(VerificationResult)

0 commit comments

Comments
 (0)