Spaces:
Runtime error
Runtime error
Upload 6 files
Browse files- Dockerfile +28 -0
- agents.py +41 -0
- app.py +267 -0
- requirements.txt +32 -0
- tasks.py +160 -0
- utils.py +169 -0
Dockerfile
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Python runtime as the base image
|
2 |
+
FROM python:3.11-slim
|
3 |
+
|
4 |
+
# Set working directory in the container
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy project files
|
8 |
+
COPY app.py utils.py agents.py tasks.py requirements.txt ./
|
9 |
+
|
10 |
+
# Install system dependencies
|
11 |
+
RUN apt-get update && apt-get install -y \
|
12 |
+
gcc \
|
13 |
+
libffi-dev \
|
14 |
+
&& rm -rf /var/lib/apt/lists/*
|
15 |
+
|
16 |
+
# Install Python dependencies
|
17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
18 |
+
|
19 |
+
# Expose port 7860 for Gradio
|
20 |
+
EXPOSE 7860
|
21 |
+
|
22 |
+
# Set environment variables
|
23 |
+
ENV CREWAI_TELEMETRY_ENABLED=false
|
24 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
25 |
+
ENV GRADIO_SERVER_PORT=7860
|
26 |
+
|
27 |
+
# Command to run the Gradio app
|
28 |
+
CMD ["python", "app.py"]
|
agents.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# agents.py
|
2 |
+
|
3 |
+
from utils import gemini_llm
|
4 |
+
from crewai import Agent
|
5 |
+
|
6 |
+
# Agents
|
7 |
+
finance_knowledge_agent = Agent(
|
8 |
+
role="Finance Knowledge Expert",
|
9 |
+
goal="Provide accurate, concise, and structured answers to general finance-related questions using provided documents and web data.",
|
10 |
+
backstory="An expert with deep knowledge of financial concepts, trained on documents including Basics.pdf, Statementanalysis.pdf, and Financialterms.pdf.",
|
11 |
+
llm=gemini_llm,
|
12 |
+
verbose=True,
|
13 |
+
allow_delegation=False
|
14 |
+
)
|
15 |
+
|
16 |
+
market_news_agent = Agent(
|
17 |
+
role="Market News Analyst",
|
18 |
+
goal="Fetch, summarize, and analyze recent financial news and market trends to provide actionable insights.",
|
19 |
+
backstory="A financial journalist with expertise in identifying key market trends and summarizing news for actionable insights.",
|
20 |
+
llm=gemini_llm,
|
21 |
+
verbose=True,
|
22 |
+
allow_delegation=False
|
23 |
+
)
|
24 |
+
|
25 |
+
stock_analysis_agent = Agent(
|
26 |
+
role="Stock Analysis Expert",
|
27 |
+
goal="Provide detailed and actionable analysis of specific stocks, including performance trends and basic technical insights.",
|
28 |
+
backstory="A seasoned stock market analyst with expertise in fundamental analysis and basic trend interpretation based on real-time data.",
|
29 |
+
llm=gemini_llm,
|
30 |
+
verbose=True,
|
31 |
+
allow_delegation=False
|
32 |
+
)
|
33 |
+
|
34 |
+
response_refiner_agent = Agent(
|
35 |
+
role="Response Refiner and Reporter",
|
36 |
+
goal="Simplify, verify, and format responses from other agents into a concise, professional report for the user.",
|
37 |
+
backstory="A meticulous editor with a background in finance, specializing in simplifying complex information and presenting it in a clear, professional report format.",
|
38 |
+
llm=gemini_llm,
|
39 |
+
verbose=True,
|
40 |
+
allow_delegation=False
|
41 |
+
)
|
app.py
ADDED
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# interface.py
|
2 |
+
|
3 |
+
import gradio as gr
|
4 |
+
from crewai import Crew, Process
|
5 |
+
from agents import finance_knowledge_agent, market_news_agent, stock_analysis_agent, response_refiner_agent
|
6 |
+
from tasks import get_finance_knowledge_task, get_market_news_task, get_stock_analysis_task, get_response_refiner_task
|
7 |
+
from utils import determine_question_type, search_qdrant
|
8 |
+
|
9 |
+
# Initialize Crew
|
10 |
+
finance_crew = Crew(
|
11 |
+
agents=[finance_knowledge_agent, market_news_agent, stock_analysis_agent, response_refiner_agent],
|
12 |
+
tasks=[],
|
13 |
+
process=Process.sequential,
|
14 |
+
verbose=1
|
15 |
+
)
|
16 |
+
|
17 |
+
def get_response(query):
|
18 |
+
"""Get chatbot response."""
|
19 |
+
finance_crew.tasks = [] # Reset tasks for each query
|
20 |
+
|
21 |
+
try:
|
22 |
+
question_type, processed_query = determine_question_type(query)
|
23 |
+
|
24 |
+
# Determine RAG usage
|
25 |
+
rag_note = "RAG_SUFFICIENT" # Default for finance_knowledge
|
26 |
+
if question_type == "finance_knowledge":
|
27 |
+
contexts = search_qdrant(query, top_k=2)
|
28 |
+
if contexts and len(contexts) > 0:
|
29 |
+
shortened_contexts = []
|
30 |
+
for ctx in contexts:
|
31 |
+
text = ctx["text"]
|
32 |
+
if len(text) > 300:
|
33 |
+
text = text[:297] + "..."
|
34 |
+
shortened_contexts.append({
|
35 |
+
"source": ctx["source"],
|
36 |
+
"text": text
|
37 |
+
})
|
38 |
+
context_text = "\n\n".join([f"Source: {ctx['source']}\nContent: {ctx['text']}" for ctx in shortened_contexts])
|
39 |
+
is_context_useful = len(context_text) > 30 and any(keyword in context_text.lower() for keyword in query.lower().split())
|
40 |
+
rag_note = "RAG_SUFFICIENT" if is_context_useful else "RAG_NOT_USED"
|
41 |
+
else:
|
42 |
+
rag_note = "RAG_NOT_USED"
|
43 |
+
initial_task = get_finance_knowledge_task(query)
|
44 |
+
elif question_type == "market_news":
|
45 |
+
rag_note = "NO_RAG_NEEDED"
|
46 |
+
initial_task = get_market_news_task(query)
|
47 |
+
elif question_type == "stock_analysis":
|
48 |
+
rag_note = "NO_RAG_NEEDED"
|
49 |
+
initial_task = get_stock_analysis_task(processed_query)
|
50 |
+
else:
|
51 |
+
initial_task = get_finance_knowledge_task(query)
|
52 |
+
|
53 |
+
finance_crew.tasks.append(initial_task)
|
54 |
+
initial_response = finance_crew.kickoff()
|
55 |
+
|
56 |
+
refiner_task = get_response_refiner_task(query, initial_response, question_type, rag_note=rag_note)
|
57 |
+
finance_crew.tasks = [refiner_task]
|
58 |
+
final_report = finance_crew.kickoff()
|
59 |
+
|
60 |
+
return final_report
|
61 |
+
except Exception as e:
|
62 |
+
return f"Error: {e}\nPlease try again."
|
63 |
+
|
64 |
+
# CSS
|
65 |
+
custom_css = """
|
66 |
+
:root {
|
67 |
+
--primary-color: #00416a;
|
68 |
+
--secondary-color: #047fb7;
|
69 |
+
--title-color: #FFD700;
|
70 |
+
--text-color: #ffffff;
|
71 |
+
--bg-dark: #1a1a1a;
|
72 |
+
--bg-medium: #2a2a2a;
|
73 |
+
--bg-light: #353535;
|
74 |
+
--input-bg: #ffffff;
|
75 |
+
--input-text: #333333;
|
76 |
+
--button-green: #4CAF50;
|
77 |
+
}
|
78 |
+
body, html {
|
79 |
+
background-color: #1a1a1a !important;
|
80 |
+
margin: 0;
|
81 |
+
padding: 0;
|
82 |
+
width: 100%;
|
83 |
+
height: 100%;
|
84 |
+
overflow-x: hidden;
|
85 |
+
}
|
86 |
+
.gradio-container {
|
87 |
+
background: linear-gradient(135deg, #1a1a1a 0%, #00416a 100%) !important;
|
88 |
+
font-family: 'Montserrat', 'Arial', sans-serif !important;
|
89 |
+
max-width: 100% !important;
|
90 |
+
width: 100% !important;
|
91 |
+
margin: 0 !important;
|
92 |
+
padding: 25px !important;
|
93 |
+
min-height: 100vh !important;
|
94 |
+
}
|
95 |
+
.gradio-container .input-box,
|
96 |
+
.gradio-container .output-box,
|
97 |
+
.gr-form,
|
98 |
+
.gr-box,
|
99 |
+
.gr-padded,
|
100 |
+
.gr-panel,
|
101 |
+
.gr-input,
|
102 |
+
.gr-input-label,
|
103 |
+
textarea,
|
104 |
+
.gr-textarea,
|
105 |
+
.gr-textbox {
|
106 |
+
background-color: #ffffff !important;
|
107 |
+
}
|
108 |
+
textarea,
|
109 |
+
.gr-textarea textarea,
|
110 |
+
.gr-textbox textarea,
|
111 |
+
.gr-textbox input {
|
112 |
+
background-color: #ffffff !important;
|
113 |
+
color: #333333 !important;
|
114 |
+
}
|
115 |
+
.gr-button.submit-button {
|
116 |
+
background-color: #4CAF50 !important;
|
117 |
+
color: #ffffff !important;
|
118 |
+
border: none !important;
|
119 |
+
padding: 10px 20px !important;
|
120 |
+
border-radius: 8px !important;
|
121 |
+
font-weight: bold !important;
|
122 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2) !important;
|
123 |
+
text-align: center !important;
|
124 |
+
width: 150px !important;
|
125 |
+
margin: 10px auto !important;
|
126 |
+
display: block !important;
|
127 |
+
}
|
128 |
+
button,
|
129 |
+
.gr-button span,
|
130 |
+
.submit-button span {
|
131 |
+
color: #ffffff !important;
|
132 |
+
background-color: transparent !important;
|
133 |
+
}
|
134 |
+
.examples-title,
|
135 |
+
.gr-examples .gr-interface-title,
|
136 |
+
footer div,
|
137 |
+
footer span {
|
138 |
+
color: #ffffff !important;
|
139 |
+
background-color: transparent !important;
|
140 |
+
font-weight: bold !important;
|
141 |
+
}
|
142 |
+
.gr-box {
|
143 |
+
border-radius: 8px !important;
|
144 |
+
padding: 12px !important;
|
145 |
+
background-color: #ffffff !important;
|
146 |
+
border: 1px solid #047fb7 !important;
|
147 |
+
}
|
148 |
+
label, .gr-block.gr-box label {
|
149 |
+
color: #ffffff !important;
|
150 |
+
font-weight: bold !important;
|
151 |
+
background-color: transparent !important;
|
152 |
+
}
|
153 |
+
.gr-examples .gr-sample-btn {
|
154 |
+
background-color: #e0e0e0 !important;
|
155 |
+
border: 1px solid #047fb7 !important;
|
156 |
+
color: #333333 !important;
|
157 |
+
border-radius: 8px !important;
|
158 |
+
}
|
159 |
+
h1 {
|
160 |
+
color: #FFD700 !important;
|
161 |
+
text-align: center !important;
|
162 |
+
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3) !important;
|
163 |
+
font-weight: bold !important;
|
164 |
+
}
|
165 |
+
h3 {
|
166 |
+
color: #ffffff !important;
|
167 |
+
text-align: center !important;
|
168 |
+
}
|
169 |
+
.finance-icons {
|
170 |
+
text-align: center !important;
|
171 |
+
color: #ffffff !important;
|
172 |
+
}
|
173 |
+
.ticker-tape {
|
174 |
+
background-color: #353535 !important;
|
175 |
+
overflow: hidden !important;
|
176 |
+
white-space: nowrap !important;
|
177 |
+
padding: 8px 0 !important;
|
178 |
+
margin: 15px 0 !important;
|
179 |
+
border-radius: 5px !important;
|
180 |
+
}
|
181 |
+
.ticker-content {
|
182 |
+
display: inline-block !important;
|
183 |
+
animation: ticker 30s linear infinite !important;
|
184 |
+
color: #ffffff !important;
|
185 |
+
}
|
186 |
+
@keyframes ticker {
|
187 |
+
0% { transform: translateX(100%); }
|
188 |
+
100% { transform: translateX(-100%); }
|
189 |
+
}
|
190 |
+
.stock-symbol {
|
191 |
+
margin: 0 15px !important;
|
192 |
+
display: inline-block !important;
|
193 |
+
color: #ffffff !important;
|
194 |
+
font-weight: bold !important;
|
195 |
+
}
|
196 |
+
.up { color: #4CAF50 !important; }
|
197 |
+
.down { color: #FF5252 !important; }
|
198 |
+
.examples {
|
199 |
+
background-color: #ffffff !important;
|
200 |
+
border-radius: 8px !important;
|
201 |
+
padding: 10px !important;
|
202 |
+
}
|
203 |
+
.footer-container, .footer, .gr-footnote {
|
204 |
+
display: none !important;
|
205 |
+
}
|
206 |
+
.gradio-container .prose,
|
207 |
+
.gradio-container .prose p {
|
208 |
+
background-color: transparent !important;
|
209 |
+
}
|
210 |
+
"""
|
211 |
+
|
212 |
+
# HTML components for design
|
213 |
+
ticker_html = """
|
214 |
+
<div class="ticker-tape">
|
215 |
+
<div class="ticker-content">
|
216 |
+
<span class="stock-symbol">AAPL <span class="up">+1.2%</span></span>
|
217 |
+
<span class="stock-symbol">MSFT <span class="up">+0.8%</span></span>
|
218 |
+
<span class="stock-symbol">GOOGL <span class="down">-0.4%</span></span>
|
219 |
+
<span class="stock-symbol">AMZN <span class="up">+2.1%</span></span>
|
220 |
+
<span class="stock-symbol">TSLA <span class="down">-1.7%</span></span>
|
221 |
+
<span class="stock-symbol">JPM <span class="up">+0.5%</span></span>
|
222 |
+
<span class="stock-symbol">BAC <span class="down">-0.2%</span></span>
|
223 |
+
<span class="stock-symbol">WMT <span class="up">+0.3%</span></span>
|
224 |
+
</div>
|
225 |
+
</div>
|
226 |
+
"""
|
227 |
+
|
228 |
+
finance_icons = """
|
229 |
+
<div class="finance-icons">
|
230 |
+
<span style="color: #4CAF50;">📊 📈 💹 💰 📉 🏦 📃 </span>
|
231 |
+
</div>
|
232 |
+
"""
|
233 |
+
|
234 |
+
# Gradio interface
|
235 |
+
with gr.Blocks(css=custom_css, theme=gr.themes.Base(), analytics_enabled=False) as interface:
|
236 |
+
gr.HTML("<h1>Professional Finance Assistant</h1>")
|
237 |
+
gr.HTML("<h3>Your AI-powered financial advisor and market analyst</h3>")
|
238 |
+
|
239 |
+
gr.HTML(finance_icons)
|
240 |
+
gr.HTML(ticker_html)
|
241 |
+
|
242 |
+
with gr.Row():
|
243 |
+
with gr.Column(scale=1):
|
244 |
+
input_text = gr.Textbox(
|
245 |
+
lines=4,
|
246 |
+
placeholder="Ask me anything about finance (e.g., 'What is dividend investing?', 'Analyze Tesla stock', 'Latest market news on tech sector')",
|
247 |
+
label="Your Financial Query",
|
248 |
+
show_label=True,
|
249 |
+
interactive=True
|
250 |
+
)
|
251 |
+
submit_btn = gr.Button("Submit", elem_classes=["submit-button"])
|
252 |
+
|
253 |
+
output_text = gr.Textbox(label="Financial Analysis", lines=10)
|
254 |
+
|
255 |
+
example_queries = [
|
256 |
+
["What is the difference between bull and bear markets?"],
|
257 |
+
["Analyze AAPL stock performance"],
|
258 |
+
["Latest news about cryptocurrency market"],
|
259 |
+
["Explain P/E ratio and its importance"]
|
260 |
+
]
|
261 |
+
gr.Examples(example_queries, inputs=input_text, examples_per_page=5, label="Examples")
|
262 |
+
|
263 |
+
# Event handlers
|
264 |
+
submit_btn.click(fn=get_response, inputs=input_text, outputs=output_text)
|
265 |
+
|
266 |
+
# Launch the interface
|
267 |
+
interface.launch(share=False, inbrowser=True)
|
requirements.txt
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Core dependencies
|
2 |
+
python-dotenv # For loading environment variables from .env file
|
3 |
+
pypdf # For PDF parsing
|
4 |
+
numpy # For numerical operations
|
5 |
+
|
6 |
+
# Embeddings
|
7 |
+
sentence-transformers # For generating embeddings using HuggingFace models
|
8 |
+
|
9 |
+
# Qdrant integration
|
10 |
+
qdrant-client # Qdrant vector database client
|
11 |
+
langchain-qdrant # LangChain integration for Qdrant
|
12 |
+
|
13 |
+
# LangChain and related
|
14 |
+
langchain # Core LangChain library
|
15 |
+
langchain-community # Community-contributed LangChain modules
|
16 |
+
langchain_huggingface # LangChain integration for HuggingFace embeddings
|
17 |
+
|
18 |
+
# CrewAI for agent-based workflows
|
19 |
+
crewai # Framework for multi-agent workflows
|
20 |
+
|
21 |
+
# Web and data handling
|
22 |
+
requests # For making HTTP requests (e.g., Serper API, Alpha Vantage API)
|
23 |
+
pandas # For data manipulation and analysis
|
24 |
+
|
25 |
+
# Jupyter (if running notebook locally)
|
26 |
+
jupyter # For running Jupyter notebooks (e.g., setup_qdrant.ipynb)
|
27 |
+
|
28 |
+
# Web interface
|
29 |
+
gradio # For creating web interfaces
|
30 |
+
|
31 |
+
# Production server
|
32 |
+
gunicorn # For running the app in production
|
tasks.py
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# tasks.py
|
2 |
+
|
3 |
+
from utils import search_qdrant, search_news, get_stock_data
|
4 |
+
from crewai import Task
|
5 |
+
from agents import finance_knowledge_agent, market_news_agent, stock_analysis_agent, response_refiner_agent
|
6 |
+
|
7 |
+
def get_finance_knowledge_task(query):
|
8 |
+
"""Task for answering general finance knowledge questions."""
|
9 |
+
contexts = search_qdrant(query, top_k=3)
|
10 |
+
context_text = "\n\n".join([f"Source: {ctx['source']}\nContent: {ctx['text']}" for ctx in contexts])
|
11 |
+
is_context_useful = len(context_text) > 50 and any(query.lower() in ctx["text"].lower() for ctx in contexts)
|
12 |
+
|
13 |
+
web_results = search_news(query, max_results=3)
|
14 |
+
web_text = "\n\n".join([f"Title: {item['title']}\nSummary: {item['snippet']}" for item in web_results]) if web_results else "No additional info from the web."
|
15 |
+
|
16 |
+
if is_context_useful:
|
17 |
+
prompt = f"""
|
18 |
+
User query: '{query}'
|
19 |
+
|
20 |
+
You are a Finance Knowledge Expert. Use the following RAG data and web search results to provide a concise, accurate response:
|
21 |
+
|
22 |
+
RAG Data:
|
23 |
+
{context_text}
|
24 |
+
|
25 |
+
Web Search Results:
|
26 |
+
{web_text}
|
27 |
+
|
28 |
+
### Instructions:
|
29 |
+
- Provide a clear definition or explanation related to the query.
|
30 |
+
- Include a practical example or implication if relevant.
|
31 |
+
- Cite your sources (e.g., "According to Basics.pdf" or "Based on web search").
|
32 |
+
- Keep the response concise, under 200 words.
|
33 |
+
"""
|
34 |
+
else:
|
35 |
+
prompt = f"""
|
36 |
+
User query: '{query}'
|
37 |
+
|
38 |
+
The RAG data from Qdrant was insufficient:
|
39 |
+
{context_text}
|
40 |
+
|
41 |
+
Web search results:
|
42 |
+
{web_text}
|
43 |
+
|
44 |
+
### Instructions:
|
45 |
+
- Rely on your own knowledge and web results to provide a concise, accurate response.
|
46 |
+
- Note that RAG data was insufficient.
|
47 |
+
- Include a practical example or implication if relevant.
|
48 |
+
- Cite your sources (e.g., "Based on web search").
|
49 |
+
- Keep the response concise, under 200 words.
|
50 |
+
"""
|
51 |
+
return Task(
|
52 |
+
description=prompt,
|
53 |
+
agent=finance_knowledge_agent,
|
54 |
+
expected_output="A concise explanation of the financial concept, with an example and cited sources, under 200 words."
|
55 |
+
)
|
56 |
+
|
57 |
+
def get_market_news_task(query):
|
58 |
+
"""Task for summarizing and analyzing market news."""
|
59 |
+
news = search_news(query, max_results=3)
|
60 |
+
news_text = "\n\n".join([f"Title: {item['title']}\nSummary: {item['snippet']}" for item in news]) if news else "No recent news found."
|
61 |
+
|
62 |
+
prompt = f"""
|
63 |
+
User query: '{query}'
|
64 |
+
|
65 |
+
You are a Market News Analyst. Analyze the following news data and provide a summary with actionable insights:
|
66 |
+
|
67 |
+
News Data:
|
68 |
+
{news_text}
|
69 |
+
|
70 |
+
### Instructions:
|
71 |
+
- Summarize the key points from the news in 3-4 sentences.
|
72 |
+
- Highlight any trends or events that could impact the market.
|
73 |
+
- Provide one actionable insight or recommendation for investors.
|
74 |
+
- Cite the news sources (e.g., "According to [title]").
|
75 |
+
- Keep the response concise, under 200 words.
|
76 |
+
"""
|
77 |
+
return Task(
|
78 |
+
description=prompt,
|
79 |
+
agent=market_news_agent,
|
80 |
+
expected_output="A concise summary of market news, highlighting trends, with an actionable insight, under 200 words."
|
81 |
+
)
|
82 |
+
|
83 |
+
def get_stock_analysis_task(symbol):
|
84 |
+
"""Task for analyzing a specific stock with basic technical insights."""
|
85 |
+
stock_data = get_stock_data(symbol)
|
86 |
+
if "error" in stock_data:
|
87 |
+
prompt = f"""
|
88 |
+
User query: 'Analyze {symbol}'
|
89 |
+
|
90 |
+
You are a Stock Analysis Expert. There was an error fetching data for the stock:
|
91 |
+
|
92 |
+
Error: {stock_data['error']}
|
93 |
+
|
94 |
+
### Instructions:
|
95 |
+
- Provide a general overview of the stock based on your knowledge.
|
96 |
+
- Suggest a potential reason for the error.
|
97 |
+
- Recommend an action for the user.
|
98 |
+
- Keep the response concise, under 200 words.
|
99 |
+
"""
|
100 |
+
else:
|
101 |
+
data_text = f"Price: {stock_data['price']}\nChange: {stock_data['change']} ({stock_data['change_percent']})"
|
102 |
+
prompt = f"""
|
103 |
+
User query: 'Analyze {symbol}'
|
104 |
+
|
105 |
+
You are a Stock Analysis Expert. Analyze the following stock data with basic technical insights:
|
106 |
+
|
107 |
+
Stock Data:
|
108 |
+
{data_text}
|
109 |
+
|
110 |
+
### Instructions:
|
111 |
+
- Interpret the stock's performance and identify any price trend (e.g., upward/downward movement).
|
112 |
+
- Identify potential factors influencing the stock (e.g., market trends, sector performance).
|
113 |
+
- Provide an investment recommendation (e.g., "Hold", "Buy", "Sell") with a brief rationale.
|
114 |
+
- Keep the response concise, under 200 words.
|
115 |
+
"""
|
116 |
+
return Task(
|
117 |
+
description=prompt,
|
118 |
+
agent=stock_analysis_agent,
|
119 |
+
expected_output="A concise analysis of the stock's performance with an investment recommendation, under 150 words."
|
120 |
+
)
|
121 |
+
|
122 |
+
def get_response_refiner_task(query, initial_response, question_type, rag_note="NO_RAG_NEEDED"):
|
123 |
+
"""Task for refining and reporting the response."""
|
124 |
+
|
125 |
+
# Create a special note for RAG information
|
126 |
+
rag_message = ""
|
127 |
+
if rag_note == "RAG_NOT_USED":
|
128 |
+
rag_message = "Note: No relevant information found in RAG system, web search results were used."
|
129 |
+
elif rag_note == "RAG_LIMITED":
|
130 |
+
# Don't show any special message when RAG and web search are used together
|
131 |
+
rag_message = ""
|
132 |
+
elif rag_note == "RAG_SUFFICIENT":
|
133 |
+
# Don't show any special message when RAG is sufficient
|
134 |
+
rag_message = ""
|
135 |
+
|
136 |
+
# Task for refining finance response
|
137 |
+
prompt = f"""
|
138 |
+
User query: '{query}'
|
139 |
+
Initial Response: '{initial_response}'
|
140 |
+
Question Type: '{question_type}'
|
141 |
+
|
142 |
+
{rag_message}
|
143 |
+
|
144 |
+
You are a Response Refiner and Reporter. Refine the initial response and present it in a professional report format.
|
145 |
+
|
146 |
+
### Instructions:
|
147 |
+
- Simplify the language for a general audience.
|
148 |
+
- Verify accuracy and logical alignment with the query; add a note if issues are found.
|
149 |
+
- Format as a report:
|
150 |
+
**Financial Report for Query: '{query}'**
|
151 |
+
- **Summary**: [Simplified summary in 3-4 sentences]
|
152 |
+
- **Key Insight**: [One key takeaway or recommendation]
|
153 |
+
- **Source/Note**: [Cite source or add note]
|
154 |
+
- Keep under 200 words and do not tell word count.
|
155 |
+
"""
|
156 |
+
return Task(
|
157 |
+
description=prompt,
|
158 |
+
agent=response_refiner_agent,
|
159 |
+
expected_output="A simplified and professionally formatted report, under 200 words."
|
160 |
+
)
|
utils.py
ADDED
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# utils.py
|
2 |
+
|
3 |
+
import os
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
from langchain_qdrant import QdrantVectorStore
|
6 |
+
from langchain.embeddings import HuggingFaceEmbeddings
|
7 |
+
from crewai import Agent, Task, Crew, Process, LLM
|
8 |
+
import requests
|
9 |
+
from requests.exceptions import ConnectionError, Timeout, HTTPError
|
10 |
+
from functools import lru_cache
|
11 |
+
|
12 |
+
# Load environment variables from .env file
|
13 |
+
load_dotenv()
|
14 |
+
|
15 |
+
# Settings
|
16 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
17 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
18 |
+
COLLECTION_NAME = "finance-chatbot"
|
19 |
+
MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
|
20 |
+
ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY")
|
21 |
+
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
|
22 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
23 |
+
|
24 |
+
# Initialize embeddings
|
25 |
+
embeddings = HuggingFaceEmbeddings(model_name='sentence-transformers/all-MiniLM-L6-v2')
|
26 |
+
|
27 |
+
# Connect to the existing Qdrant collection
|
28 |
+
qdrant = QdrantVectorStore.from_existing_collection(
|
29 |
+
embedding=embeddings,
|
30 |
+
url=QDRANT_URL,
|
31 |
+
api_key=QDRANT_API_KEY,
|
32 |
+
collection_name=COLLECTION_NAME
|
33 |
+
)
|
34 |
+
|
35 |
+
# Initialize Mistral LLM
|
36 |
+
mistral_llm = LLM(model="mistral/mistral-large-latest", api_key=MISTRAL_API_KEY, temperature=0.7)
|
37 |
+
|
38 |
+
# Initialize Gemini LLM
|
39 |
+
gemini_llm = LLM( model="gemini/gemini-2.0-flash", gemini_llm=GEMINI_API_KEY, temperature=0.7)
|
40 |
+
|
41 |
+
# Functions
|
42 |
+
@lru_cache(maxsize=100)
|
43 |
+
def search_qdrant(query, top_k=3):
|
44 |
+
"""Search Qdrant for relevant documents."""
|
45 |
+
try:
|
46 |
+
retriever = qdrant.as_retriever(search_type="similarity", search_kwargs={"k": top_k})
|
47 |
+
results = retriever.invoke(query)
|
48 |
+
return [{"text": doc.page_content, "source": doc.metadata.get("source", "Unknown")} for doc in results]
|
49 |
+
except Exception:
|
50 |
+
return []
|
51 |
+
|
52 |
+
def search_news(query, max_results=5):
|
53 |
+
"""Search for recent financial news using Serper API."""
|
54 |
+
try:
|
55 |
+
url = "https://google.serper.dev/search"
|
56 |
+
headers = {
|
57 |
+
"X-API-KEY": SERPER_API_KEY,
|
58 |
+
"Content-Type": "application/json"
|
59 |
+
}
|
60 |
+
payload = {
|
61 |
+
"q": f"{query} finance news",
|
62 |
+
"num": max_results
|
63 |
+
}
|
64 |
+
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
65 |
+
response.raise_for_status()
|
66 |
+
data = response.json()
|
67 |
+
|
68 |
+
results = data.get("organic", [])
|
69 |
+
if not results:
|
70 |
+
return [{"title": "No recent news available", "url": "", "snippet": "Could not fetch news. Please try again later."}]
|
71 |
+
|
72 |
+
formatted_results = [
|
73 |
+
{
|
74 |
+
"title": item.get("title", ""),
|
75 |
+
"url": item.get("link", ""),
|
76 |
+
"snippet": item.get("snippet", "")
|
77 |
+
}
|
78 |
+
for item in results[:max_results]
|
79 |
+
]
|
80 |
+
return formatted_results
|
81 |
+
|
82 |
+
except ConnectionError:
|
83 |
+
return [{"title": "Connection Error", "url": "", "snippet": "Failed to connect to the news API. Please check your internet connection."}]
|
84 |
+
except Timeout:
|
85 |
+
return [{"title": "Timeout Error", "url": "", "snippet": "News API request timed out. Please try again later."}]
|
86 |
+
except HTTPError as e:
|
87 |
+
if response.status_code == 429:
|
88 |
+
return [{"title": "Rate Limit Exceeded", "url": "", "snippet": "Too many requests to the news API. Please try again later."}]
|
89 |
+
return [{"title": "HTTP Error", "url": "", "snippet": f"Failed to fetch news due to HTTP error: {e}"}]
|
90 |
+
except Exception:
|
91 |
+
return [{"title": "Error", "url": "", "snippet": "An unexpected error occurred while fetching news. Please try again later."}]
|
92 |
+
|
93 |
+
def get_stock_data(symbol):
|
94 |
+
"""Fetch stock data using Alpha Vantage API."""
|
95 |
+
try:
|
96 |
+
url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={ALPHA_VANTAGE_API_KEY}"
|
97 |
+
response = requests.get(url, timeout=10)
|
98 |
+
response.raise_for_status()
|
99 |
+
data = response.json().get("Global Quote", {})
|
100 |
+
if not data:
|
101 |
+
return {"symbol": symbol, "error": "No data found for this symbol."}
|
102 |
+
return {
|
103 |
+
"symbol": symbol,
|
104 |
+
"price": data.get("05. price", "N/A"),
|
105 |
+
"change": data.get("09. change", "N/A"),
|
106 |
+
"change_percent": data.get("10. change percent", "N/A")
|
107 |
+
}
|
108 |
+
except ConnectionError:
|
109 |
+
return {"symbol": symbol, "error": "Failed to connect to the stock API. Please check your internet connection."}
|
110 |
+
except Timeout:
|
111 |
+
return {"symbol": symbol, "error": "Stock API request timed out. Please try again later."}
|
112 |
+
except HTTPError as e:
|
113 |
+
if response.status_code == 429:
|
114 |
+
return {"symbol": symbol, "error": "Too many requests to the stock API. Please try again later."}
|
115 |
+
return {"symbol": symbol, "error": f"Failed to fetch stock data due to HTTP error: {e}"}
|
116 |
+
except Exception:
|
117 |
+
return {"symbol": symbol, "error": "An unexpected error occurred while fetching stock data. Please try again later."}
|
118 |
+
|
119 |
+
@lru_cache(maxsize=100)
|
120 |
+
def determine_question_type(query):
|
121 |
+
"""Determine the type of user query using Mistral LLM via CrewAI's task mechanism."""
|
122 |
+
prompt = f"""
|
123 |
+
Analyze the following user query and determine its category:
|
124 |
+
- finance_knowledge: General questions about financial terms, concepts, or strategies
|
125 |
+
- market_news: Questions about current market news, trends, or events
|
126 |
+
- stock_analysis: Questions about specific stock analysis (e.g., mentioning a stock ticker like AAPL)
|
127 |
+
|
128 |
+
Query: "{query}"
|
129 |
+
|
130 |
+
Provide your response in this format:
|
131 |
+
Category: <category>
|
132 |
+
Extra Data: <additional info, such as the stock ticker for stock_analysis, or the query itself>
|
133 |
+
"""
|
134 |
+
|
135 |
+
classifier_agent = Agent(
|
136 |
+
role="Query Classifier",
|
137 |
+
goal="Classify user queries into appropriate categories.",
|
138 |
+
backstory="An expert in natural language understanding, capable of analyzing queries and categorizing them accurately.",
|
139 |
+
llm=mistral_llm,
|
140 |
+
verbose=True,
|
141 |
+
allow_delegation=False
|
142 |
+
)
|
143 |
+
|
144 |
+
classifier_task = Task(
|
145 |
+
description=prompt,
|
146 |
+
agent=classifier_agent,
|
147 |
+
expected_output="A classification of the query in the format: Category: <category>\nExtra Data: <additional info>"
|
148 |
+
)
|
149 |
+
|
150 |
+
temp_crew = Crew(
|
151 |
+
agents=[classifier_agent],
|
152 |
+
tasks=[classifier_task],
|
153 |
+
process=Process.sequential,
|
154 |
+
verbose=False
|
155 |
+
)
|
156 |
+
|
157 |
+
try:
|
158 |
+
response = temp_crew.kickoff()
|
159 |
+
response_text = response.raw if hasattr(response, 'raw') else str(response)
|
160 |
+
lines = response_text.strip().split("\n")
|
161 |
+
if len(lines) < 2:
|
162 |
+
raise ValueError("Invalid response format from LLM")
|
163 |
+
category_line = lines[0].replace("Category: ", "").strip()
|
164 |
+
extra_data_line = lines[1].replace("Extra Data: ", "").strip()
|
165 |
+
if category_line not in ["finance_knowledge", "market_news", "stock_analysis"]:
|
166 |
+
raise ValueError(f"Invalid category: {category_line}")
|
167 |
+
return category_line, extra_data_line
|
168 |
+
except Exception:
|
169 |
+
return "finance_knowledge", query
|