Aleksey Matsarski commited on
Commit
fd4627c
·
1 Parent(s): 5a0e4ad

add Notebook with report

Browse files
.ipynb_checkpoints/Multi-Agent_Financial_Analysis_System-checkpoint.ipynb ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [],
3
+ "metadata": {},
4
+ "nbformat": 4,
5
+ "nbformat_minor": 5
6
+ }
Multi-Agent_Financial_Analysis_System.ipynb ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "attachments": {},
5
+ "cell_type": "markdown",
6
+ "id": "9691b63b-1d38-4218-a2f5-aa1c67c43887",
7
+ "metadata": {},
8
+ "source": [
9
+ "# Multi-Agent Financial Analysis System\n",
10
+ "### A Collaborative Agentic AI for Market Insight Generation"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "id": "6d64badb-1b22-43a5-a680-d960c4c7b78e",
16
+ "metadata": {},
17
+ "source": [
18
+ "## 1. Overview"
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "markdown",
23
+ "id": "de32ae6a-90c2-42c7-9ed6-712c37b3f11c",
24
+ "metadata": {},
25
+ "source": [
26
+ "This notebook implements a Multi-Agent Financial Analysis System powered by agentic AI. It orchestrates specialized LLM agents to analyze market news, earnings reports, and stock data, producing structured, explainable investment insights.\n",
27
+ "Unlike traditional single-pipeline systems, this framework enables reasoning, task routing, self-critique, and iterative refinement — mirroring professional financial research workflows."
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "markdown",
32
+ "id": "fa74af42-3265-46f1-82b9-7524e6290eb3",
33
+ "metadata": {},
34
+ "source": [
35
+ "## 2. LLM Initialization"
36
+ ]
37
+ },
38
+ {
39
+ "cell_type": "markdown",
40
+ "id": "410834a2-5b5c-404d-8592-5499a73d473f",
41
+ "metadata": {},
42
+ "source": [
43
+ "Initializes the OpenAI-compatible ChatOpenAI model and environment configuration.\n",
44
+ "This step defines model parameters (e.g., temperature, model name) and API keys. It ensures reproducibility and sets up the base reasoning component shared by all agents."
45
+ ]
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "execution_count": null,
50
+ "id": "8bad6079-aa26-4c61-a43e-bf2897ab51ed",
51
+ "metadata": {},
52
+ "outputs": [],
53
+ "source": [
54
+ "import os\n",
55
+ "from langchain_openai import ChatOpenAI\n",
56
+ "\n",
57
+ "def init_main_model(llm_model_name: str):\n",
58
+ " openai_api_key = os.getenv(\"openai_api_key\")\n",
59
+ " llm = ChatOpenAI(api_key=openai_api_key, model=llm_model_name, temperature=0)\n",
60
+ "\n",
61
+ " return llm"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "markdown",
66
+ "id": "1449d0e8-0ffd-4f07-b721-08695d31f52e",
67
+ "metadata": {},
68
+ "source": [
69
+ "## 3. Agents Overview"
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "markdown",
74
+ "id": "3afebe1d-ab88-4430-8c4a-d752a542b74d",
75
+ "metadata": {},
76
+ "source": [
77
+ "### 3.1 News Agent"
78
+ ]
79
+ },
80
+ {
81
+ "cell_type": "markdown",
82
+ "id": "81ab4759-6c93-414b-b120-5cefa08bd5bb",
83
+ "metadata": {},
84
+ "source": [
85
+ "<b>Focuses on market-moving catalysts.</b>\n",
86
+ "\n",
87
+ "- Integrates web search (via DuckDuckGo or custom search_news_tool) to retrieve current headlines.\n",
88
+ "\n",
89
+ "- Summarizes sentiment and relevance using an LLM prompt template.\n",
90
+ "\n",
91
+ "- Outputs structured findings that downstream agents can interpret.\n",
92
+ "\n",
93
+ "<b>Tools used:</b>\n",
94
+ "\n",
95
+ "- duckduckgo_search for headline discovery\n",
96
+ "\n",
97
+ "- langchain.tools for tool registration\n",
98
+ "\n",
99
+ "- ChatPromptTemplate for templated prompts"
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "markdown",
104
+ "id": "7d8be7ab-f439-4734-90b9-05fec56a84b7",
105
+ "metadata": {},
106
+ "source": [
107
+ "#### News Agent Tools"
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "code",
112
+ "execution_count": null,
113
+ "id": "81c3d2f7-679c-4862-93bf-1efb2ed9122f",
114
+ "metadata": {},
115
+ "outputs": [],
116
+ "source": [
117
+ "from langchain.tools import tool\n",
118
+ "try:\n",
119
+ " from duckduckgo_search import DDGS\n",
120
+ "except Exception:\n",
121
+ " DDGS = None\n",
122
+ "\n",
123
+ "@tool(\"search_news\", return_direct=False)\n",
124
+ "def search_news_tool(query: str, max_results: int = 5) -> str:\n",
125
+ " \"\"\"\n",
126
+ " Search latest headlines & snippets relevant to a stock or topic.\n",
127
+ " Uses duckduckgo_search as a simple public news proxy.\n",
128
+ " Returns a concise, newline-separated list of 'title — url'.\n",
129
+ " \"\"\"\n",
130
+ " if DDGS is None:\n",
131
+ " return (\"duckduckgo_search not installed. \"\n",
132
+ " \"Install with `pip install duckduckgo-search` \"\n",
133
+ " \"or replace this tool with your news API.\")\n",
134
+ " items = []\n",
135
+ " with DDGS() as ddgs:\n",
136
+ " for r in ddgs.news(query, timelimit=\"7d\", max_results=max_results):\n",
137
+ " title = r.get(\"title\", \"\")[:160]\n",
138
+ " url = r.get(\"url\", \"\")\n",
139
+ " if title and url:\n",
140
+ " items.append(f\"{title} — {url}\")\n",
141
+ " if not items:\n",
142
+ " return \"No recent news found.\"\n",
143
+ " return \"\\n\".join(items)"
144
+ ]
145
+ },
146
+ {
147
+ "cell_type": "markdown",
148
+ "id": "44a7b828-de97-4fbd-a24b-6a7c8fad6e5b",
149
+ "metadata": {},
150
+ "source": [
151
+ "#### News Agent"
152
+ ]
153
+ },
154
+ {
155
+ "cell_type": "code",
156
+ "execution_count": null,
157
+ "id": "7a262a48-05f4-4fa1-b716-1f7d137e0f76",
158
+ "metadata": {},
159
+ "outputs": [],
160
+ "source": [
161
+ "from langchain.agents import AgentExecutor, create_tool_calling_agent\n",
162
+ "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
163
+ "from agents.news_agent.tools import search_news_tool\n",
164
+ "\n",
165
+ "news_agent_system = (\n",
166
+ " \"You are a News Analyst. Use the search tool to gather 5-8 recent, credible items.\"\n",
167
+ " \"Synthesize themes, risks, catalysts, and sentiment for investors. Output a concise\"\n",
168
+ " \"markdown summary with bullet points and 1-2 short citations (URLs).\"\n",
169
+ ")\n",
170
+ "\n",
171
+ "def create_news_agent(model) -> AgentExecutor:\n",
172
+ "\n",
173
+ " prompt = ChatPromptTemplate.from_messages(\n",
174
+ " [\n",
175
+ " (\"system\", news_agent_system),\n",
176
+ " (\"human\", \"{input}\"),\n",
177
+ " MessagesPlaceholder(\"agent_scratchpad\"),\n",
178
+ " ]\n",
179
+ " )\n",
180
+ " agent = create_tool_calling_agent(llm=model, tools=[search_news_tool], prompt=prompt)\n",
181
+ "\n",
182
+ " return AgentExecutor(agent=agent, tools=[search_news_tool], verbose=False, handle_parsing_errors=True)"
183
+ ]
184
+ },
185
+ {
186
+ "cell_type": "markdown",
187
+ "id": "7fc6b0f0-3bd7-4e3a-9f2d-30955d7ef27f",
188
+ "metadata": {},
189
+ "source": [
190
+ "### 3.2 Earnings Agent"
191
+ ]
192
+ },
193
+ {
194
+ "cell_type": "markdown",
195
+ "id": "d15a8b60-09cd-428e-a4e3-4606e30386dd",
196
+ "metadata": {},
197
+ "source": [
198
+ "<b>Analyzes corporate earnings reports and financial statements.</b>\n",
199
+ "\n",
200
+ "- Parses key performance indicators (revenue, EPS, margins).\n",
201
+ "\n",
202
+ "- Compares against consensus estimates.\n",
203
+ "\n",
204
+ "- Produces a concise summary and sentiment classification (positive/neutral/negative).\n",
205
+ "\n",
206
+ "Uses similar modular design — each step encapsulated in a callable tool or agent chain."
207
+ ]
208
+ },
209
+ {
210
+ "cell_type": "markdown",
211
+ "id": "529197ba-1e17-4b74-9a99-c4718ad25ee9",
212
+ "metadata": {},
213
+ "source": [
214
+ "#### Earnings Agent Tools"
215
+ ]
216
+ },
217
+ {
218
+ "cell_type": "code",
219
+ "execution_count": null,
220
+ "id": "12b1149d-4c61-46f5-85a9-02920ed9e148",
221
+ "metadata": {},
222
+ "outputs": [],
223
+ "source": [
224
+ "from langchain.tools import tool\n",
225
+ "import yfinance as yf\n",
226
+ "import datetime as dt\n",
227
+ "\n",
228
+ "@tool(\"fetch_earnings\", return_direct=False)\n",
229
+ "def fetch_earnings_tool(ticker: str) -> str:\n",
230
+ " \"\"\"\n",
231
+ " Fetch upcoming and recent earnings info via yfinance.\n",
232
+ " Returns a concise summary (dates + surprises if available).\n",
233
+ " \"\"\"\n",
234
+ " tk = yf.Ticker(ticker)\n",
235
+ " lines = [f\"EARNINGS SNAPSHOT for {ticker.upper()}\"]\n",
236
+ "\n",
237
+ " # Upcoming earnings (earnings_dates includes future dates)\n",
238
+ " try:\n",
239
+ " ed = tk.earnings_dates # DataFrame if available\n",
240
+ " if ed is not None and not ed.empty:\n",
241
+ " # Take the next upcoming date and last reported\n",
242
+ " ed_sorted = ed.sort_index()\n",
243
+ " upcoming = ed_sorted[ed_sorted.index >= dt.datetime.now().date()]\n",
244
+ " last = ed_sorted[ed_sorted.index < dt.datetime.now().date()]\n",
245
+ " if not upcoming.empty:\n",
246
+ " lines.append(f\"Upcoming: {upcoming.index[0].strftime('%Y-%m-%d')}\")\n",
247
+ " if not last.empty:\n",
248
+ " # Try EPS surprise columns if present\n",
249
+ " row = last.iloc[-1]\n",
250
+ " surprise = None\n",
251
+ " for k in [\"EPS Surprise %\", \"Surprise(%)\", \"epssurprisepct\", \"epssurprisepercent\"]:\n",
252
+ " if k in row and row[k] is not None:\n",
253
+ " surprise = row[k]\n",
254
+ " break\n",
255
+ " lines.append(\n",
256
+ " f\"Last reported: {last.index[-1].strftime('%Y-%m-%d')}\"\n",
257
+ " + (f\", EPS surprise: {surprise}\" if surprise is not None else \"\")\n",
258
+ " )\n",
259
+ " else:\n",
260
+ " lines.append(\"No earnings_dates available.\")\n",
261
+ " except Exception as e:\n",
262
+ " lines.append(f\"earnings_dates unavailable: {e}\")\n",
263
+ "\n",
264
+ " # Quarterly financials (very high-level)\n",
265
+ " try:\n",
266
+ " qf = tk.quarterly_financials\n",
267
+ " if qf is not None and not qf.empty:\n",
268
+ " cols = list(qf.columns)\n",
269
+ " if cols:\n",
270
+ " last_q = cols[0]\n",
271
+ " revenue = qf.loc[\"Total Revenue\", last_q] if \"Total Revenue\" in qf.index else None\n",
272
+ " gross_profit = qf.loc[\"Gross Profit\", last_q] if \"Gross Profit\" in qf.index else None\n",
273
+ " lines.append(f\"Last quarter ({last_q.date()}): Revenue={revenue}, GrossProfit={gross_profit}\")\n",
274
+ " except Exception:\n",
275
+ " pass\n",
276
+ "\n",
277
+ " return \"\\n\".join(lines)"
278
+ ]
279
+ },
280
+ {
281
+ "cell_type": "markdown",
282
+ "id": "89951220-c05e-4374-9bc8-f640d29e2fc0",
283
+ "metadata": {},
284
+ "source": [
285
+ "#### Earnings Agent"
286
+ ]
287
+ },
288
+ {
289
+ "cell_type": "code",
290
+ "execution_count": null,
291
+ "id": "9bf7d226-2885-42e1-bd4a-064192ba6513",
292
+ "metadata": {},
293
+ "outputs": [],
294
+ "source": [
295
+ "from langchain.agents import AgentExecutor, create_tool_calling_agent\n",
296
+ "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
297
+ "from agents.earnings_agent.tools import fetch_earnings_tool\n",
298
+ "\n",
299
+ "earnings_agent_system = (\n",
300
+ " \"You are an Earnings Analyst. Use the earnings tool to summarize the latest and upcoming\"\n",
301
+ " \"earnings information (dates, surprises if available) and key line items. Provide a \"\n",
302
+ " \"short view on momentum and watchouts. Output concise markdown.\"\n",
303
+ ")\n",
304
+ "\n",
305
+ "def create_earnings_agent(model) -> AgentExecutor:\n",
306
+ "\n",
307
+ " prompt = ChatPromptTemplate.from_messages(\n",
308
+ " [\n",
309
+ " (\"system\", earnings_agent_system),\n",
310
+ " (\"human\", \"{input}\"),\n",
311
+ " MessagesPlaceholder(\"agent_scratchpad\"),\n",
312
+ " ]\n",
313
+ " )\n",
314
+ " agent = create_tool_calling_agent(llm=model, tools=[fetch_earnings_tool], prompt=prompt)\n",
315
+ "\n",
316
+ " return AgentExecutor(agent=agent, tools=[fetch_earnings_tool], verbose=False, handle_parsing_errors=True)"
317
+ ]
318
+ },
319
+ {
320
+ "cell_type": "markdown",
321
+ "id": "092e5091-ae1b-44f7-88f4-600499f87b14",
322
+ "metadata": {},
323
+ "source": [
324
+ "### 3.3 Market Agent"
325
+ ]
326
+ },
327
+ {
328
+ "cell_type": "markdown",
329
+ "id": "1fb285c6-6b32-4b2e-a7d1-b20a8f4ce7d6",
330
+ "metadata": {},
331
+ "source": [
332
+ "Integrates quantitative signals (stock trends, volatility, RSI, etc.) with textual insights from the other agents.\n",
333
+ "Performs reasoning over structured market data and qualitative narratives to identify actionable opportunities."
334
+ ]
335
+ },
336
+ {
337
+ "cell_type": "markdown",
338
+ "id": "61170e19-91b1-480c-900b-45dbb009e52a",
339
+ "metadata": {},
340
+ "source": [
341
+ "#### Market Agent Tools"
342
+ ]
343
+ },
344
+ {
345
+ "cell_type": "code",
346
+ "execution_count": null,
347
+ "id": "1f9aef88-159c-4804-a537-d7309cd06fcc",
348
+ "metadata": {},
349
+ "outputs": [],
350
+ "source": [
351
+ "from langchain.tools import tool\n",
352
+ "import yfinance as yf\n",
353
+ "import datetime as dt\n",
354
+ "\n",
355
+ "@tool(\"fetch_market_snapshot\", return_direct=False)\n",
356
+ "def fetch_market_snapshot_tool(ticker: str) -> str:\n",
357
+ " \"\"\"\n",
358
+ " Pulls basic market snapshot with yfinance: price, change, volume, valuation.\n",
359
+ " Returns a compact textual snapshot.\n",
360
+ " \"\"\"\n",
361
+ " tk = yf.Ticker(ticker)\n",
362
+ " info = {}\n",
363
+ " try:\n",
364
+ " price = tk.fast_info.last_price\n",
365
+ " prev_close = tk.fast_info.previous_close\n",
366
+ " change = None\n",
367
+ " if price is not None and prev_close:\n",
368
+ " change = (price - prev_close) / prev_close * 100\n",
369
+ " info.update({\n",
370
+ " \"price\": price, \"prev_close\": prev_close, \"pct_change\": change,\n",
371
+ " \"market_cap\": tk.fast_info.market_cap, \"volume\": tk.fast_info.last_volume,\n",
372
+ " \"currency\": tk.fast_info.currency\n",
373
+ " })\n",
374
+ " except Exception as e:\n",
375
+ " return f\"Market snapshot failed: {e}\"\n",
376
+ "\n",
377
+ " lines = [f\"MARKET SNAPSHOT for {ticker.upper()}\"]\n",
378
+ " lines.append(f\"Price: {info.get('price')} {info.get('currency')}\")\n",
379
+ " if info.get(\"pct_change\") is not None:\n",
380
+ " lines.append(f\"Day change: {info['pct_change']:.2f}%\")\n",
381
+ " lines.append(f\"Market Cap: {info.get('market_cap')}\")\n",
382
+ " lines.append(f\"Volume: {info.get('volume')}\")\n",
383
+ " return \"\\n\".join(lines)"
384
+ ]
385
+ },
386
+ {
387
+ "cell_type": "markdown",
388
+ "id": "b97c1c85-036a-4298-a602-469c0b5bae86",
389
+ "metadata": {},
390
+ "source": [
391
+ "#### Market Agent"
392
+ ]
393
+ },
394
+ {
395
+ "cell_type": "code",
396
+ "execution_count": null,
397
+ "id": "242fb17f-be7c-4e19-be55-8c56007fd358",
398
+ "metadata": {},
399
+ "outputs": [],
400
+ "source": [
401
+ "from langchain.agents import AgentExecutor, create_tool_calling_agent\n",
402
+ "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
403
+ "from agents.market_agent.tools import fetch_market_snapshot_tool\n",
404
+ "from pathlib import Path\n",
405
+ "import yaml\n",
406
+ "\n",
407
+ "market_agent_system = (\n",
408
+ " \"You are a Market & Valuation Analyst. Use the market snapshot tool to extract current\" \n",
409
+ " \"trading context and discuss short-term technicals/flow and high-level valuation notes.\" \n",
410
+ " \"Output concise markdown.\"\n",
411
+ ")\n",
412
+ "\n",
413
+ "\n",
414
+ "def create_market_agent(model) -> AgentExecutor:\n",
415
+ "\n",
416
+ " prompt = ChatPromptTemplate.from_messages(\n",
417
+ " [\n",
418
+ " (\"system\", market_agent_system),\n",
419
+ " (\"human\", \"{input}\"),\n",
420
+ " MessagesPlaceholder(\"agent_scratchpad\"),\n",
421
+ " ]\n",
422
+ " )\n",
423
+ " agent = create_tool_calling_agent(llm=model, tools=[fetch_market_snapshot_tool], prompt=prompt)\n",
424
+ "\n",
425
+ " return AgentExecutor(agent=agent, tools=[fetch_market_snapshot_tool], verbose=False, handle_parsing_errors=True)"
426
+ ]
427
+ },
428
+ {
429
+ "cell_type": "markdown",
430
+ "id": "4373c80a-c833-4dd8-8083-ef69a4edf871",
431
+ "metadata": {},
432
+ "source": [
433
+ "## 4. Workflow"
434
+ ]
435
+ },
436
+ {
437
+ "cell_type": "markdown",
438
+ "id": "edebefcd-0aa9-435d-874c-2eb04c296468",
439
+ "metadata": {},
440
+ "source": [
441
+ "This section defines the coordination logic and workflow orchestration:\n",
442
+ "\n",
443
+ "- Supervisor Agent routes queries to relevant specialists (news, earnings, or market).\n",
444
+ "\n",
445
+ "- Tool execution is dynamically selected based on the input context.\n",
446
+ "\n",
447
+ "- Memory and reflection allow the system to carry forward past reasoning chains.\n",
448
+ "\n",
449
+ "- Prompts are defined using ChatPromptTemplate, and YAML configuration supports modular editing."
450
+ ]
451
+ },
452
+ {
453
+ "cell_type": "markdown",
454
+ "id": "ab795dd3-686e-412d-9257-5ba2e3200d21",
455
+ "metadata": {},
456
+ "source": [
457
+ "### 4.1 Graph Nodes"
458
+ ]
459
+ },
460
+ {
461
+ "cell_type": "code",
462
+ "execution_count": null,
463
+ "id": "206e9a1d-f2da-4af4-a39e-d6dc76ba2524",
464
+ "metadata": {},
465
+ "outputs": [],
466
+ "source": [
467
+ "from langchain.agents import AgentExecutor\n",
468
+ "from workflow.graph_state import GraphState\n",
469
+ "\n",
470
+ "news_user_prompt = \"Research recent news for {ticker}. Focus on price-moving catalysts.\"\n",
471
+ "earnings_user_prompt = \"Analyze earnings for {ticker}. Summarize last and upcoming earnings. Use the tool.\"\n",
472
+ "market_user_prompt = \"Provide a market snapshot for {ticker}. Use the tool.\"\n",
473
+ "\n",
474
+ "AGENTS = [\"news\", \"earnings\", \"market\"]\n",
475
+ "\n",
476
+ "def news_node(state: GraphState, agent: AgentExecutor) -> GraphState:\n",
477
+ " ticker = state[\"ticker\"]\n",
478
+ " query = news_user_prompt.format(ticker=ticker)\n",
479
+ " res = agent.invoke({\"input\": query})\n",
480
+ " state[\"news_summary\"] = res[\"output\"]\n",
481
+ " state[\"completed\"] = list(set(state[\"completed\"] + [\"news\"]))\n",
482
+ " return state\n",
483
+ "\n",
484
+ "\n",
485
+ "def earnings_node(state: GraphState, agent: AgentExecutor) -> GraphState:\n",
486
+ " ticker = state[\"ticker\"]\n",
487
+ " query = earnings_user_prompt.format(ticker=ticker)\n",
488
+ " res = agent.invoke({\"input\": query})\n",
489
+ " state[\"earnings_summary\"] = res[\"output\"]\n",
490
+ " state[\"completed\"] = list(set(state[\"completed\"] + [\"earnings\"]))\n",
491
+ " return state\n",
492
+ "\n",
493
+ "\n",
494
+ "def market_node(state: GraphState, agent: AgentExecutor) -> GraphState:\n",
495
+ " ticker = state[\"ticker\"]\n",
496
+ " query = market_user_prompt.format(ticker=ticker)\n",
497
+ " res = agent.invoke({\"input\": query})\n",
498
+ " state[\"market_summary\"] = res[\"output\"]\n",
499
+ " state[\"completed\"] = list(set(state[\"completed\"] + [\"market\"]))\n",
500
+ " return state\n",
501
+ "\n",
502
+ "\n",
503
+ "def synth_node(state: GraphState, synthesizer_chain) -> GraphState:\n",
504
+ " out = synthesizer_chain.invoke(\n",
505
+ " {\n",
506
+ " \"ticker\": state[\"ticker\"],\n",
507
+ " \"news_summary\": state.get(\"news_summary\", \"\"),\n",
508
+ " \"earnings_summary\": state.get(\"earnings_summary\", \"\"),\n",
509
+ " \"market_summary\": state.get(\"market_summary\", \"\"),\n",
510
+ " }\n",
511
+ " )\n",
512
+ " state[\"final_recommendation\"] = out.content if hasattr(out, \"content\") else str(out)\n",
513
+ " return state\n",
514
+ " \n",
515
+ "def supervisor_node(state: GraphState) -> GraphState:\n",
516
+ " # Do any bookkeeping here if needed; otherwise just pass state through\n",
517
+ " return state\n",
518
+ "\n",
519
+ "def supervisor_router(state: GraphState) -> str:\n",
520
+ " remaining = [a for a in AGENTS if a not in state.get(\"completed\", [])]\n",
521
+ " return remaining[0] if remaining else \"synth\""
522
+ ]
523
+ },
524
+ {
525
+ "cell_type": "markdown",
526
+ "id": "f27d4f01-74bc-4fa3-888f-98947c11d282",
527
+ "metadata": {},
528
+ "source": [
529
+ "### 4.2 Graph State"
530
+ ]
531
+ },
532
+ {
533
+ "cell_type": "code",
534
+ "execution_count": null,
535
+ "id": "8d9787f0-27d5-4b48-9a0f-29e72da51855",
536
+ "metadata": {},
537
+ "outputs": [],
538
+ "source": [
539
+ "from typing import TypedDict, List, Optional, Dict, Any\n",
540
+ "\n",
541
+ "class GraphState(TypedDict):\n",
542
+ " ticker: str\n",
543
+ " query: str # general query / task\n",
544
+ " # outputs collected from agents\n",
545
+ " news_summary: Optional[str]\n",
546
+ " earnings_summary: Optional[str]\n",
547
+ " market_summary: Optional[str]\n",
548
+ " # bookkeeping\n",
549
+ " completed: List[str]\n",
550
+ " # final\n",
551
+ " final_recommendation: Optional[str]"
552
+ ]
553
+ },
554
+ {
555
+ "cell_type": "markdown",
556
+ "id": "9ab7d2ec-fa8c-4388-a102-8d2dad8cfe31",
557
+ "metadata": {},
558
+ "source": [
559
+ "### 4.3 Agents Workflow"
560
+ ]
561
+ },
562
+ {
563
+ "cell_type": "code",
564
+ "execution_count": null,
565
+ "id": "c8413fbc-749c-442e-bc23-0b7c2cb54647",
566
+ "metadata": {},
567
+ "outputs": [],
568
+ "source": [
569
+ "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
570
+ "import yaml\n",
571
+ "from langgraph.graph import StateGraph, END\n",
572
+ "\n",
573
+ "from agents.earnings_agent.earnings_agent import create_earnings_agent\n",
574
+ "from agents.market_agent.market_agent import create_market_agent\n",
575
+ "from agents.news_agent.news_agent import create_news_agent\n",
576
+ "from model.init_model import init_main_model\n",
577
+ "from workflow.graph_state import GraphState\n",
578
+ "from workflow.nodes.nodes import news_node, earnings_node, market_node, synth_node, supervisor_node, AGENTS, supervisor_router\n",
579
+ "from pathlib import Path\n",
580
+ "\n",
581
+ "system_synthesizer = (\n",
582
+ " \"You are the Lead Portfolio Analyst. Merge inputs from News, Earnings, and Market agents.\" \n",
583
+ " \"Produce a final, actionable recommendation block (Buy/Hold/Sell with confidence 0-1),\" \n",
584
+ " \"key drivers (bull/bear), near-term catalysts, and 2-3 risks. Be concise and concrete.\"\n",
585
+ ")\n",
586
+ "\n",
587
+ "human_synthesizer = (\n",
588
+ " \"Ticker: {ticker}\\n\\n\"\n",
589
+ " \"### News Summary\\n{news_summary}\\n\\n\"\n",
590
+ " \"### Earnings Summary\\n{earnings_summary}\\n\\n\"\n",
591
+ " \"### Market Summary\\n{market_summary}\\n\\n\"\n",
592
+ " \"Write the final recommendation now.\"\n",
593
+ ")\n",
594
+ "\n",
595
+ "def make_synthesizer(model):\n",
596
+ " \"\"\"Final writer to merge all agent outputs into actionable recommendations.\"\"\"\n",
597
+ " template = ChatPromptTemplate.from_messages(\n",
598
+ " [\n",
599
+ " (\"system\", system_synthesizer),\n",
600
+ " (\"human\", human_synthesizer)\n",
601
+ " ]\n",
602
+ " )\n",
603
+ " return template | model # LC chain: Prompt -> LLM\n",
604
+ "\n",
605
+ "def build_agents_workflow(llm_model_name):\n",
606
+ " # --- Base LLM for agents & synthesizer, we can initiate different models for agents here ---\n",
607
+ " model = init_main_model(llm_model_name)\n",
608
+ "\n",
609
+ " # --- Create specialized agents ---\n",
610
+ " news_agent = create_news_agent(model)\n",
611
+ " earnings_agent = create_earnings_agent(model)\n",
612
+ " market_agent = create_market_agent(model)\n",
613
+ "\n",
614
+ " # --- Create synthesizer chain ---\n",
615
+ " synthesizer = make_synthesizer(model)\n",
616
+ "\n",
617
+ " # --- LangGraph: wire nodes ---\n",
618
+ " g = StateGraph(GraphState)\n",
619
+ "\n",
620
+ " # Bind node callables with their dependencies via closures\n",
621
+ " g.add_node(\"news\", lambda s: news_node(s, news_agent))\n",
622
+ " g.add_node(\"earnings\", lambda s: earnings_node(s, earnings_agent))\n",
623
+ " g.add_node(\"market\", lambda s: market_node(s, market_agent))\n",
624
+ " g.add_node(\"synth\", lambda s: synth_node(s, synthesizer))\n",
625
+ "\n",
626
+ " # Supervisor node\n",
627
+ " g.add_node(\"supervisor\", supervisor_node)\n",
628
+ " # Edges: start -> supervisor -> (news|earnings|market|synth) -> supervisor ... -> synth -> END\n",
629
+ " g.set_entry_point(\"supervisor\")\n",
630
+ "\n",
631
+ " for a in AGENTS:\n",
632
+ " g.add_edge(a, \"supervisor\")\n",
633
+ " g.add_edge(\"synth\", END)\n",
634
+ "\n",
635
+ " # Route decisions come from the router function (returns a string)\n",
636
+ " g.add_conditional_edges(\n",
637
+ " \"supervisor\",\n",
638
+ " supervisor_router, # returns: \"news\" | \"earnings\" | \"market\" | \"synth\"\n",
639
+ " {\n",
640
+ " \"news\": \"news\",\n",
641
+ " \"earnings\": \"earnings\",\n",
642
+ " \"market\": \"market\",\n",
643
+ " \"synth\": \"synth\",\n",
644
+ " },\n",
645
+ " )\n",
646
+ "\n",
647
+ " return g.compile()"
648
+ ]
649
+ },
650
+ {
651
+ "cell_type": "markdown",
652
+ "id": "bcc79b8d-cd71-405f-b4f0-bd6656e4931c",
653
+ "metadata": {},
654
+ "source": [
655
+ "## 5 Run"
656
+ ]
657
+ },
658
+ {
659
+ "cell_type": "code",
660
+ "execution_count": null,
661
+ "id": "41288526-ba4d-4980-b847-791ea5c119d8",
662
+ "metadata": {},
663
+ "outputs": [],
664
+ "source": [
665
+ "from workflow.agents_workflow import build_agents_workflow\n",
666
+ "from workflow.graph_state import GraphState\n",
667
+ "\n",
668
+ "# Run locally without gradio\n",
669
+ "\n",
670
+ "app = build_agents_workflow(llm_model_name=\"gpt-4o-mini\")\n",
671
+ "\n",
672
+ "def run_user_query(ticker):\n",
673
+ " QUERY = f\" Produce investor-ready insights for {ticker}.\"\n",
674
+ " init_state: GraphState = {\n",
675
+ " \"ticker\": ticker,\n",
676
+ " \"query\": QUERY,\n",
677
+ " \"news_summary\": None,\n",
678
+ " \"earnings_summary\": None,\n",
679
+ " \"market_summary\": None,\n",
680
+ " \"completed\": [],\n",
681
+ " \"final_recommendation\": None,\n",
682
+ " }\n",
683
+ " final_state = app.invoke(init_state)\n",
684
+ "\n",
685
+ " return final_state\n",
686
+ "\n",
687
+ "state = run_user_query(\"AAPL\")\n",
688
+ "\n",
689
+ "print(\"\\n\" + \"=\" * 80)\n",
690
+ "print(f\"### NEWS SUMMARY\\n{state['news_summary']}\\n\")\n",
691
+ "print(f\"### EARNINGS SUMMARY\\n{state['earnings_summary']}\\n\")\n",
692
+ "print(f\"### MARKET SUMMARY\\n{state['market_summary']}\\n\")\n",
693
+ "print(f\"### FINAL RECOMMENDATION\\n{state['final_recommendation']}\\n\")"
694
+ ]
695
+ },
696
+ {
697
+ "cell_type": "markdown",
698
+ "id": "fc07563c-91af-4d9d-b8dc-c920e41f163a",
699
+ "metadata": {},
700
+ "source": [
701
+ "## 6. System Architecture Summary"
702
+ ]
703
+ },
704
+ {
705
+ "cell_type": "markdown",
706
+ "id": "478cd45e-f264-4b05-8dac-5716f95880d1",
707
+ "metadata": {},
708
+ "source": [
709
+ "| **Component** | **Description** |\n",
710
+ "| -------------------- | ----------------------------------------------- |\n",
711
+ "| **Supervisor Agent** | Plans, routes, and consolidates results |\n",
712
+ "| **News Agent** | Gathers market news and sentiment |\n",
713
+ "| **Earnings Agent** | Extracts and interprets company earnings data |\n",
714
+ "| **Market Agent** | Analyzes stock trends and technical indicators |\n",
715
+ "| **Evaluator Agent** | Critiques and scores the overall report |\n",
716
+ "| **Memory Layer** | Maintains conversational and analytical context |\n",
717
+ "| **Iteration Loop** | Refines reasoning via feedback cycles |\n",
718
+ "\n",
719
+ "<b>Key Strengths</b>\n",
720
+ "✅ Modular YAML + LangChain integration for reusable prompt templates.\n",
721
+ "✅ Realistic workflow emulating professional equity research teams.\n",
722
+ "✅ Self-evaluating loop ensures higher factual consistency.\n",
723
+ "✅ Extensible for additional agents (e.g., ESG Analyst, Risk Scorer).\n"
724
+ ]
725
+ },
726
+ {
727
+ "cell_type": "markdown",
728
+ "id": "8de30b3f-f006-4a6a-94af-53d6d794c346",
729
+ "metadata": {},
730
+ "source": [
731
+ "## 7. Conclusion"
732
+ ]
733
+ },
734
+ {
735
+ "cell_type": "markdown",
736
+ "id": "bb1bcbc0-1e67-4477-964d-f67a0e6b2205",
737
+ "metadata": {},
738
+ "source": [
739
+ "The Multi-Agent Financial Analysis System exemplifies next-generation AI infrastructure for market intelligence — one that reasons, collaborates, and evolves.\n",
740
+ "By blending structured reasoning with self-improvement loops, it brings the intelligence of multi-analyst teams into an automated, explainable framework for financial decision-making."
741
+ ]
742
+ },
743
+ {
744
+ "cell_type": "code",
745
+ "execution_count": null,
746
+ "id": "b166440b-6bd5-405f-af9c-2af46a84b416",
747
+ "metadata": {},
748
+ "outputs": [],
749
+ "source": []
750
+ }
751
+ ],
752
+ "metadata": {
753
+ "kernelspec": {
754
+ "display_name": "Python 3 (ipykernel)",
755
+ "language": "python",
756
+ "name": "python3"
757
+ },
758
+ "language_info": {
759
+ "codemirror_mode": {
760
+ "name": "ipython",
761
+ "version": 3
762
+ },
763
+ "file_extension": ".py",
764
+ "mimetype": "text/x-python",
765
+ "name": "python",
766
+ "nbconvert_exporter": "python",
767
+ "pygments_lexer": "ipython3",
768
+ "version": "3.12.5"
769
+ }
770
+ },
771
+ "nbformat": 4,
772
+ "nbformat_minor": 5
773
+ }