cardinal1 / agents /rag_analyzer.py
don-unagi's picture
add vector store
8d8e17a
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain.schema import StrOutputParser
from operator import itemgetter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from .state import AgentState
import os
# Initialize embedding model
embeddings = HuggingFaceEmbeddings(model_name="Snowflake/snowflake-arctic-embed-l")
# Initialize Vector DB
vector_db = Chroma(
embedding_function=embeddings,
persist_directory="./chroma_db",
collection_name="Security_Analysis" # Explicitly use the Security_Analysis collection
)
# Define RAG prompt template for technical analysis interpretation
TECHNICAL_RAG_PROMPT = """
CONTEXT:
{context}
TECHNICAL INDICATORS:
{technical_data}
USER PORTFOLIO INFORMATION:
Risk Level: {risk_level}
Investment Goals: {investment_goals}
You are a financial advisor with expertise in both technical and fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd along with the technical indicators to provide insights on the stock.
For each stock, analyze:
1. What the technical indicators suggest about current market sentiment and potential price movements
2. How the fundamental data aligns with value investing principles
3. Whether the stock appears to be undervalued, fairly valued, or overvalued
4. How this stock fits within the user's risk profile and investment goals
Provide a balanced analysis that considers both technical signals and fundamental principles.
"""
technical_rag_prompt = ChatPromptTemplate.from_template(TECHNICAL_RAG_PROMPT)
# Define RAG prompt template for portfolio insights
PORTFOLIO_RAG_PROMPT = """
CONTEXT:
{context}
USER PORTFOLIO INFORMATION:
Risk Level: {risk_level}
Investment Goals: {investment_goals}
Portfolio: {portfolio}
You are a financial advisor with expertise in fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd to provide insights on the user's portfolio and investment strategy.
Focus on applying fundamental analysis principles to the user's specific situation.
Answer the following questions:
1. What would Graham and Dodd recommend for this investor with their risk level and goals?
2. What fundamental analysis principles should be applied to this portfolio?
3. How should this investor think about value investing in today's market?
Provide your answers in a clear, concise format.
"""
portfolio_rag_prompt = ChatPromptTemplate.from_template(PORTFOLIO_RAG_PROMPT)
# Define a batch analysis prompt for multiple stocks
BATCH_ANALYSIS_PROMPT = """
CONTEXT:
{context}
USER PORTFOLIO INFORMATION:
Risk Level: {risk_level}
Investment Goals: {investment_goals}
STOCKS TO ANALYZE:
{stocks_data}
You are a financial advisor with expertise in both technical and fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd along with the technical indicators to provide insights on each stock.
For each stock, provide a brief analysis that includes:
1. What the technical indicators suggest about current market sentiment
2. Whether the stock appears to be undervalued, fairly valued, or overvalued
3. How this stock fits within the user's risk profile and investment goals
Format your response as a JSON-like structure with ticker symbols as keys and analysis as values.
"""
batch_analysis_prompt = ChatPromptTemplate.from_template(BATCH_ANALYSIS_PROMPT)
def setup_technical_rag_chain():
"""Set up the RAG chain for technical analysis interpretation."""
# Initialize LLM - using a faster model for individual stock analysis
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)
# Create retriever
retriever = vector_db.as_retriever(search_kwargs={"k": 3})
# Create RAG chain for technical analysis
technical_rag_chain = (
{
"context": lambda x: retriever.invoke("technical analysis indicators interpretation value investing"),
"technical_data": itemgetter("technical_data"),
"risk_level": itemgetter("risk_level"),
"investment_goals": itemgetter("investment_goals")
}
| technical_rag_prompt
| llm
| StrOutputParser()
)
return technical_rag_chain
def setup_portfolio_rag_chain():
"""Set up the RAG chain for portfolio insights."""
# Initialize LLM - using GPT-4o-mini for better performance while maintaining quality
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
# Create retriever
retriever = vector_db.as_retriever(search_kwargs={"k": 5})
# Create RAG chain for portfolio insights
portfolio_rag_chain = (
{
"context": lambda x: retriever.invoke("value investing portfolio analysis Graham Dodd"),
"risk_level": itemgetter("risk_level"),
"investment_goals": itemgetter("investment_goals"),
"portfolio": itemgetter("portfolio")
}
| portfolio_rag_prompt
| llm
| StrOutputParser()
)
return portfolio_rag_chain
def setup_batch_analysis_chain():
"""Set up the RAG chain for batch stock analysis."""
# Initialize LLM - using GPT-4o-mini for better performance while maintaining quality
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
# Create retriever
retriever = vector_db.as_retriever(search_kwargs={"k": 4})
# Create RAG chain for batch analysis
batch_analysis_chain = (
{
"context": lambda x: retriever.invoke("technical analysis fundamental analysis value investing"),
"stocks_data": itemgetter("stocks_data"),
"risk_level": itemgetter("risk_level"),
"investment_goals": itemgetter("investment_goals")
}
| batch_analysis_prompt
| llm
| StrOutputParser()
)
return batch_analysis_chain
# Initialize the RAG chains
technical_rag_chain = setup_technical_rag_chain()
portfolio_rag_chain = setup_portfolio_rag_chain()
batch_analysis_chain = setup_batch_analysis_chain()
def get_stock_interpretation(technical_data_str, risk_level, investment_goals):
"""Get RAG-based interpretation for a stock's technical data."""
return technical_rag_chain.invoke({
"technical_data": technical_data_str,
"risk_level": risk_level,
"investment_goals": investment_goals
})
def get_portfolio_insights(portfolio_str, risk_level, investment_goals):
"""Get RAG-based insights for a portfolio."""
return portfolio_rag_chain.invoke({
"risk_level": risk_level,
"investment_goals": investment_goals,
"portfolio": portfolio_str
})
def rag_analyzer(state: AgentState) -> AgentState:
"""Performs RAG-based analysis on technical data and portfolio."""
portfolio = state["portfolio_data"]
risk_level = state["risk_level"]
investment_goals = state["investment_goals"]
technical_analysis = state.get("technical_analysis", {})
# Format portfolio for RAG
portfolio_str = ""
for ticker, data in portfolio.items():
portfolio_str += f"{ticker}: {data['shares']} shares at ${data['purchase_price']:.2f}, "
portfolio_str += f"current value: ${data['value']:.2f}, "
portfolio_str += f"gain/loss: {data['gain_loss_pct']:.2f}%\n"
# Generate portfolio-level insights using RAG (single call instead of multiple)
portfolio_insights_text = get_portfolio_insights(
portfolio_str,
risk_level,
investment_goals
)
# Parse the portfolio insights into structured format
lines = portfolio_insights_text.strip().split('\n')
portfolio_insights = []
# Extract questions and answers from the response
current_question = None
current_answer = []
for line in lines:
if line.startswith('1.') or line.startswith('2.') or line.startswith('3.'):
# If we have a previous question, save it
if current_question is not None:
portfolio_insights.append({
"question": current_question,
"response": '\n'.join(current_answer).strip()
})
current_answer = []
# Extract new question
parts = line.split(':', 1)
if len(parts) > 1:
current_question = parts[0].strip()
current_answer.append(parts[1].strip())
else:
current_question = line.strip()
elif current_question is not None:
current_answer.append(line)
# Add the last question/answer if exists
if current_question is not None and current_answer:
portfolio_insights.append({
"question": current_question,
"response": '\n'.join(current_answer).strip()
})
# If we couldn't parse properly, create a fallback structure
if not portfolio_insights:
portfolio_insights = [
{"question": "Portfolio Analysis", "response": portfolio_insights_text}
]
# Only process technical analysis if there are stocks to analyze
if technical_analysis:
# Prepare batch analysis data instead of individual calls
stocks_data = ""
for ticker, data in technical_analysis.items():
if "error" not in data:
stocks_data += f"Stock: {ticker}\n"
stocks_data += f"Current Price: ${data['current_price']:.2f}\n"
stocks_data += "Technical Indicators:\n"
for key, value in data['technical_indicators'].items():
stocks_data += f" {key}: {value}\n"
stocks_data += "Fundamental Data:\n"
for key, value in data['fundamental_data'].items():
if value is not None:
stocks_data += f" {key}: {value}\n"
stocks_data += "\n---\n\n"
# Only make the batch analysis call if we have stocks to analyze
if stocks_data:
try:
# Get batch analysis for all stocks in one call
batch_analysis_result = batch_analysis_chain.invoke({
"stocks_data": stocks_data,
"risk_level": risk_level,
"investment_goals": investment_goals
})
# Try to parse as JSON first
try:
import json
import re
# Try to find JSON-like content in the response using regex
json_match = re.search(r'\{[\s\S]*\}', batch_analysis_result)
if json_match:
json_str = json_match.group(0)
analysis_data = json.loads(json_str)
# Update technical_analysis with the parsed JSON
for ticker, analysis in analysis_data.items():
if ticker in technical_analysis:
technical_analysis[ticker]["rag_interpretation"] = analysis
else:
# Fallback to the original text parsing approach
current_ticker = None
current_analysis = []
for line in batch_analysis_result.split('\n'):
if ':' in line and line.split(':')[0].strip() in technical_analysis:
# If we have a previous ticker, save its analysis
if current_ticker is not None and current_ticker in technical_analysis:
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip()
current_analysis = []
# Start new ticker
current_ticker = line.split(':')[0].strip()
current_analysis.append(line.split(':', 1)[1].strip())
elif current_ticker is not None:
current_analysis.append(line)
# Add the last ticker's analysis
if current_ticker is not None and current_ticker in technical_analysis:
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip()
except (json.JSONDecodeError, Exception) as json_err:
print(f"Error parsing JSON response: {str(json_err)}")
# Log the raw response for debugging
print(f"Raw response: {batch_analysis_result}")
# Fallback to old approach if JSON parsing fails
current_ticker = None
current_analysis = []
for line in batch_analysis_result.split('\n'):
if ':' in line and line.split(':')[0].strip() in technical_analysis:
# If we have a previous ticker, save its analysis
if current_ticker is not None and current_ticker in technical_analysis:
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip()
current_analysis = []
# Start new ticker
current_ticker = line.split(':')[0].strip()
current_analysis.append(line.split(':', 1)[1].strip())
elif current_ticker is not None:
current_analysis.append(line)
# Add the last ticker's analysis
if current_ticker is not None and current_ticker in technical_analysis:
technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip()
except Exception as e:
# Fallback to a simpler approach if batch processing fails
import traceback
print(f"Error in batch analysis: {str(e)}")
print(traceback.format_exc())
for ticker, data in technical_analysis.items():
if "error" not in data:
technical_analysis[ticker]["rag_interpretation"] = f"Analysis unavailable due to processing error: {str(e)}"
# Combine portfolio insights
combined_insights = "\n\n".join([f"Q: {insight['question']}\nA: {insight['response']}" for insight in portfolio_insights])
# Update state with RAG insights
state["technical_analysis"] = technical_analysis
state["rag_context"] = combined_insights
state["fundamental_analysis"] = state.get("fundamental_analysis", {})
state["fundamental_analysis"]["security_analysis_insights"] = portfolio_insights
# Add message to communication
state["messages"] = state.get("messages", []) + [{
"role": "ai",
"content": f"[RAGAnalyzer] I've provided insights on your portfolio and individual stocks based on value investing principles from Security Analysis by Graham and Dodd."
}]
return state