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)