Aleksey Matsarski commited on
Commit
a47e415
·
1 Parent(s): 10a5b56

Refactoring code, provide better abstraction and file structure

Browse files
.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 langchain_openai import ChatOpenAI
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 make_news_agent, make_earnings_agent, make_market_agent, make_synthesizer
4
- from agents.graph_state import GraphState
 
5
  from langchain_openai import ChatOpenAI
6
- from agents.nodes import supervisor_node, news_node, earnings_node, market_node, synth_node
7
- from agents.supervisor_router import AGENTS, supervisor_router
 
 
 
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 = make_news_agent(llm)
17
- earnings_agent = make_earnings_agent(llm)
18
- market_agent = make_market_agent(llm)
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 huggingface_hub import InferenceClient
3
- from agents.build_graph import build_graph
4
 
5
- app = build_graph(llm_model_name="gpt-4o-mini")
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, create_tool_calling_agent
2
- from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
3
- from agents.graph_state import GraphState
 
 
 
 
 
 
 
4
 
5
  def news_node(state: GraphState, agent: AgentExecutor) -> GraphState:
6
  ticker = state["ticker"]
7
- q = f"Research recent news for {ticker}. Focus on price-moving catalysts."
8
- res = agent.invoke({"input": q})
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
- q = f"Analyze earnings for {ticker}. Summarize last and upcoming earnings. Use the tool."
17
- res = agent.invoke({"input": q})
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
- q = f"Provide a market snapshot for {ticker}. Use the tool."
26
- res = agent.invoke({"input": q})
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.