Aleksey Matsarski
commited on
Commit
·
a47e415
1
Parent(s):
10a5b56
Refactoring code, provide better abstraction and file structure
Browse files- .DS_Store +0 -0
- .idea/.gitignore +8 -0
- .idea/Financial_analysis_system_draft.iml +14 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +7 -0
- .idea/modules.xml +8 -0
- .idea/vcs.xml +6 -0
- agents/agent_builder.py +1 -4
- agents/build_graph.py +11 -7
- agents/earnings_agent/earnings_agent.py +22 -0
- agents/earnings_agent/prompts.yaml +4 -0
- agents/earnings_agent/tools.py +54 -0
- agents/market_agent/market_agent.py +23 -0
- agents/market_agent/prompts.yaml +4 -0
- agents/market_agent/tools.py +33 -0
- agents/news_agent/news_agent.py +22 -0
- agents/news_agent/prompts.yaml +4 -0
- agents/news_agent/tools.py +27 -0
- agents/supervisor_router.py +0 -7
- app.py +4 -4
- main.py +32 -0
- model/init_model.py +8 -0
- requirements.txt +8 -1
- tools/__init__.py +0 -0
- {agents → tools}/tools.py +0 -0
- workflow/agents_workflow.py +69 -0
- {agents → workflow}/graph_state.py +0 -0
- {agents → workflow/nodes}/nodes.py +16 -11
- workflow/nodes/prompts.yaml +3 -0
- workflow/prompts.yaml +11 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.idea/.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Default ignored files
|
| 2 |
+
/shelf/
|
| 3 |
+
/workspace.xml
|
| 4 |
+
# Editor-based HTTP Client requests
|
| 5 |
+
/httpRequests/
|
| 6 |
+
# Datasource local storage ignored files
|
| 7 |
+
/dataSources/
|
| 8 |
+
/dataSources.local.xml
|
.idea/Financial_analysis_system_draft.iml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<module type="PYTHON_MODULE" version="4">
|
| 3 |
+
<component name="NewModuleRootManager">
|
| 4 |
+
<content url="file://$MODULE_DIR$">
|
| 5 |
+
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
| 6 |
+
</content>
|
| 7 |
+
<orderEntry type="jdk" jdkName="Python 3.9 (Financial_analysis_system_draft)" jdkType="Python SDK" />
|
| 8 |
+
<orderEntry type="sourceFolder" forTests="false" />
|
| 9 |
+
</component>
|
| 10 |
+
<component name="PyDocumentationSettings">
|
| 11 |
+
<option name="format" value="PLAIN" />
|
| 12 |
+
<option name="myDocStringFormat" value="Plain" />
|
| 13 |
+
</component>
|
| 14 |
+
</module>
|
.idea/inspectionProfiles/profiles_settings.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<component name="InspectionProjectProfileManager">
|
| 2 |
+
<settings>
|
| 3 |
+
<option name="USE_PROJECT_PROFILE" value="false" />
|
| 4 |
+
<version value="1.0" />
|
| 5 |
+
</settings>
|
| 6 |
+
</component>
|
.idea/misc.xml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="Black">
|
| 4 |
+
<option name="sdkName" value="Python 3.9 (Financial_analysis_system_draft)" />
|
| 5 |
+
</component>
|
| 6 |
+
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (Financial_analysis_system_draft)" project-jdk-type="Python SDK" />
|
| 7 |
+
</project>
|
.idea/modules.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="ProjectModuleManager">
|
| 4 |
+
<modules>
|
| 5 |
+
<module fileurl="file://$PROJECT_DIR$/.idea/Financial_analysis_system_draft.iml" filepath="$PROJECT_DIR$/.idea/Financial_analysis_system_draft.iml" />
|
| 6 |
+
</modules>
|
| 7 |
+
</component>
|
| 8 |
+
</project>
|
.idea/vcs.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="VcsDirectoryMappings">
|
| 4 |
+
<mapping directory="" vcs="Git" />
|
| 5 |
+
</component>
|
| 6 |
+
</project>
|
agents/agent_builder.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
| 2 |
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 3 |
-
from
|
| 4 |
-
from langgraph.graph import StateGraph, END
|
| 5 |
-
from typing import TypedDict, List, Optional, Dict, Any
|
| 6 |
-
from agents.tools import search_news_tool, fetch_earnings_tool, fetch_earnings_tool, fetch_market_snapshot_tool
|
| 7 |
|
| 8 |
def build_agent(llm, tools, system_instructions: str) -> AgentExecutor:
|
| 9 |
"""Create a tool-calling agent with a focused system prompt."""
|
|
|
|
| 1 |
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
| 2 |
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 3 |
+
from tools.tools import search_news_tool, fetch_earnings_tool, fetch_market_snapshot_tool
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
def build_agent(llm, tools, system_instructions: str) -> AgentExecutor:
|
| 6 |
"""Create a tool-calling agent with a focused system prompt."""
|
agents/build_graph.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
import os
|
| 2 |
from langgraph.graph import StateGraph, END
|
| 3 |
-
from agents.agent_builder import
|
| 4 |
-
from agents.
|
|
|
|
| 5 |
from langchain_openai import ChatOpenAI
|
| 6 |
-
|
| 7 |
-
from agents.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def build_graph(llm_model_name: str = "gpt-4o-mini"):
|
| 10 |
|
|
@@ -13,9 +17,9 @@ def build_graph(llm_model_name: str = "gpt-4o-mini"):
|
|
| 13 |
llm = ChatOpenAI(api_key=openai_api_key,model=llm_model_name, temperature=0)
|
| 14 |
|
| 15 |
# --- Create specialized agents ---
|
| 16 |
-
news_agent =
|
| 17 |
-
earnings_agent =
|
| 18 |
-
market_agent =
|
| 19 |
|
| 20 |
# --- Create synthesizer chain ---
|
| 21 |
synthesizer = make_synthesizer(llm)
|
|
|
|
| 1 |
import os
|
| 2 |
from langgraph.graph import StateGraph, END
|
| 3 |
+
from agents.agent_builder import make_synthesizer
|
| 4 |
+
from agents.earnings_agent.earnings_agent import create_earnings_agent
|
| 5 |
+
from workflow.graph_state import GraphState
|
| 6 |
from langchain_openai import ChatOpenAI
|
| 7 |
+
|
| 8 |
+
from agents.market_agent.market_agent import create_market_agent
|
| 9 |
+
from agents.news_agent.news_agent import create_news_agent
|
| 10 |
+
from workflow.nodes.nodes import supervisor_node, news_node, earnings_node, market_node, synth_node, AGENTS
|
| 11 |
+
|
| 12 |
|
| 13 |
def build_graph(llm_model_name: str = "gpt-4o-mini"):
|
| 14 |
|
|
|
|
| 17 |
llm = ChatOpenAI(api_key=openai_api_key,model=llm_model_name, temperature=0)
|
| 18 |
|
| 19 |
# --- Create specialized agents ---
|
| 20 |
+
news_agent = create_news_agent(llm)
|
| 21 |
+
earnings_agent = create_earnings_agent(llm)
|
| 22 |
+
market_agent = create_market_agent(llm)
|
| 23 |
|
| 24 |
# --- Create synthesizer chain ---
|
| 25 |
synthesizer = make_synthesizer(llm)
|
agents/earnings_agent/earnings_agent.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
| 2 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 3 |
+
from agents.earnings_agent.tools import fetch_earnings_tool
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import yaml
|
| 6 |
+
|
| 7 |
+
yaml_path = Path(__file__).parent / "prompts.yaml"
|
| 8 |
+
with yaml_path.open() as f:
|
| 9 |
+
prompt_template = yaml.safe_load(f)
|
| 10 |
+
|
| 11 |
+
def create_earnings_agent(model) -> AgentExecutor:
|
| 12 |
+
|
| 13 |
+
prompt = ChatPromptTemplate.from_messages(
|
| 14 |
+
[
|
| 15 |
+
("system", prompt_template["system"]),
|
| 16 |
+
("human", "{input}"),
|
| 17 |
+
MessagesPlaceholder("agent_scratchpad"),
|
| 18 |
+
]
|
| 19 |
+
)
|
| 20 |
+
agent = create_tool_calling_agent(llm=model, tools=[fetch_earnings_tool], prompt=prompt)
|
| 21 |
+
|
| 22 |
+
return AgentExecutor(agent=agent, tools=[fetch_earnings_tool], verbose=False, handle_parsing_errors=True)
|
agents/earnings_agent/prompts.yaml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system: |
|
| 2 |
+
You are an Earnings Analyst. Use the earnings tool to summarize the latest and upcoming
|
| 3 |
+
earnings information (dates, surprises if available) and key line items. Provide a
|
| 4 |
+
short view on momentum and watchouts. Output concise markdown.
|
agents/earnings_agent/tools.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.tools import tool
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
import datetime as dt
|
| 4 |
+
|
| 5 |
+
@tool("fetch_earnings", return_direct=False)
|
| 6 |
+
def fetch_earnings_tool(ticker: str) -> str:
|
| 7 |
+
"""
|
| 8 |
+
Fetch upcoming and recent earnings info via yfinance.
|
| 9 |
+
Returns a concise summary (dates + surprises if available).
|
| 10 |
+
"""
|
| 11 |
+
tk = yf.Ticker(ticker)
|
| 12 |
+
lines = [f"EARNINGS SNAPSHOT for {ticker.upper()}"]
|
| 13 |
+
|
| 14 |
+
# Upcoming earnings (earnings_dates includes future dates)
|
| 15 |
+
try:
|
| 16 |
+
ed = tk.earnings_dates # DataFrame if available
|
| 17 |
+
if ed is not None and not ed.empty:
|
| 18 |
+
# Take the next upcoming date and last reported
|
| 19 |
+
ed_sorted = ed.sort_index()
|
| 20 |
+
upcoming = ed_sorted[ed_sorted.index >= dt.datetime.now().date()]
|
| 21 |
+
last = ed_sorted[ed_sorted.index < dt.datetime.now().date()]
|
| 22 |
+
if not upcoming.empty:
|
| 23 |
+
lines.append(f"Upcoming: {upcoming.index[0].strftime('%Y-%m-%d')}")
|
| 24 |
+
if not last.empty:
|
| 25 |
+
# Try EPS surprise columns if present
|
| 26 |
+
row = last.iloc[-1]
|
| 27 |
+
surprise = None
|
| 28 |
+
for k in ["EPS Surprise %", "Surprise(%)", "epssurprisepct", "epssurprisepercent"]:
|
| 29 |
+
if k in row and row[k] is not None:
|
| 30 |
+
surprise = row[k]
|
| 31 |
+
break
|
| 32 |
+
lines.append(
|
| 33 |
+
f"Last reported: {last.index[-1].strftime('%Y-%m-%d')}"
|
| 34 |
+
+ (f", EPS surprise: {surprise}" if surprise is not None else "")
|
| 35 |
+
)
|
| 36 |
+
else:
|
| 37 |
+
lines.append("No earnings_dates available.")
|
| 38 |
+
except Exception as e:
|
| 39 |
+
lines.append(f"earnings_dates unavailable: {e}")
|
| 40 |
+
|
| 41 |
+
# Quarterly financials (very high-level)
|
| 42 |
+
try:
|
| 43 |
+
qf = tk.quarterly_financials
|
| 44 |
+
if qf is not None and not qf.empty:
|
| 45 |
+
cols = list(qf.columns)
|
| 46 |
+
if cols:
|
| 47 |
+
last_q = cols[0]
|
| 48 |
+
revenue = qf.loc["Total Revenue", last_q] if "Total Revenue" in qf.index else None
|
| 49 |
+
gross_profit = qf.loc["Gross Profit", last_q] if "Gross Profit" in qf.index else None
|
| 50 |
+
lines.append(f"Last quarter ({last_q.date()}): Revenue={revenue}, GrossProfit={gross_profit}")
|
| 51 |
+
except Exception:
|
| 52 |
+
pass
|
| 53 |
+
|
| 54 |
+
return "\n".join(lines)
|
agents/market_agent/market_agent.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
| 2 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 3 |
+
from agents.market_agent.tools import fetch_market_snapshot_tool
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import yaml
|
| 6 |
+
|
| 7 |
+
yaml_path = Path(__file__).parent / "prompts.yaml"
|
| 8 |
+
with yaml_path.open() as f:
|
| 9 |
+
prompt_template = yaml.safe_load(f)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def create_market_agent(model) -> AgentExecutor:
|
| 13 |
+
|
| 14 |
+
prompt = ChatPromptTemplate.from_messages(
|
| 15 |
+
[
|
| 16 |
+
("system", prompt_template["system"]),
|
| 17 |
+
("human", "{input}"),
|
| 18 |
+
MessagesPlaceholder("agent_scratchpad"),
|
| 19 |
+
]
|
| 20 |
+
)
|
| 21 |
+
agent = create_tool_calling_agent(llm=model, tools=[fetch_market_snapshot_tool], prompt=prompt)
|
| 22 |
+
|
| 23 |
+
return AgentExecutor(agent=agent, tools=[fetch_market_snapshot_tool], verbose=False, handle_parsing_errors=True)
|
agents/market_agent/prompts.yaml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system: |
|
| 2 |
+
You are a Market & Valuation Analyst. Use the market snapshot tool to extract current
|
| 3 |
+
trading context and discuss short-term technicals/flow and high-level valuation notes.
|
| 4 |
+
Output concise markdown.
|
agents/market_agent/tools.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.tools import tool
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
import datetime as dt
|
| 4 |
+
|
| 5 |
+
@tool("fetch_market_snapshot", return_direct=False)
|
| 6 |
+
def fetch_market_snapshot_tool(ticker: str) -> str:
|
| 7 |
+
"""
|
| 8 |
+
Pulls basic market snapshot with yfinance: price, change, volume, valuation.
|
| 9 |
+
Returns a compact textual snapshot.
|
| 10 |
+
"""
|
| 11 |
+
tk = yf.Ticker(ticker)
|
| 12 |
+
info = {}
|
| 13 |
+
try:
|
| 14 |
+
price = tk.fast_info.last_price
|
| 15 |
+
prev_close = tk.fast_info.previous_close
|
| 16 |
+
change = None
|
| 17 |
+
if price is not None and prev_close:
|
| 18 |
+
change = (price - prev_close) / prev_close * 100
|
| 19 |
+
info.update({
|
| 20 |
+
"price": price, "prev_close": prev_close, "pct_change": change,
|
| 21 |
+
"market_cap": tk.fast_info.market_cap, "volume": tk.fast_info.last_volume,
|
| 22 |
+
"currency": tk.fast_info.currency
|
| 23 |
+
})
|
| 24 |
+
except Exception as e:
|
| 25 |
+
return f"Market snapshot failed: {e}"
|
| 26 |
+
|
| 27 |
+
lines = [f"MARKET SNAPSHOT for {ticker.upper()}"]
|
| 28 |
+
lines.append(f"Price: {info.get('price')} {info.get('currency')}")
|
| 29 |
+
if info.get("pct_change") is not None:
|
| 30 |
+
lines.append(f"Day change: {info['pct_change']:.2f}%")
|
| 31 |
+
lines.append(f"Market Cap: {info.get('market_cap')}")
|
| 32 |
+
lines.append(f"Volume: {info.get('volume')}")
|
| 33 |
+
return "\n".join(lines)
|
agents/news_agent/news_agent.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
| 2 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 3 |
+
from agents.news_agent.tools import search_news_tool
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import yaml
|
| 6 |
+
|
| 7 |
+
yaml_path = Path(__file__).parent / "prompts.yaml"
|
| 8 |
+
with yaml_path.open() as f:
|
| 9 |
+
prompt_template = yaml.safe_load(f)
|
| 10 |
+
|
| 11 |
+
def create_news_agent(model) -> AgentExecutor:
|
| 12 |
+
|
| 13 |
+
prompt = ChatPromptTemplate.from_messages(
|
| 14 |
+
[
|
| 15 |
+
("system", prompt_template["system"]),
|
| 16 |
+
("human", "{input}"),
|
| 17 |
+
MessagesPlaceholder("agent_scratchpad"),
|
| 18 |
+
]
|
| 19 |
+
)
|
| 20 |
+
agent = create_tool_calling_agent(llm=model, tools=[search_news_tool], prompt=prompt)
|
| 21 |
+
|
| 22 |
+
return AgentExecutor(agent=agent, tools=[search_news_tool], verbose=False, handle_parsing_errors=True)
|
agents/news_agent/prompts.yaml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system: |
|
| 2 |
+
You are a News Analyst. Use the search tool to gather 5-8 recent, credible items.
|
| 3 |
+
Synthesize themes, risks, catalysts, and sentiment for investors. Output a concise
|
| 4 |
+
markdown summary with bullet points and 1-2 short citations (URLs).
|
agents/news_agent/tools.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.tools import tool
|
| 2 |
+
try:
|
| 3 |
+
from duckduckgo_search import DDGS
|
| 4 |
+
except Exception:
|
| 5 |
+
DDGS = None
|
| 6 |
+
|
| 7 |
+
@tool("search_news", return_direct=False)
|
| 8 |
+
def search_news_tool(query: str, max_results: int = 5) -> str:
|
| 9 |
+
"""
|
| 10 |
+
Search latest headlines & snippets relevant to a stock or topic.
|
| 11 |
+
Uses duckduckgo_search as a simple public news proxy.
|
| 12 |
+
Returns a concise, newline-separated list of 'title — url'.
|
| 13 |
+
"""
|
| 14 |
+
if DDGS is None:
|
| 15 |
+
return ("duckduckgo_search not installed. "
|
| 16 |
+
"Install with `pip install duckduckgo-search` "
|
| 17 |
+
"or replace this tool with your news API.")
|
| 18 |
+
items = []
|
| 19 |
+
with DDGS() as ddgs:
|
| 20 |
+
for r in ddgs.news(query, timelimit="7d", max_results=max_results):
|
| 21 |
+
title = r.get("title", "")[:160]
|
| 22 |
+
url = r.get("url", "")
|
| 23 |
+
if title and url:
|
| 24 |
+
items.append(f"{title} — {url}")
|
| 25 |
+
if not items:
|
| 26 |
+
return "No recent news found."
|
| 27 |
+
return "\n".join(items)
|
agents/supervisor_router.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
from agents.graph_state import GraphState
|
| 2 |
-
|
| 3 |
-
AGENTS = ["news", "earnings", "market"]
|
| 4 |
-
|
| 5 |
-
def supervisor_router(state: GraphState) -> str:
|
| 6 |
-
remaining = [a for a in AGENTS if a not in state.get("completed", [])]
|
| 7 |
-
return remaining[0] if remaining else "synth"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from
|
| 3 |
-
from
|
| 4 |
|
| 5 |
-
app =
|
| 6 |
|
| 7 |
def run_user_query(ticker, history):
|
| 8 |
QUERY = f"Produce investor-ready insights for {ticker}."
|
|
@@ -29,4 +29,4 @@ with gr.Blocks() as demo:
|
|
| 29 |
clear.click(lambda: None, None, chatbot, queue=False)
|
| 30 |
|
| 31 |
if __name__ == "__main__":
|
| 32 |
-
demo.launch()
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from workflow.agents_workflow import build_agents_workflow
|
| 3 |
+
from workflow.graph_state import GraphState
|
| 4 |
|
| 5 |
+
app = build_agents_workflow(llm_model_name="gpt-4o-mini")
|
| 6 |
|
| 7 |
def run_user_query(ticker, history):
|
| 8 |
QUERY = f"Produce investor-ready insights for {ticker}."
|
|
|
|
| 29 |
clear.click(lambda: None, None, chatbot, queue=False)
|
| 30 |
|
| 31 |
if __name__ == "__main__":
|
| 32 |
+
demo.launch(share=True, show_api=False)
|
main.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agents.build_graph import build_graph
|
| 2 |
+
from workflow.agents_workflow import build_agents_workflow
|
| 3 |
+
from workflow.graph_state import GraphState
|
| 4 |
+
|
| 5 |
+
# Run locally
|
| 6 |
+
|
| 7 |
+
app = build_agents_workflow(llm_model_name="gpt-4o-mini")
|
| 8 |
+
|
| 9 |
+
def run_user_query(ticker):
|
| 10 |
+
QUERY = f"Produce investor-ready insights for {ticker}."
|
| 11 |
+
init_state: GraphState = {
|
| 12 |
+
"ticker": ticker,
|
| 13 |
+
"query": QUERY,
|
| 14 |
+
"news_summary": None,
|
| 15 |
+
"earnings_summary": None,
|
| 16 |
+
"market_summary": None,
|
| 17 |
+
"completed": [],
|
| 18 |
+
"final_recommendation": None,
|
| 19 |
+
}
|
| 20 |
+
final_state = app.invoke(init_state)
|
| 21 |
+
|
| 22 |
+
# Update the Gradio chat history
|
| 23 |
+
|
| 24 |
+
return final_state
|
| 25 |
+
|
| 26 |
+
state = run_user_query("AAPL")
|
| 27 |
+
|
| 28 |
+
print("\n" + "=" * 80)
|
| 29 |
+
print(f"### NEWS SUMMARY\n{state['news_summary']}\n")
|
| 30 |
+
print(f"### EARNINGS SUMMARY\n{state['earnings_summary']}\n")
|
| 31 |
+
print(f"### MARKET SUMMARY\n{state['market_summary']}\n")
|
| 32 |
+
print(f"### FINAL RECOMMENDATION\n{state['final_recommendation']}\n")
|
model/init_model.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from langchain_openai import ChatOpenAI
|
| 3 |
+
|
| 4 |
+
def init_main_model(llm_model_name: str):
|
| 5 |
+
openai_api_key = os.getenv("openai_api_key")
|
| 6 |
+
llm = ChatOpenAI(api_key=openai_api_key, model=llm_model_name, temperature=0)
|
| 7 |
+
|
| 8 |
+
return llm
|
requirements.txt
CHANGED
|
@@ -2,4 +2,11 @@ langgraph==0.6.10
|
|
| 2 |
langchain==0.3.27
|
| 3 |
langchain_openai==0.3.35
|
| 4 |
duckduckgo_search==8.1.1
|
| 5 |
-
yfinance==0.2.66
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
langchain==0.3.27
|
| 3 |
langchain_openai==0.3.35
|
| 4 |
duckduckgo_search==8.1.1
|
| 5 |
+
#yfinance==0.2.66
|
| 6 |
+
websockets
|
| 7 |
+
yfinance~=0.2.53
|
| 8 |
+
duckduckgo-search~=8.1.1
|
| 9 |
+
pyyaml~=6.0.3
|
| 10 |
+
langchain-openai~=0.3.35
|
| 11 |
+
gradio~=4.44.1
|
| 12 |
+
huggingface-hub~=0.35.3
|
tools/__init__.py
ADDED
|
File without changes
|
{agents → tools}/tools.py
RENAMED
|
File without changes
|
workflow/agents_workflow.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 2 |
+
import yaml
|
| 3 |
+
from langgraph.graph import StateGraph, END
|
| 4 |
+
|
| 5 |
+
from agents.earnings_agent.earnings_agent import create_earnings_agent
|
| 6 |
+
from agents.market_agent.market_agent import create_market_agent
|
| 7 |
+
from agents.news_agent.news_agent import create_news_agent
|
| 8 |
+
from model.init_model import init_main_model
|
| 9 |
+
from workflow.graph_state import GraphState
|
| 10 |
+
from workflow.nodes.nodes import news_node, earnings_node, market_node, synth_node, supervisor_node, AGENTS, supervisor_router
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
yaml_path = Path(__file__).parent / "prompts.yaml"
|
| 14 |
+
with yaml_path.open() as f:
|
| 15 |
+
prompt_template = yaml.safe_load(f)
|
| 16 |
+
|
| 17 |
+
def make_synthesizer(model):
|
| 18 |
+
"""Final writer to merge all agent outputs into actionable recommendations."""
|
| 19 |
+
template = ChatPromptTemplate.from_messages(
|
| 20 |
+
[
|
| 21 |
+
("system", prompt_template["system"]),
|
| 22 |
+
("human", prompt_template["human"])
|
| 23 |
+
]
|
| 24 |
+
)
|
| 25 |
+
return template | model # LC chain: Prompt -> LLM
|
| 26 |
+
|
| 27 |
+
def build_agents_workflow(llm_model_name):
|
| 28 |
+
# --- Base LLM for agents & synthesizer, we can initiate different models for agents here ---
|
| 29 |
+
model = init_main_model(llm_model_name)
|
| 30 |
+
|
| 31 |
+
# --- Create specialized agents ---
|
| 32 |
+
news_agent = create_news_agent(model)
|
| 33 |
+
earnings_agent = create_earnings_agent(model)
|
| 34 |
+
market_agent = create_market_agent(model)
|
| 35 |
+
|
| 36 |
+
# --- Create synthesizer chain ---
|
| 37 |
+
synthesizer = make_synthesizer(model)
|
| 38 |
+
|
| 39 |
+
# --- LangGraph: wire nodes ---
|
| 40 |
+
g = StateGraph(GraphState)
|
| 41 |
+
|
| 42 |
+
# Bind node callables with their dependencies via closures
|
| 43 |
+
g.add_node("news", lambda s: news_node(s, news_agent))
|
| 44 |
+
g.add_node("earnings", lambda s: earnings_node(s, earnings_agent))
|
| 45 |
+
g.add_node("market", lambda s: market_node(s, market_agent))
|
| 46 |
+
g.add_node("synth", lambda s: synth_node(s, synthesizer))
|
| 47 |
+
|
| 48 |
+
# Supervisor node
|
| 49 |
+
g.add_node("supervisor", supervisor_node)
|
| 50 |
+
# Edges: start -> supervisor -> (news|earnings|market|synth) -> supervisor ... -> synth -> END
|
| 51 |
+
g.set_entry_point("supervisor")
|
| 52 |
+
|
| 53 |
+
for a in AGENTS:
|
| 54 |
+
g.add_edge(a, "supervisor")
|
| 55 |
+
g.add_edge("synth", END)
|
| 56 |
+
|
| 57 |
+
# Route decisions come from the router function (returns a string)
|
| 58 |
+
g.add_conditional_edges(
|
| 59 |
+
"supervisor",
|
| 60 |
+
supervisor_router, # returns: "news" | "earnings" | "market" | "synth"
|
| 61 |
+
{
|
| 62 |
+
"news": "news",
|
| 63 |
+
"earnings": "earnings",
|
| 64 |
+
"market": "market",
|
| 65 |
+
"synth": "synth",
|
| 66 |
+
},
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
return g.compile()
|
{agents → workflow}/graph_state.py
RENAMED
|
File without changes
|
{agents → workflow/nodes}/nodes.py
RENAMED
|
@@ -1,11 +1,18 @@
|
|
| 1 |
-
from langchain.agents import AgentExecutor
|
| 2 |
-
from
|
| 3 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
def news_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
| 6 |
ticker = state["ticker"]
|
| 7 |
-
|
| 8 |
-
res = agent.invoke({"input":
|
| 9 |
state["news_summary"] = res["output"]
|
| 10 |
state["completed"] = list(set(state["completed"] + ["news"]))
|
| 11 |
return state
|
|
@@ -13,8 +20,8 @@ def news_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
|
| 13 |
|
| 14 |
def earnings_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
| 15 |
ticker = state["ticker"]
|
| 16 |
-
|
| 17 |
-
res = agent.invoke({"input":
|
| 18 |
state["earnings_summary"] = res["output"]
|
| 19 |
state["completed"] = list(set(state["completed"] + ["earnings"]))
|
| 20 |
return state
|
|
@@ -22,8 +29,8 @@ def earnings_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
|
| 22 |
|
| 23 |
def market_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
| 24 |
ticker = state["ticker"]
|
| 25 |
-
|
| 26 |
-
res = agent.invoke({"input":
|
| 27 |
state["market_summary"] = res["output"]
|
| 28 |
state["completed"] = list(set(state["completed"] + ["market"]))
|
| 29 |
return state
|
|
@@ -45,8 +52,6 @@ def supervisor_node(state: GraphState) -> GraphState:
|
|
| 45 |
# Do any bookkeeping here if needed; otherwise just pass state through
|
| 46 |
return state
|
| 47 |
|
| 48 |
-
AGENTS = ["news", "earnings", "market"]
|
| 49 |
-
|
| 50 |
def supervisor_router(state: GraphState) -> str:
|
| 51 |
remaining = [a for a in AGENTS if a not in state.get("completed", [])]
|
| 52 |
return remaining[0] if remaining else "synth"
|
|
|
|
| 1 |
+
from langchain.agents import AgentExecutor
|
| 2 |
+
from workflow.graph_state import GraphState
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import yaml
|
| 5 |
+
|
| 6 |
+
yaml_path = Path(__file__).parent / "prompts.yaml"
|
| 7 |
+
with yaml_path.open() as f:
|
| 8 |
+
prompt_template = yaml.safe_load(f)
|
| 9 |
+
|
| 10 |
+
AGENTS = ["news", "earnings", "market"]
|
| 11 |
|
| 12 |
def news_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
| 13 |
ticker = state["ticker"]
|
| 14 |
+
query = prompt_template['news_user_prompt'].format(ticker=ticker)
|
| 15 |
+
res = agent.invoke({"input": query})
|
| 16 |
state["news_summary"] = res["output"]
|
| 17 |
state["completed"] = list(set(state["completed"] + ["news"]))
|
| 18 |
return state
|
|
|
|
| 20 |
|
| 21 |
def earnings_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
| 22 |
ticker = state["ticker"]
|
| 23 |
+
query = prompt_template['earnings_user_prompt'].format(ticker=ticker)
|
| 24 |
+
res = agent.invoke({"input": query})
|
| 25 |
state["earnings_summary"] = res["output"]
|
| 26 |
state["completed"] = list(set(state["completed"] + ["earnings"]))
|
| 27 |
return state
|
|
|
|
| 29 |
|
| 30 |
def market_node(state: GraphState, agent: AgentExecutor) -> GraphState:
|
| 31 |
ticker = state["ticker"]
|
| 32 |
+
query = prompt_template['market_user_prompt'].format(ticker=ticker)
|
| 33 |
+
res = agent.invoke({"input": query})
|
| 34 |
state["market_summary"] = res["output"]
|
| 35 |
state["completed"] = list(set(state["completed"] + ["market"]))
|
| 36 |
return state
|
|
|
|
| 52 |
# Do any bookkeeping here if needed; otherwise just pass state through
|
| 53 |
return state
|
| 54 |
|
|
|
|
|
|
|
| 55 |
def supervisor_router(state: GraphState) -> str:
|
| 56 |
remaining = [a for a in AGENTS if a not in state.get("completed", [])]
|
| 57 |
return remaining[0] if remaining else "synth"
|
workflow/nodes/prompts.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
news_user_prompt: Research recent news for {ticker}. Focus on price-moving catalysts.
|
| 2 |
+
earnings_user_prompt: Analyze earnings for {ticker}. Summarize last and upcoming earnings. Use the tool.
|
| 3 |
+
market_user_prompt: Provide a market snapshot for {ticker}. Use the tool.
|
workflow/prompts.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system: |
|
| 2 |
+
You are the Lead Portfolio Analyst. Merge inputs from News, Earnings, and Market agents.
|
| 3 |
+
Produce a final, actionable recommendation block (Buy/Hold/Sell with confidence 0-1),
|
| 4 |
+
key drivers (bull/bear), near-term catalysts, and 2-3 risks. Be concise and concrete.
|
| 5 |
+
|
| 6 |
+
human: |
|
| 7 |
+
Ticker: {ticker}\n\n
|
| 8 |
+
### News Summary\n{news_summary}\n\n
|
| 9 |
+
### Earnings Summary\n{earnings_summary}\n\n
|
| 10 |
+
### Market Summary\n{market_summary}\n\n
|
| 11 |
+
Write the final recommendation now.
|