Skip to content

Commit 5e4e486

Browse files
committed
Port financial research example and reasoning_content example
1 parent 396fb5c commit 5e4e486

16 files changed

+664
-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-mini",
33+
output_type=FinancialReportData,
34+
)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from collections.abc import Sequence
5+
6+
from temporalio import workflow
7+
8+
from agents import RunConfig, Runner, RunResult, custom_span, trace
9+
10+
from openai_agents.financial_research_agent.agents.financials_agent import (
11+
new_financials_agent,
12+
)
13+
from openai_agents.financial_research_agent.agents.planner_agent import (
14+
FinancialSearchItem,
15+
FinancialSearchPlan,
16+
new_planner_agent,
17+
)
18+
from openai_agents.financial_research_agent.agents.risk_agent import new_risk_agent
19+
from openai_agents.financial_research_agent.agents.search_agent import (
20+
new_search_agent,
21+
)
22+
from openai_agents.financial_research_agent.agents.verifier_agent import (
23+
VerificationResult,
24+
new_verifier_agent,
25+
)
26+
from openai_agents.financial_research_agent.agents.writer_agent import (
27+
FinancialReportData,
28+
new_writer_agent,
29+
)
30+
31+
32+
async def _summary_extractor(run_result: RunResult) -> str:
33+
"""Custom output extractor for sub-agents that return an AnalysisSummary."""
34+
# The financial/risk analyst agents emit an AnalysisSummary with a `summary` field.
35+
# We want the tool call to return just that summary text so the writer can drop it inline.
36+
return str(run_result.final_output.summary)
37+
38+
39+
class FinancialResearchManager:
40+
"""
41+
Orchestrates the full flow: planning, searching, sub-analysis, writing, and verification.
42+
"""
43+
44+
def __init__(self) -> None:
45+
self.run_config = RunConfig()
46+
self.planner_agent = new_planner_agent()
47+
self.search_agent = new_search_agent()
48+
self.financials_agent = new_financials_agent()
49+
self.risk_agent = new_risk_agent()
50+
self.writer_agent = new_writer_agent()
51+
self.verifier_agent = new_verifier_agent()
52+
53+
async def run(self, query: str) -> str:
54+
with trace("Financial research trace"):
55+
search_plan = await self._plan_searches(query)
56+
search_results = await self._perform_searches(search_plan)
57+
report = await self._write_report(query, search_results)
58+
verification = await self._verify_report(report)
59+
60+
# Return formatted output
61+
result = f"""=====REPORT=====
62+
63+
{report.markdown_report}
64+
65+
=====FOLLOW UP QUESTIONS=====
66+
67+
{chr(10).join(report.follow_up_questions)}
68+
69+
=====VERIFICATION=====
70+
71+
Verified: {verification.verified}
72+
Issues: {verification.issues}"""
73+
74+
return result
75+
76+
async def _plan_searches(self, query: str) -> FinancialSearchPlan:
77+
result = await Runner.run(
78+
self.planner_agent,
79+
f"Query: {query}",
80+
run_config=self.run_config,
81+
)
82+
return result.final_output_as(FinancialSearchPlan)
83+
84+
async def _perform_searches(
85+
self, search_plan: FinancialSearchPlan
86+
) -> Sequence[str]:
87+
with custom_span("Search the web"):
88+
tasks = [
89+
asyncio.create_task(self._search(item)) for item in search_plan.searches
90+
]
91+
results: list[str] = []
92+
for task in workflow.as_completed(tasks):
93+
result = await task
94+
if result is not None:
95+
results.append(result)
96+
return results
97+
98+
async def _search(self, item: FinancialSearchItem) -> str | None:
99+
input_data = f"Search term: {item.query}\nReason: {item.reason}"
100+
try:
101+
result = await Runner.run(
102+
self.search_agent,
103+
input_data,
104+
run_config=self.run_config,
105+
)
106+
return str(result.final_output)
107+
except Exception:
108+
return None
109+
110+
async def _write_report(
111+
self, query: str, search_results: Sequence[str]
112+
) -> FinancialReportData:
113+
# Expose the specialist analysts as tools so the writer can invoke them inline
114+
# and still produce the final FinancialReportData output.
115+
fundamentals_tool = self.financials_agent.as_tool(
116+
tool_name="fundamentals_analysis",
117+
tool_description="Use to get a short write-up of key financial metrics",
118+
custom_output_extractor=_summary_extractor,
119+
)
120+
risk_tool = self.risk_agent.as_tool(
121+
tool_name="risk_analysis",
122+
tool_description="Use to get a short write-up of potential red flags",
123+
custom_output_extractor=_summary_extractor,
124+
)
125+
writer_with_tools = self.writer_agent.clone(
126+
tools=[fundamentals_tool, risk_tool]
127+
)
128+
129+
input_data = (
130+
f"Original query: {query}\nSummarized search results: {search_results}"
131+
)
132+
result = await Runner.run(
133+
writer_with_tools,
134+
input_data,
135+
run_config=self.run_config,
136+
)
137+
return result.final_output_as(FinancialReportData)
138+
139+
async def _verify_report(self, report: FinancialReportData) -> VerificationResult:
140+
result = await Runner.run(
141+
self.verifier_agent,
142+
report.markdown_report,
143+
run_config=self.run_config,
144+
)
145+
return result.final_output_as(VerificationResult)

0 commit comments

Comments
 (0)