Ardaarslan02 commited on
Commit
736730d
·
verified ·
1 Parent(s): f3bfc16

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +28 -0
  2. agents.py +41 -0
  3. app.py +267 -0
  4. requirements.txt +32 -0
  5. tasks.py +160 -0
  6. 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