File size: 18,298 Bytes
6d185db ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 0c292a6 ac0c47c f75e1de 2d33470 fcc2706 ac0c47c fcc2706 2d33470 ac0c47c f7f1228 ac0c47c 0c292a6 4e8fe76 0c292a6 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 4e8fe76 fcc2706 f75e1de fcc2706 2d33470 fcc2706 2d33470 fcc2706 f75e1de a6125d7 f75e1de ac0c47c 2d33470 fcc2706 2d33470 fcc2706 f75e1de ac0c47c fcc2706 ac0c47c f75e1de 2d33470 0c292a6 ac0c47c 0c292a6 fcc2706 ac0c47c fcc2706 ac0c47c 2d33470 fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c fcc2706 ac0c47c 428bf02 ac0c47c fcc2706 ac0c47c 0c292a6 ac0c47c 2d33470 f75e1de ac0c47c 2d33470 ac0c47c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
from flask import Flask, render_template, request, jsonify, send_from_directory, current_app, send_file,abort,make_response
from dotenv import load_dotenv
from flask_cors import CORS
import os
import asyncio
from functools import wraps
import logging
import weaviate
from openai import AsyncOpenAI
from config import COLLECTION_NAME
import re
import threading
import queue
import time
from weaviate.exceptions import WeaviateTimeoutError
from functools import lru_cache
from flask_talisman import Talisman
import concurrent.futures
import psutil
from collections import deque
# Get the absolute path of the directory containing app.py
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
Talisman(app, content_security_policy=None) # We'll define CSP separately
# Load environment variables
load_dotenv()
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Set up AsyncOpenAI client
openai_client = AsyncOpenAI(api_key=os.getenv('OPENAI_API_KEY'))
# Initialize Weaviate client
client = None
# Global variable to track connection status
connection_status = {"status": "Disconnected", "color": "red"}
# Add a new global variable to store conversation history
conversation_history = deque(maxlen=10) # Store last 10 exchanges
# Add Content Security Policy headers
@app.after_request
def add_csp_headers(response):
csp = (
"default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; "
"script-src 'self' https: 'unsafe-inline' 'unsafe-eval'; "
"style-src 'self' https: 'unsafe-inline'; "
"img-src 'self' data: https:; "
"connect-src 'self' https:; "
"font-src 'self' https:; "
"object-src 'none'; "
"media-src 'self' https:; "
"frame-src 'self' https:; "
"worker-src 'self' blob:; "
"form-action 'self'; "
"base-uri 'self'; "
"frame-ancestors 'self';"
)
response.headers['Content-Security-Policy'] = csp
return response
@lru_cache(maxsize=1)
def get_weaviate_client():
return weaviate.Client(
url=os.getenv('WCS_URL'),
auth_client_secret=weaviate.auth.AuthApiKey(os.getenv('WCS_API_KEY')),
additional_headers={
"X-OpenAI-Api-Key": os.getenv('OPENAI_API_KEY')
},
timeout_config=(5, 60) # (connect timeout, read timeout)
)
def get_or_create_client():
global client
if client is None:
client = get_weaviate_client()
return client
def initialize_weaviate_client(max_retries=3, retry_delay=5):
global connection_status
retries = 0
while retries < max_retries:
connection_status = {"status": "Connecting...", "color": "orange"}
try:
logger.info(f"Attempting to connect to Weaviate (Attempt {retries + 1}/{max_retries})")
client = get_or_create_client()
# Test the connection
client.schema.get()
connection_status = {"status": "Connected", "color": "green"}
logger.info("Successfully connected to Weaviate")
return connection_status
except Exception as e:
logger.error(f"Error connecting to Weaviate: {str(e)}")
connection_status = {"status": f"Error: {str(e)}", "color": "red"}
retries += 1
if retries < max_retries:
logger.info(f"Retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
else:
logger.error("Max retries reached. Could not connect to Weaviate.")
return connection_status
# Initialize Weaviate client in a separate thread
initialization_thread = threading.Thread(target=initialize_weaviate_client)
initialization_thread.start()
# Async-compatible caching decorator
def async_lru_cache(maxsize=1024):
cache = {}
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache:
if len(cache) >= maxsize:
cache.pop(next(iter(cache)))
cache[key] = await func(*args, **kwargs)
return cache[key]
return wrapper
return decorator
@async_lru_cache(maxsize=1000)
async def get_embedding(text):
response = await openai_client.embeddings.create(
input=text,
model="text-embedding-3-large"
)
return response.data[0].embedding
async def search_multimodal(query: str, limit: int = 30, alpha: float = 0.6):
logger.info(f"Starting multimodal search for query: {query}")
try:
query_vector = await get_embedding(query)
logger.info(f"Generated query embedding of length {len(query_vector)}")
response = await asyncio.to_thread(
client.query.get(COLLECTION_NAME, ["content_type", "source_document", "page_number",
"paragraph_number", "text", "image_path", "description", "table_content"])
.with_hybrid(query=query, vector=query_vector, alpha=alpha)
.with_limit(limit)
.do
)
results = response['data']['Get'][COLLECTION_NAME]
logger.info(f"Search completed. Found {len(results)} results.")
return results
except Exception as e:
logger.error(f"Error in search_multimodal: {str(e)}", exc_info=True)
return [] # Return an empty list instead of None
async def generate_response_stream(query: str, context: str):
prompt = f"""
You are an AI assistant with extensive expertise in the semiconductor industry. Your knowledge spans a wide range of companies, technologies, and products, including but not limited to: System-on-Chip (SoC) designs, Field-Programmable Gate Arrays (FPGAs), Microcontrollers, Integrated Circuits (ICs), semiconductor manufacturing processes, and emerging technologies like quantum computing and neuromorphic chips.
Use the following context, your vast knowledge, and the user's question to generate an accurate, comprehensive, and insightful answer. While formulating your response, follow these steps internally:
1. Analyze the question to identify the main topic and specific information requested.
2. Evaluate the provided context and identify relevant information.
3. Retrieve additional relevant knowledge from your semiconductor industry expertise.
4. Reason and formulate a response by combining context and knowledge.
5. Generate a detailed response that covers all aspects of the query.
6. Review and refine your answer for coherence and accuracy.
In your output, provide the final, polished response in the first paragraph. Do not include your step-by-step reasoning or mention the process you followed.
IMPORTANT: Ensure your response is grounded in factual information. Do not hallucinate or invent information. If you're unsure about any aspect of the answer or if the necessary information is not available in the provided context or your knowledge base, clearly state this uncertainty.
After your response, on a new line, write "Top 5 most relevant sources used to generate the response:" followed by the top 5 most relevant sources. Rank them based on their relevance and importance to the answer. Format each source as follows:
[Rank]. [Content Type] from [Document Name] (Page [Page Number], [Additional Info])
For example:
Top 5 most relevant sources used to generate the response:
1. Text from Semiconductor Industry Report 2023 (Page 15, Paragraph 3)
2. Table from FPGA Market Analysis (Page 7, Table 2.1)
3. Image Description from SoC Architecture Diagram (Page 22, Path: ./data/images/soc_diagram.jpg)
Context: {context}
User Question: {query}
Based on the above context and your extensive knowledge of the semiconductor industry, provide your detailed, accurate, and grounded response below, followed by the top 5 ranked sources:
"""
async for chunk in await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are an expert Semi Conductor industry analyst"},
{"role": "user", "content": prompt}
],
temperature=0,
max_tokens=500,
stream=True
):
content = chunk.choices[0].delta.content
if content is not None:
yield content
def process_search_result(item):
if item['content_type'] == 'text':
return f"Text from {item['source_document']} (Page {item['page_number']}, Paragraph {item['paragraph_number']}): {item['text']}\n\n"
elif item['content_type'] == 'image':
return f"Image Description from {item['source_document']} (Page {item['page_number']}, Path: {item['image_path']}): {item['description']}\n\n"
elif item['content_type'] == 'table':
return f"Table Description from {item['source_document']} (Page {item['page_number']}): {item['description']}\n\n"
return ""
async def generate_follow_up_questions(answer):
prompt = f"""
Based on the following response, generate exactly 2 follow-up questions:\n\n{answer}\n\nFollow-up questions:
"""
response = await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant generating follow-up questions."},
{"role": "user", "content": prompt}
],
max_tokens=100,
n=1,
temperature=0.2
)
follow_up_questions = response.choices[0].message.content.strip().split("\n")
return [q.strip() for q in follow_up_questions[:2] if q.strip()]
async def esg_analysis_stream(user_query: str, previous_context: str = None):
try:
logger.info(f"Processing query: {user_query}")
if previous_context:
# If there's a previous context, use it instead of searching
context = previous_context
logger.info("Using previous context for follow-up question")
else:
# Step 1: Search for relevant information
search_results = await search_multimodal(user_query)
logger.info(f"Found {len(search_results)} search results")
if not search_results:
return "I'm sorry, but I couldn't find any relevant information to answer your query.", "", []
# Step 2: Process search results concurrently
with concurrent.futures.ThreadPoolExecutor() as executor:
context_parts = list(await asyncio.get_event_loop().run_in_executor(
executor,
lambda: list(executor.map(process_search_result, search_results))
))
context = "".join(context_parts)
logger.info(f"Processed search results into context of length {len(context)}")
# Step 3 and 4: Generate response and follow-up questions concurrently
response_task = asyncio.create_task(generate_and_split_response(user_query, context))
follow_up_task = asyncio.create_task(generate_follow_up_questions(user_query))
main_response, sources = await response_task
follow_up_questions = await follow_up_task
return main_response, sources, follow_up_questions, context
except Exception as e:
logger.error(f"Error in esg_analysis_stream: {str(e)}", exc_info=True)
return "I apologize, but an error occurred while processing your request.", "", [], ""
async def generate_and_split_response(query: str, context: str):
full_response = await generate_response(query, context)
parts = full_response.split("Top 5 most relevant sources used to generate the response:", 1)
main_response = parts[0].strip() if parts else full_response
sources = parts[1].strip() if len(parts) > 1 else ""
return main_response, sources
async def generate_response(query: str, context: str):
prompt = f"""
You are an AI assistant with extensive expertise in the semiconductor industry. Your knowledge spans a wide range of companies, technologies, and products, including but not limited to: System-on-Chip (SoC) designs, Field-Programmable Gate Arrays (FPGAs), Microcontrollers, Integrated Circuits (ICs), semiconductor manufacturing processes, and emerging technologies like quantum computing and neuromorphic chips.
Use the following context, your vast knowledge, and the user's question to generate an accurate, comprehensive, and insightful answer. While formulating your response, follow these steps internally:
Analyze the question to identify the main topic and specific information requested.
Evaluate the provided context and identify relevant information.
Retrieve additional relevant knowledge from your semiconductor industry expertise.
Reason and formulate a response by combining context and knowledge.
Generate a detailed response that covers all aspects of the query.
Review and refine your answer for coherence and accuracy.
Also when any general query is asked respond like you are a human and answer the question as you would answer in real life.
Do not give response with information about the company or any other information for queries like Hi, Hello, How are you etc.
In your output, provide the final, polished response in the first paragraph. Do not include your step-by-step reasoning or mention the process you followed.
IMPORTANT NOTE: Ensure your response is grounded in factual information. Do not hallucinate or invent information. If you're unsure about any aspect of the answer or if the necessary information is not available in the provided context or your knowledge base, clearly state this uncertainty.
After your response, on a new line, write "Top 5 most relevant sources used to generate the response:" followed by the top 5 most relevant sources. Rank them based on their relevance and importance to the answer. Format each source as follows:
[Rank]. [Content Type] from [Document Name] (Page [Page Number], [Additional Info])
For example:
Top 5 most relevant sources used to generate the response:
Text from Semiconductor Industry Report 2023 (Page 15, Paragraph 3)
Table from FPGA Market Analysis (Page 7, Table 2.1)
Image Description from SoC Architecture Diagram (Page 22, Path: ./data/images/soc_diagram.jpg)
IMPORTANT NOTE: Only provide sources if it is referenced or mentioned in the response.
Context: {context}
User Question: {query}
Based on the above context and your extensive knowledge of the semiconductor industry, provide your detailed, accurate, and grounded response below, followed by the top 5 ranked sources:
"""
response = await openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are an expert Semi Conductor industry analyst"},
{"role": "user", "content": prompt}
],
temperature=0,
max_tokens=500
)
return response.choices[0].message.content
@app.route('/')
def index():
return render_template('index.html')
@app.route('/ask', methods=['POST'])
async def ask():
global connection_status, conversation_history
if connection_status["status"] != "Connected":
initialize_weaviate_client()
if connection_status["status"] != "Connected":
return jsonify({'error': 'Weaviate client is not connected'}), 503
try:
user_question = request.json['question']
# Check if it's a follow-up question
if conversation_history and any(keyword in user_question.lower() for keyword in ["previous", "before", "last"]):
previous_context = conversation_history[-1]['context']
main_response, sources, follow_up_questions, context = await esg_analysis_stream(user_question, previous_context)
else:
main_response, sources, follow_up_questions, context = await esg_analysis_stream(user_question)
# Update conversation history
conversation_history.append({
'question': user_question,
'response': main_response,
'sources': sources,
'context': context
})
response_data = {
'response': main_response,
'sources': sources,
'follow_up_questions': follow_up_questions[:2] # Limit to 2 follow-up questions
}
return jsonify(response_data)
except Exception as e:
logger.error(f"Error processing request: {str(e)}", exc_info=True)
return jsonify({'error': 'An error occurred while processing your request'}), 500
@app.route('/data/<path:filename>')
def serve_data_file(filename):
try:
# Remove the './data/' prefix if it's present in the filename
if filename.startswith('./data/'):
filename = filename[7:]
return send_from_directory('data', filename, mimetype='application/pdf')
except FileNotFoundError:
return f"Error: File {filename} not found", 404
@app.route('/status')
def status():
return jsonify(connection_status)
@app.route('/test-pdf')
def test_pdf():
return '''
<h1>PDF Test</h1>
<object data="./data/DS950 - Versal Architecture and Product Data Sheet - Overview - v2.2 - 240604.pdf" type="application/pdf" width="100%" height="500px">
<p>It appears you don't have a PDF plugin for this browser.
No biggie... you can <a href="./data/DS950 - Versal Architecture and Product Data Sheet - Overview - v2.2 - 240604.pdf">click here to download the PDF file.</a></p>
</object>
'''
@app.route('/check_connection', methods=['GET'])
def check_connection():
global connection_status
if connection_status["status"] != "Connected":
initialize_weaviate_client()
return jsonify(connection_status)
@app.route('/history', methods=['GET'])
def get_history():
global conversation_history
history_data = list(conversation_history)
return jsonify(history_data)
if __name__ == '__main__':
CORS(app, resources={r"/*": {"origins": "*"}})
# Run with threading (recommended for I/O-bound tasks)
app.run(host="0.0.0.0", port=7860, debug=True, threaded=True)
|