| import operator |
| import os |
| from typing import Annotated, TypedDict |
|
|
| import pandas as pd |
| import streamlit as st |
| from dotenv import load_dotenv |
| from langchain_chroma import Chroma |
| from langchain_community.tools import DuckDuckGoSearchRun |
| from langchain_core.messages import HumanMessage, SystemMessage |
| from langchain_huggingface import HuggingFaceEmbeddings |
| from langchain_openai import ChatOpenAI |
| from langgraph.graph import END, StateGraph |
|
|
| from news_price_correlation import build_correlation_analysis |
| from nvidia_agent_core import forecast_markdown, predict_nvidia_stock_payload, route_query, run_ensemble_forecast |
|
|
|
|
| load_dotenv() |
|
|
| st.set_page_config(page_title="NVIDIA AI Assistant", page_icon="📈", layout="wide") |
|
|
| CSV_PATH = "nvda_2014_to_2026.csv" |
| MODEL_PATH = "nvidia_price_model.pkl" |
| CHROMA_PATH = "./chroma_db_v2" |
| COLLECTION_NAME = "nvidia_annual_reports_2014_2025" |
|
|
| if not os.getenv("OPENAI_API_KEY"): |
| st.error("OPENAI_API_KEY not found in .env") |
| st.stop() |
|
|
| llm = ChatOpenAI(model="gpt-5.4", temperature=0.3, max_tokens=1024) |
| ddg_search = DuckDuckGoSearchRun() |
|
|
|
|
| @st.cache_resource |
| def load_rag(): |
| try: |
| import chromadb |
| from chromadb.config import Settings |
|
|
| embeddings = HuggingFaceEmbeddings( |
| model_name="all-mpnet-base-v2", |
| model_kwargs={"device": "cpu"}, |
| ) |
| client = chromadb.PersistentClient( |
| path=CHROMA_PATH, |
| settings=Settings(allow_reset=True), |
| ) |
| return Chroma( |
| client=client, |
| collection_name=COLLECTION_NAME, |
| embedding_function=embeddings, |
| ) |
| except Exception as exc: |
| st.sidebar.error(f"RAG load failed: {exc}") |
| return None |
|
|
|
|
| vectorstore = load_rag() |
|
|
|
|
| class AgentState(TypedDict): |
| query: str |
| response: str |
| debug_log: Annotated[str, operator.add] |
| next_node: str |
|
|
|
|
| def router_node(state: AgentState) -> AgentState: |
| notebook_route = route_query(state["query"]) |
| route_map = { |
| "ML_agent": "trader", |
| "results_strategy_agent": "researcher", |
| "outlook_agent": "researcher", |
| "correlation_agent": "correlation", |
| "general_agent": "general", |
| } |
|
|
| if notebook_route == "ML_agent" and any( |
| term in state["query"].lower() |
| for term in ["12 months", "one year", "next year", "2027", "long term"] |
| ): |
| state["next_node"] = "long_term_analyst" |
| else: |
| state["next_node"] = route_map.get(notebook_route, "general") |
|
|
| state["debug_log"] = f"Router Decision: {state['next_node']} (notebook route: {notebook_route})\n" |
| return state |
|
|
|
|
| def researcher_node(state: AgentState) -> AgentState: |
| query = state["query"] |
| context = "" |
| if vectorstore: |
| docs = vectorstore.similarity_search(query, k=5) |
| context = "\n\n".join(doc.page_content[:700] for doc in docs) |
|
|
| news = ddg_search.run(f"NVIDIA {query} latest news OR earnings")[:1000] |
| response = llm.invoke( |
| [ |
| SystemMessage(content="You are NVIDIA expert researcher. Use retrieved report context and current news."), |
| HumanMessage(content=f"Query: {query}\n\nAnnual-report context:\n{context}\n\nNews:\n{news}"), |
| ] |
| ).content |
|
|
| state["response"] = response |
| state["debug_log"] += "Researcher Agent completed.\n" |
| return state |
|
|
|
|
| def trader_node(state: AgentState) -> AgentState: |
| try: |
| payload = predict_nvidia_stock_payload(periods=7) |
| state["response"] = forecast_markdown(payload) |
| state["debug_log"] += ( |
| "Trader Agent -> called shared Prophet v3 residual ensemble; " |
| f"model={payload.get('model_version')}; residual={payload.get('uses_residual_model')}\n" |
| ) |
| except Exception as exc: |
| state["response"] = f"Forecast error: {exc}" |
| state["debug_log"] += f"Trader Agent error: {exc}\n" |
| return state |
|
|
|
|
| def long_term_analyst_node(state: AgentState) -> AgentState: |
| try: |
| checkpoint, forecast, _ = run_ensemble_forecast(periods=30, model_path=MODEL_PATH) |
| last_close = checkpoint["last_close"] |
| final_30d = float(forecast["yhat"].iloc[-1]) |
| upside_30d = (final_30d / last_close - 1) * 100 |
| pred = forecast.tail(7)[["ds", "yhat"]].copy() |
| pred["ds"] = pd.to_datetime(pred["ds"]).dt.strftime("%Y-%m-%d") |
|
|
| context = "" |
| if vectorstore: |
| docs = vectorstore.similarity_search(state["query"], k=6) |
| context = "\n\n".join(doc.page_content[:600] for doc in docs) |
| news = ddg_search.run("NVIDIA price target 2026 2027 analyst forecast Blackwell revenue")[:1000] |
|
|
| llm_response = llm.invoke( |
| [ |
| SystemMessage(content="You are NVIDIA senior equity analyst. Combine quantitative forecast with fundamentals."), |
| HumanMessage( |
| content=f"""Query: {state['query']} |
| Short-term Prophet ensemble: expected {upside_30d:+.1f}% in 30 business days to ${final_30d:.2f} |
| Annual-report context: {context} |
| Latest analyst/news: {news} |
| |
| Give a balanced 12-month outlook with key drivers, risks, a realistic range, and a trade view.""" |
| ), |
| ] |
| ).content |
|
|
| forecast_lines = "\n".join(f"{row['ds']}: **${row['yhat']:.2f}**" for _, row in pred.iterrows()) |
| state["response"] = f"""**NVIDIA 12-Month Outlook (Hybrid ML + LLM)** |
| |
| **Short-term Prophet v3 residual ensemble, next 7 business days:** |
| {forecast_lines} |
| |
| **30-business-day expected move:** **{upside_30d:+.1f}%** -> ~${final_30d:.2f} |
| |
| {llm_response} |
| |
| *Model: {checkpoint.get('model_version', 'unknown')} | Residual ML: {checkpoint.get('residual_model') is not None} | Backtested MAPE: {checkpoint.get('backtest_mape', 0):.2f}% | Directional Accuracy: {checkpoint.get('directional_accuracy', 0):.1f}%* |
| """ |
| state["debug_log"] += "Long-term Analyst completed with shared ensemble forecast.\n" |
| except Exception as exc: |
| state["response"] = f"Long-term analysis error: {exc}" |
| state["debug_log"] += f"Long-term Analyst error: {exc}\n" |
| return state |
|
|
|
|
| def correlation_node(state: AgentState) -> AgentState: |
| try: |
| state["response"] = build_correlation_analysis( |
| query=state["query"], |
| search=ddg_search.run, |
| llm=llm, |
| csv_path=CSV_PATH, |
| ) |
| state["debug_log"] += "Correlation Agent -> shared news/price correlation analysis.\n" |
| except Exception as exc: |
| state["response"] = f"Correlation analysis error: {exc}" |
| state["debug_log"] += f"Correlation Agent error: {exc}\n" |
| return state |
|
|
|
|
| def general_node(state: AgentState) -> AgentState: |
| response = llm.invoke( |
| [ |
| SystemMessage(content="You are a helpful NVIDIA assistant."), |
| HumanMessage(content=state["query"]), |
| ] |
| ).content |
| state["response"] = response |
| state["debug_log"] += "General Agent completed.\n" |
| return state |
|
|
|
|
| workflow = StateGraph(AgentState) |
| workflow.add_node("router", router_node) |
| workflow.add_node("researcher", researcher_node) |
| workflow.add_node("trader", trader_node) |
| workflow.add_node("long_term_analyst", long_term_analyst_node) |
| workflow.add_node("correlation", correlation_node) |
| workflow.add_node("general", general_node) |
| workflow.set_entry_point("router") |
| workflow.add_conditional_edges( |
| "router", |
| lambda state: state["next_node"], |
| { |
| "researcher": "researcher", |
| "trader": "trader", |
| "long_term_analyst": "long_term_analyst", |
| "correlation": "correlation", |
| "general": "general", |
| }, |
| ) |
| for node in ["researcher", "trader", "long_term_analyst", "correlation", "general"]: |
| workflow.add_edge(node, END) |
|
|
| app = workflow.compile() |
|
|
|
|
| st.title("NVIDIA AI Assistant - Multi-Agent System") |
| st.caption("NTU DSAI Capstone | RAG + DuckDuckGo + Prophet v3 Residual ML Ensemble") |
|
|
| if "messages" not in st.session_state: |
| st.session_state.messages = [] |
|
|
| for msg in st.session_state.messages: |
| with st.chat_message(msg["role"]): |
| st.markdown(msg["content"]) |
|
|
| if prompt := st.chat_input("Ask anything about NVIDIA..."): |
| st.session_state.messages.append({"role": "user", "content": prompt}) |
| with st.chat_message("user"): |
| st.markdown(prompt) |
|
|
| with st.chat_message("assistant"): |
| with st.spinner("Thinking..."): |
| try: |
| result = app.invoke({"query": prompt, "response": "", "debug_log": "", "next_node": ""}) |
| answer = result.get("response", "Sorry, I couldn't generate an answer.") |
| log = result.get("debug_log", "No debug info") |
| except Exception as exc: |
| answer = f"Agent error: {exc}" |
| log = f"Error: {exc}" |
|
|
| st.markdown(answer) |
| with st.expander("Debug Trace"): |
| st.code(log) |
|
|
| st.session_state.messages.append({"role": "assistant", "content": answer}) |
|
|
| with st.sidebar: |
| st.success("Prophet v3 residual ensemble ready") |
| st.success("News/price correlation agent ready") |
| st.success("RAG ready" if vectorstore else "RAG unavailable") |
| st.success("DuckDuckGo ready") |
| if st.button("Clear Chat"): |
| st.session_state.messages = [] |
| st.rerun() |
|
|