Skip to content

Commit dc33729

Browse files
jssmithtconley1428
andauthored
OpenAI Agents financial research and reasoning examples (#226)
* update for plugins * formatting * reference main branch * cleanup * switch to plugins on the runners * move around samples * update README files * formatting update * formatting * timeout adjustments * Port financial research example and reasoning_content example * formatting * switch model * pin model snapshot * Revert uv.lock --------- Co-authored-by: Tim Conley <[email protected]>
1 parent f4486af commit dc33729

16 files changed

+663
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Financial Research Agent
2+
3+
Multi-agent financial research system with specialized roles, extended with Temporal's durable execution.
4+
5+
*Adapted from [OpenAI Agents SDK financial research agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent)*
6+
7+
## Architecture
8+
9+
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.
10+
11+
The flow is:
12+
13+
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.
14+
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.)
15+
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.
16+
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.
17+
5. **Verification**: A final verifier agent audits the report for obvious inconsistencies or missing sourcing.
18+
19+
## Running the Example
20+
21+
First, start the worker:
22+
```bash
23+
uv run openai_agents/financial_research_agent/run_worker.py
24+
```
25+
26+
Then run the financial research workflow:
27+
```bash
28+
uv run openai_agents/financial_research_agent/run_financial_research_workflow.py
29+
```
30+
31+
Enter a query like:
32+
```
33+
Write up an analysis of Apple Inc.'s most recent quarter.
34+
```
35+
36+
You can also just hit enter to run this query, which is provided as the default.
37+
38+
## Components
39+
40+
### Agents
41+
42+
- **Planner Agent**: Creates a search plan with 5-15 relevant search terms
43+
- **Search Agent**: Uses web search to gather financial information
44+
- **Financials Agent**: Analyzes company fundamentals (revenue, profit, margins)
45+
- **Risk Agent**: Identifies potential red flags and risk factors
46+
- **Writer Agent**: Synthesizes information into a comprehensive report
47+
- **Verifier Agent**: Audits the final report for consistency and accuracy
48+
49+
### Writer Agent Tools
50+
51+
The writer agent has access to tools that invoke the specialist analysts:
52+
- `fundamentals_analysis`: Get financial performance analysis
53+
- `risk_analysis`: Get risk factor assessment
54+
55+
## Temporal Integration
56+
57+
The example demonstrates several Temporal patterns:
58+
- Durable execution of multi-step research workflows
59+
- Parallel execution of web searches using `asyncio.create_task`
60+
- Use of `workflow.as_completed` for handling concurrent tasks
61+
- Proper import handling with `workflow.unsafe.imports_passed_through()`
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from agents import Agent
2+
from pydantic import BaseModel
3+
4+
# A sub-agent focused on analyzing a company's fundamentals.
5+
FINANCIALS_PROMPT = (
6+
"You are a financial analyst focused on company fundamentals such as revenue, "
7+
"profit, margins and growth trajectory. Given a collection of web (and optional file) "
8+
"search results about a company, write a concise analysis of its recent financial "
9+
"performance. Pull out key metrics or quotes. 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+
def new_financials_agent() -> Agent:
19+
return Agent(
20+
name="FundamentalsAnalystAgent",
21+
instructions=FINANCIALS_PROMPT,
22+
output_type=AnalysisSummary,
23+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from agents import Agent
2+
from pydantic import BaseModel
3+
4+
# Generate a plan of searches to ground the financial analysis.
5+
# For a given financial question or company, we want to search for
6+
# recent news, official filings, analyst commentary, and other
7+
# relevant background.
8+
PROMPT = (
9+
"You are a financial research planner. Given a request for financial analysis, "
10+
"produce a set of web searches to gather the context needed. Aim for recent "
11+
"headlines, earnings calls or 10-K snippets, analyst commentary, and industry background. "
12+
"Output between 5 and 15 search terms to query for."
13+
)
14+
15+
16+
class FinancialSearchItem(BaseModel):
17+
reason: str
18+
"""Your reasoning for why this search is relevant."""
19+
20+
query: str
21+
"""The search term to feed into a web (or file) search."""
22+
23+
24+
class FinancialSearchPlan(BaseModel):
25+
searches: list[FinancialSearchItem]
26+
"""A list of searches to perform."""
27+
28+
29+
def new_planner_agent() -> Agent:
30+
return Agent(
31+
name="FinancialPlannerAgent",
32+
instructions=PROMPT,
33+
model="o3-mini",
34+
output_type=FinancialSearchPlan,
35+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from agents import Agent
2+
from pydantic import BaseModel
3+
4+
# A sub-agent specializing in identifying risk factors or concerns.
5+
RISK_PROMPT = (
6+
"You are a risk analyst looking for potential red flags in a company's outlook. "
7+
"Given background research, produce a short analysis of risks such as competitive threats, "
8+
"regulatory issues, supply chain problems, or slowing growth. Keep it under 2 paragraphs."
9+
)
10+
11+
12+
class AnalysisSummary(BaseModel):
13+
summary: str
14+
"""Short text summary for this aspect of the analysis."""
15+
16+
17+
def new_risk_agent() -> Agent:
18+
return Agent(
19+
name="RiskAnalystAgent",
20+
instructions=RISK_PROMPT,
21+
output_type=AnalysisSummary,
22+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
14+
def new_search_agent() -> Agent:
15+
return Agent(
16+
name="FinancialSearchAgent",
17+
instructions=INSTRUCTIONS,
18+
tools=[WebSearchTool()],
19+
model_settings=ModelSettings(tool_choice="required"),
20+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from agents import Agent
2+
from pydantic import BaseModel
3+
4+
# Agent to sanity-check a synthesized report for consistency and recall.
5+
# This can be used to flag potential gaps or obvious mistakes.
6+
VERIFIER_PROMPT = (
7+
"You are a meticulous auditor. You have been handed a financial analysis report. "
8+
"Your job is to verify the report is internally consistent, clearly sourced, and makes "
9+
"no unsupported claims. Point out any issues or uncertainties."
10+
)
11+
12+
13+
class VerificationResult(BaseModel):
14+
verified: bool
15+
"""Whether the report seems coherent and plausible."""
16+
17+
issues: str
18+
"""If not verified, describe the main issues or concerns."""
19+
20+
21+
def new_verifier_agent() -> Agent:
22+
return Agent(
23+
name="VerificationAgent",
24+
instructions=VERIFIER_PROMPT,
25+
model="gpt-4o",
26+
output_type=VerificationResult,
27+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from agents import Agent
2+
from pydantic import BaseModel
3+
4+
# Writer agent brings together the raw search results and optionally calls out
5+
# to sub-analyst tools for specialized commentary, then returns a cohesive markdown report.
6+
WRITER_PROMPT = (
7+
"You are a senior financial analyst. You will be provided with the original query and "
8+
"a set of raw search summaries. Your task is to synthesize these into a long-form markdown "
9+
"report (at least several paragraphs) including a short executive summary and follow-up "
10+
"questions. If needed, you can call the available analysis tools (e.g. fundamentals_analysis, "
11+
"risk_analysis) to get short specialist write-ups to incorporate."
12+
)
13+
14+
15+
class FinancialReportData(BaseModel):
16+
short_summary: str
17+
"""A short 2-3 sentence executive summary."""
18+
19+
markdown_report: str
20+
"""The full markdown report."""
21+
22+
follow_up_questions: list[str]
23+
"""Suggested follow-up questions for further research."""
24+
25+
26+
# Note: We will attach tools to specialist analyst agents at runtime in the manager.
27+
# This shows how an agent can use tools to delegate to specialized subagents.
28+
def new_writer_agent() -> Agent:
29+
return Agent(
30+
name="FinancialWriterAgent",
31+
instructions=WRITER_PROMPT,
32+
model="gpt-4.1-2025-04-14",
33+
output_type=FinancialReportData,
34+
)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from collections.abc import Sequence
5+
6+
from agents import RunConfig, Runner, RunResult, custom_span, trace
7+
from temporalio import workflow
8+
9+
from openai_agents.financial_research_agent.agents.financials_agent import (
10+
new_financials_agent,
11+
)
12+
from openai_agents.financial_research_agent.agents.planner_agent import (
13+
FinancialSearchItem,
14+
FinancialSearchPlan,
15+
new_planner_agent,
16+
)
17+
from openai_agents.financial_research_agent.agents.risk_agent import new_risk_agent
18+
from openai_agents.financial_research_agent.agents.search_agent import new_search_agent
19+
from openai_agents.financial_research_agent.agents.verifier_agent import (
20+
VerificationResult,
21+
new_verifier_agent,
22+
)
23+
from openai_agents.financial_research_agent.agents.writer_agent import (
24+
FinancialReportData,
25+
new_writer_agent,
26+
)
27+
28+
29+
async def _summary_extractor(run_result: RunResult) -> str:
30+
"""Custom output extractor for sub-agents that return an AnalysisSummary."""
31+
# The financial/risk analyst agents emit an AnalysisSummary with a `summary` field.
32+
# We want the tool call to return just that summary text so the writer can drop it inline.
33+
return str(run_result.final_output.summary)
34+
35+
36+
class FinancialResearchManager:
37+
"""
38+
Orchestrates the full flow: planning, searching, sub-analysis, writing, and verification.
39+
"""
40+
41+
def __init__(self) -> None:
42+
self.run_config = RunConfig()
43+
self.planner_agent = new_planner_agent()
44+
self.search_agent = new_search_agent()
45+
self.financials_agent = new_financials_agent()
46+
self.risk_agent = new_risk_agent()
47+
self.writer_agent = new_writer_agent()
48+
self.verifier_agent = new_verifier_agent()
49+
50+
async def run(self, query: str) -> str:
51+
with trace("Financial research trace"):
52+
search_plan = await self._plan_searches(query)
53+
search_results = await self._perform_searches(search_plan)
54+
report = await self._write_report(query, search_results)
55+
verification = await self._verify_report(report)
56+
57+
# Return formatted output
58+
result = f"""=====REPORT=====
59+
60+
{report.markdown_report}
61+
62+
=====FOLLOW UP QUESTIONS=====
63+
64+
{chr(10).join(report.follow_up_questions)}
65+
66+
=====VERIFICATION=====
67+
68+
Verified: {verification.verified}
69+
Issues: {verification.issues}"""
70+
71+
return result
72+
73+
async def _plan_searches(self, query: str) -> FinancialSearchPlan:
74+
result = await Runner.run(
75+
self.planner_agent,
76+
f"Query: {query}",
77+
run_config=self.run_config,
78+
)
79+
return result.final_output_as(FinancialSearchPlan)
80+
81+
async def _perform_searches(
82+
self, search_plan: FinancialSearchPlan
83+
) -> Sequence[str]:
84+
with custom_span("Search the web"):
85+
tasks = [
86+
asyncio.create_task(self._search(item)) for item in search_plan.searches
87+
]
88+
results: list[str] = []
89+
for task in workflow.as_completed(tasks):
90+
result = await task
91+
if result is not None:
92+
results.append(result)
93+
return results
94+
95+
async def _search(self, item: FinancialSearchItem) -> str | None:
96+
input_data = f"Search term: {item.query}\nReason: {item.reason}"
97+
try:
98+
result = await Runner.run(
99+
self.search_agent,
100+
input_data,
101+
run_config=self.run_config,
102+
)
103+
return str(result.final_output)
104+
except Exception:
105+
return None
106+
107+
async def _write_report(
108+
self, query: str, search_results: Sequence[str]
109+
) -> FinancialReportData:
110+
# Expose the specialist analysts as tools so the writer can invoke them inline
111+
# and still produce the final FinancialReportData output.
112+
fundamentals_tool = self.financials_agent.as_tool(
113+
tool_name="fundamentals_analysis",
114+
tool_description="Use to get a short write-up of key financial metrics",
115+
custom_output_extractor=_summary_extractor,
116+
)
117+
risk_tool = self.risk_agent.as_tool(
118+
tool_name="risk_analysis",
119+
tool_description="Use to get a short write-up of potential red flags",
120+
custom_output_extractor=_summary_extractor,
121+
)
122+
writer_with_tools = self.writer_agent.clone(
123+
tools=[fundamentals_tool, risk_tool]
124+
)
125+
126+
input_data = (
127+
f"Original query: {query}\nSummarized search results: {search_results}"
128+
)
129+
result = await Runner.run(
130+
writer_with_tools,
131+
input_data,
132+
run_config=self.run_config,
133+
)
134+
return result.final_output_as(FinancialReportData)
135+
136+
async def _verify_report(self, report: FinancialReportData) -> VerificationResult:
137+
result = await Runner.run(
138+
self.verifier_agent,
139+
report.markdown_report,
140+
run_config=self.run_config,
141+
)
142+
return result.final_output_as(VerificationResult)

0 commit comments

Comments
 (0)