Aleksey Matsarski
commited on
Commit
·
07097fe
1
Parent(s):
2ef7125
clean code
Browse files- agents/__init__.py +0 -0
- agents/agent_builder.py +0 -62
- agents/build_graph.py +0 -57
- main.py +1 -4
- tools/__init__.py +0 -0
- tools/tools.py +0 -112
agents/__init__.py
DELETED
|
File without changes
|
agents/agent_builder.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 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."""
|
| 7 |
-
prompt = ChatPromptTemplate.from_messages(
|
| 8 |
-
[
|
| 9 |
-
("system", system_instructions),
|
| 10 |
-
# MessagesPlaceholder("chat_history"),
|
| 11 |
-
("human", "{input}"),
|
| 12 |
-
MessagesPlaceholder("agent_scratchpad"),
|
| 13 |
-
]
|
| 14 |
-
)
|
| 15 |
-
agent = create_tool_calling_agent(llm=llm, tools=tools, prompt=prompt)
|
| 16 |
-
return AgentExecutor(agent=agent, tools=tools, verbose=False, handle_parsing_errors=True)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def make_news_agent(model) -> AgentExecutor:
|
| 20 |
-
sys = (
|
| 21 |
-
"You are a News Analyst. Use the search tool to gather 5-8 recent, credible items. "
|
| 22 |
-
"Synthesize themes, risks, catalysts, and sentiment for investors. Output a concise "
|
| 23 |
-
"markdown summary with bullet points and 1-2 short citations (URLs)."
|
| 24 |
-
)
|
| 25 |
-
return build_agent(model, [search_news_tool], sys)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def make_earnings_agent(model) -> AgentExecutor:
|
| 29 |
-
sys = (
|
| 30 |
-
"You are an Earnings Analyst. Use the earnings tool to summarize the latest and upcoming "
|
| 31 |
-
"earnings information (dates, surprises if available) and key line items. Provide a "
|
| 32 |
-
"short view on momentum and watchouts. Output concise markdown."
|
| 33 |
-
)
|
| 34 |
-
return build_agent(model, [fetch_earnings_tool], sys)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def make_market_agent(model) -> AgentExecutor:
|
| 38 |
-
sys = (
|
| 39 |
-
"You are a Market & Valuation Analyst. Use the market snapshot tool to extract current "
|
| 40 |
-
"trading context and discuss short-term technicals/flow and high-level valuation notes. "
|
| 41 |
-
"Output concise markdown."
|
| 42 |
-
)
|
| 43 |
-
return build_agent(model, [fetch_market_snapshot_tool], sys)
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def make_synthesizer(model):
|
| 47 |
-
"""Final writer to merge all agent outputs into actionable recommendations."""
|
| 48 |
-
template = ChatPromptTemplate.from_messages(
|
| 49 |
-
[
|
| 50 |
-
("system",
|
| 51 |
-
"You are the Lead Portfolio Analyst. Merge inputs from News, Earnings, and Market agents. "
|
| 52 |
-
"Produce a final, actionable recommendation block (Buy/Hold/Sell with confidence 0-1), "
|
| 53 |
-
"key drivers (bull/bear), near-term catalysts, and 2-3 risks. Be concise and concrete."),
|
| 54 |
-
("human",
|
| 55 |
-
"Ticker: {ticker}\n\n"
|
| 56 |
-
"### News Summary\n{news_summary}\n\n"
|
| 57 |
-
"### Earnings Summary\n{earnings_summary}\n\n"
|
| 58 |
-
"### Market Summary\n{market_summary}\n\n"
|
| 59 |
-
"Write the final recommendation now.")
|
| 60 |
-
]
|
| 61 |
-
)
|
| 62 |
-
return template | model # LC chain: Prompt -> LLM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents/build_graph.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 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 |
-
|
| 15 |
-
# --- Base LLM for agents & synthesizer (swap to your provider as needed) ---
|
| 16 |
-
openai_api_key=os.getenv("openai_api_key")
|
| 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)
|
| 26 |
-
|
| 27 |
-
# --- LangGraph: wire nodes ---
|
| 28 |
-
g = StateGraph(GraphState)
|
| 29 |
-
|
| 30 |
-
# Bind node callables with their dependencies via closures
|
| 31 |
-
g.add_node("news", lambda s: news_node(s, news_agent))
|
| 32 |
-
g.add_node("earnings", lambda s: earnings_node(s, earnings_agent))
|
| 33 |
-
g.add_node("market", lambda s: market_node(s, market_agent))
|
| 34 |
-
g.add_node("synth", lambda s: synth_node(s, synthesizer))
|
| 35 |
-
|
| 36 |
-
# Supervisor node
|
| 37 |
-
g.add_node("supervisor", supervisor_node)
|
| 38 |
-
# Edges: start -> supervisor -> (news|earnings|market|synth) -> supervisor ... -> synth -> END
|
| 39 |
-
g.set_entry_point("supervisor")
|
| 40 |
-
|
| 41 |
-
for a in AGENTS:
|
| 42 |
-
g.add_edge(a, "supervisor")
|
| 43 |
-
g.add_edge("synth", END)
|
| 44 |
-
|
| 45 |
-
# Route decisions come from the router function (returns a string)
|
| 46 |
-
g.add_conditional_edges(
|
| 47 |
-
"supervisor",
|
| 48 |
-
supervisor_router, # returns: "news" | "earnings" | "market" | "synth"
|
| 49 |
-
{
|
| 50 |
-
"news": "news",
|
| 51 |
-
"earnings": "earnings",
|
| 52 |
-
"market": "market",
|
| 53 |
-
"synth": "synth",
|
| 54 |
-
},
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
return g.compile()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 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 |
|
|
@@ -19,8 +18,6 @@ def run_user_query(ticker):
|
|
| 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")
|
|
|
|
|
|
|
| 1 |
from workflow.agents_workflow import build_agents_workflow
|
| 2 |
from workflow.graph_state import GraphState
|
| 3 |
|
| 4 |
+
# Run locally without gradio
|
| 5 |
|
| 6 |
app = build_agents_workflow(llm_model_name="gpt-4o-mini")
|
| 7 |
|
|
|
|
| 18 |
}
|
| 19 |
final_state = app.invoke(init_state)
|
| 20 |
|
|
|
|
|
|
|
| 21 |
return final_state
|
| 22 |
|
| 23 |
state = run_user_query("AAPL")
|
tools/__init__.py
DELETED
|
File without changes
|
tools/tools.py
DELETED
|
@@ -1,112 +0,0 @@
|
|
| 1 |
-
from langchain.tools import tool
|
| 2 |
-
import yfinance as yf
|
| 3 |
-
import datetime as dt
|
| 4 |
-
try:
|
| 5 |
-
from duckduckgo_search import DDGS
|
| 6 |
-
except Exception:
|
| 7 |
-
DDGS = None # optional; handle gracefully below
|
| 8 |
-
|
| 9 |
-
@tool("search_news", return_direct=False)
|
| 10 |
-
def search_news_tool(query: str, max_results: int = 5) -> str:
|
| 11 |
-
"""
|
| 12 |
-
Search latest headlines & snippets relevant to a stock or topic.
|
| 13 |
-
Uses duckduckgo_search as a simple public news proxy.
|
| 14 |
-
Returns a concise, newline-separated list of 'title — url'.
|
| 15 |
-
"""
|
| 16 |
-
if DDGS is None:
|
| 17 |
-
return ("duckduckgo_search not installed. "
|
| 18 |
-
"Install with `pip install duckduckgo-search` "
|
| 19 |
-
"or replace this tool with your news API.")
|
| 20 |
-
items = []
|
| 21 |
-
with DDGS() as ddgs:
|
| 22 |
-
for r in ddgs.news(query, timelimit="7d", max_results=max_results):
|
| 23 |
-
title = r.get("title", "")[:160]
|
| 24 |
-
url = r.get("url", "")
|
| 25 |
-
if title and url:
|
| 26 |
-
items.append(f"{title} — {url}")
|
| 27 |
-
if not items:
|
| 28 |
-
return "No recent news found."
|
| 29 |
-
return "\n".join(items)
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
@tool("fetch_earnings", return_direct=False)
|
| 33 |
-
def fetch_earnings_tool(ticker: str) -> str:
|
| 34 |
-
"""
|
| 35 |
-
Fetch upcoming and recent earnings info via yfinance.
|
| 36 |
-
Returns a concise summary (dates + surprises if available).
|
| 37 |
-
"""
|
| 38 |
-
tk = yf.Ticker(ticker)
|
| 39 |
-
lines = [f"EARNINGS SNAPSHOT for {ticker.upper()}"]
|
| 40 |
-
|
| 41 |
-
# Upcoming earnings (earnings_dates includes future dates)
|
| 42 |
-
try:
|
| 43 |
-
ed = tk.earnings_dates # DataFrame if available
|
| 44 |
-
if ed is not None and not ed.empty:
|
| 45 |
-
# Take the next upcoming date and last reported
|
| 46 |
-
ed_sorted = ed.sort_index()
|
| 47 |
-
upcoming = ed_sorted[ed_sorted.index >= dt.datetime.now().date()]
|
| 48 |
-
last = ed_sorted[ed_sorted.index < dt.datetime.now().date()]
|
| 49 |
-
if not upcoming.empty:
|
| 50 |
-
lines.append(f"Upcoming: {upcoming.index[0].strftime('%Y-%m-%d')}")
|
| 51 |
-
if not last.empty:
|
| 52 |
-
# Try EPS surprise columns if present
|
| 53 |
-
row = last.iloc[-1]
|
| 54 |
-
surprise = None
|
| 55 |
-
for k in ["EPS Surprise %", "Surprise(%)", "epssurprisepct", "epssurprisepercent"]:
|
| 56 |
-
if k in row and row[k] is not None:
|
| 57 |
-
surprise = row[k]
|
| 58 |
-
break
|
| 59 |
-
lines.append(
|
| 60 |
-
f"Last reported: {last.index[-1].strftime('%Y-%m-%d')}"
|
| 61 |
-
+ (f", EPS surprise: {surprise}" if surprise is not None else "")
|
| 62 |
-
)
|
| 63 |
-
else:
|
| 64 |
-
lines.append("No earnings_dates available.")
|
| 65 |
-
except Exception as e:
|
| 66 |
-
lines.append(f"earnings_dates unavailable: {e}")
|
| 67 |
-
|
| 68 |
-
# Quarterly financials (very high-level)
|
| 69 |
-
try:
|
| 70 |
-
qf = tk.quarterly_financials
|
| 71 |
-
if qf is not None and not qf.empty:
|
| 72 |
-
cols = list(qf.columns)
|
| 73 |
-
if cols:
|
| 74 |
-
last_q = cols[0]
|
| 75 |
-
revenue = qf.loc["Total Revenue", last_q] if "Total Revenue" in qf.index else None
|
| 76 |
-
gross_profit = qf.loc["Gross Profit", last_q] if "Gross Profit" in qf.index else None
|
| 77 |
-
lines.append(f"Last quarter ({last_q.date()}): Revenue={revenue}, GrossProfit={gross_profit}")
|
| 78 |
-
except Exception:
|
| 79 |
-
pass
|
| 80 |
-
|
| 81 |
-
return "\n".join(lines)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
@tool("fetch_market_snapshot", return_direct=False)
|
| 85 |
-
def fetch_market_snapshot_tool(ticker: str) -> str:
|
| 86 |
-
"""
|
| 87 |
-
Pulls basic market snapshot with yfinance: price, change, volume, valuation.
|
| 88 |
-
Returns a compact textual snapshot.
|
| 89 |
-
"""
|
| 90 |
-
tk = yf.Ticker(ticker)
|
| 91 |
-
info = {}
|
| 92 |
-
try:
|
| 93 |
-
price = tk.fast_info.last_price
|
| 94 |
-
prev_close = tk.fast_info.previous_close
|
| 95 |
-
change = None
|
| 96 |
-
if price is not None and prev_close:
|
| 97 |
-
change = (price - prev_close) / prev_close * 100
|
| 98 |
-
info.update({
|
| 99 |
-
"price": price, "prev_close": prev_close, "pct_change": change,
|
| 100 |
-
"market_cap": tk.fast_info.market_cap, "volume": tk.fast_info.last_volume,
|
| 101 |
-
"currency": tk.fast_info.currency
|
| 102 |
-
})
|
| 103 |
-
except Exception as e:
|
| 104 |
-
return f"Market snapshot failed: {e}"
|
| 105 |
-
|
| 106 |
-
lines = [f"MARKET SNAPSHOT for {ticker.upper()}"]
|
| 107 |
-
lines.append(f"Price: {info.get('price')} {info.get('currency')}")
|
| 108 |
-
if info.get("pct_change") is not None:
|
| 109 |
-
lines.append(f"Day change: {info['pct_change']:.2f}%")
|
| 110 |
-
lines.append(f"Market Cap: {info.get('market_cap')}")
|
| 111 |
-
lines.append(f"Volume: {info.get('volume')}")
|
| 112 |
-
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|