InnovisionLLC commited on
Commit
07159ec
·
1 Parent(s): 1a5f5ac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1139 -4
app.py CHANGED
@@ -1,6 +1,1141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- with gr.Blocks() as demo:
4
- button = gr.LoginButton("Sign in")
5
- gr.load("models/meta-llama/Llama-3.2-3B-Instruct", provider="sambanova", accept_token=button)
6
- demo.launch()
 
1
+ from llama_index.llms.ollama import Ollama
2
+ from llama_index.embeddings.huggingface_optimum import OptimumEmbedding
3
+ from llama_index.core import Settings
4
+ from llama_index.core.memory import ChatMemoryBuffer
5
+ from llama_index.core.storage.chat_store import SimpleChatStore
6
+ from llama_index.core import VectorStoreIndex, StorageContext
7
+ from llama_index.vector_stores.duckdb import DuckDBVectorStore
8
+ from llama_index.core.llms import ChatMessage, MessageRole
9
+ import uuid
10
+ import os
11
+ import json
12
+ import nest_asyncio
13
+ from datetime import datetime
14
+ import copy
15
+ import ollama
16
+
17
  import gradio as gr
18
+ from gradio.themes.utils import colors, fonts, sizes
19
+ from gradio.themes import Base
20
+ from huggingface_hub import whoami
21
+ import re
22
+
23
+
24
+
25
+
26
+ from llama_index.core.evaluation import FaithfulnessEvaluator
27
+
28
+ from huggingface_hub import snapshot_download
29
+ import html
30
+ import concurrent.futures
31
+ import time
32
+
33
+ nest_asyncio.apply()
34
+
35
+
36
+ PERSISTENT_DIR = "/data"
37
+
38
+ FORCE_UPDATE_FLAG = False
39
+
40
+
41
+
42
+
43
+ VECTOR_STORE_DIR = "./vector_stores"
44
+ EMBED_MODEL_PATH = "./datas/bge_onnx"
45
+ CONFIG_PATH = "config.json"
46
+
47
+ DEFAULT_LLM = "Jatin19K/unsloth-q5_k_m-mistral-nemo-instruct-2407:latest"
48
+ DEFAULT_VECTOR_STORE = "CFIR"
49
+
50
+ CONVERSATION_HISTORY_PATH = "./conversation_history"
51
+
52
+
53
+ SYSTEM_PROMPT = (
54
+ "You are a helpful assistant which helps users to understand scientific knowledge "
55
+ "about biomechanics of injuries to human bodies."
56
+ )
57
+
58
+
59
+ # HF required
60
+ EMBED_MODEL_PATH = os.path.join(PERSISTENT_DIR, "bge_onnx")
61
+ VECTOR_STORE_DIR = os.path.join(PERSISTENT_DIR, "vector_stores")
62
+ CONVERSATION_HISTORY_PATH = os.path.join(PERSISTENT_DIR, "conversation_history")
63
+ token = os.getenv("HF_TOKEN")
64
+ dataset_id = os.getenv("DATASET_ID")
65
+
66
+ def download_data_if_needed():
67
+ global FORCE_UPDATE_FLAG
68
+
69
+ if not os.path.exists(EMBED_MODEL_PATH) or not os.path.exists(VECTOR_STORE_DIR):
70
+ FORCE_UPDATE_FLAG = True
71
+
72
+ if FORCE_UPDATE_FLAG:
73
+ snapshot_download(
74
+ repo_id=dataset_id,
75
+ repo_type="dataset",
76
+ token=token,
77
+ local_dir=PERSISTENT_DIR
78
+ )
79
+ print("Data downloaded successfully.")
80
+ else:
81
+ print("Data exists.")
82
+
83
+ download_data_if_needed()
84
+
85
+
86
+
87
+
88
+ def process_text_with_think_tags(text):
89
+ # Check if the text contains think tags
90
+ think_pattern = r'<think>(.*?)</think>'
91
+ think_matches = re.findall(think_pattern, text, re.DOTALL)
92
+
93
+ if think_matches:
94
+ # There are think tags present
95
+ # Extract the content inside think tags
96
+ think_content = think_matches[0] # Taking the first think block
97
+
98
+ # Remove the think tags part from the original text
99
+ remaining_text = re.sub(think_pattern, '', text, flags=re.DOTALL).strip()
100
+
101
+ # Return both parts separately
102
+ return {
103
+ 'has_two_parts': True,
104
+ 'think_part': think_content,
105
+ 'regular_part': remaining_text
106
+ }
107
+ else:
108
+ # No think tags, just one part
109
+ return {
110
+ 'has_two_parts': False,
111
+ 'full_text': text
112
+ }
113
+
114
+
115
+
116
+
117
+ class VectorStoreManager:
118
+ def __init__(self):
119
+ self.vector_stores = self.initialize_vector_stores()
120
+
121
+
122
+ def initialize_vector_stores(self):
123
+ """Scan vector store directory for DuckDB files, supporting nested directories"""
124
+ vector_stores = {}
125
+ if os.path.exists(VECTOR_STORE_DIR):
126
+ # Add default store if it exists
127
+ cfir_path = os.path.join(VECTOR_STORE_DIR, f"{DEFAULT_VECTOR_STORE}.duckdb")
128
+ if os.path.exists(cfir_path):
129
+ vector_stores[DEFAULT_VECTOR_STORE] = {
130
+ "path": cfir_path,
131
+ "display_name": DEFAULT_VECTOR_STORE,
132
+ "data": DuckDBVectorStore.from_local(cfir_path)
133
+ }
134
+
135
+ # Scan for .duckdb files in root directory and subdirectories
136
+ for root, dirs, files in os.walk(VECTOR_STORE_DIR):
137
+ for file in files:
138
+ if file.endswith(".duckdb") and file != f"{DEFAULT_VECTOR_STORE}.duckdb":
139
+ # Skip the default store since we've already handled it
140
+ if root == VECTOR_STORE_DIR and file == f"{DEFAULT_VECTOR_STORE}.duckdb":
141
+ continue
142
+
143
+ # Get the full path to the file
144
+ file_path = os.path.join(root, file)
145
+
146
+ # Calculate store_name: combine category and subcategory
147
+ rel_path = os.path.relpath(file_path, VECTOR_STORE_DIR)
148
+ path_parts = rel_path.split(os.sep)
149
+
150
+ if len(path_parts) == 1:
151
+ # Files in the root directory
152
+ store_name = path_parts[0][:-7] # Remove .duckdb
153
+ display_name = store_name
154
+ else:
155
+ # Files in subdirectories
156
+ category = path_parts[0]
157
+ file_name = path_parts[-1][:-7] # Remove .duckdb
158
+ store_name = f"{category}_{file_name}"
159
+ display_name = f"{category} - {file_name}"
160
+
161
+ vector_stores[store_name] = {
162
+ "path": file_path,
163
+ "display_name": display_name,
164
+ "data": DuckDBVectorStore.from_local(file_path)
165
+ }
166
+
167
+ return vector_stores
168
+
169
+
170
+ def get_vector_store_data(self, store_name):
171
+ """Get the actual vector store data by store name"""
172
+ return self.vector_stores[store_name]["data"]
173
+
174
+ def get_vector_store_by_display_name(self, display_name):
175
+ """Find a vector store by its display name"""
176
+ for name, store_info in self.vector_stores.items():
177
+ if store_info["display_name"] == display_name:
178
+ return self.vector_stores[name]["data"]
179
+ return None
180
+
181
+ def get_all_store_names(self):
182
+ """Get all vector store names"""
183
+ return list(self.vector_stores.keys())
184
+
185
+ def get_all_display_names(self):
186
+ """Get all display names as a list"""
187
+ return [store_info["display_name"] for store_info in self.vector_stores.values()]
188
+
189
+ def get_display_name(self, store_name):
190
+ """Get display name for a store name"""
191
+ return self.vector_stores[store_name]["display_name"]
192
+
193
+ def get_name_display_pairs(self):
194
+ """Get list of (display_name, store_name) tuples for UI dropdowns"""
195
+ return [(v["display_name"], k) for k, v in self.vector_stores.items()]
196
+
197
+ # Create a global instance
198
+ vector_store_manager = VectorStoreManager()
199
+
200
+
201
+
202
+ class DEKCIBChatbot:
203
+ def __init__(self):
204
+ self.initialize()
205
+
206
+
207
+ def initialize(self):
208
+ self.session_manager = SessionManager()
209
+ self.embed_model = OptimumEmbedding(folder_name=EMBED_MODEL_PATH)
210
+ Settings.embed_model = self.embed_model
211
+ self.vector_stores = self.initialize_vector_store()
212
+
213
+ self.config = self._load_config()
214
+ self.llm_options = self._initialize_models()
215
+
216
+
217
+
218
+ def get_user_data(self, user_id):
219
+ return user_id
220
+
221
+
222
+
223
+
224
+
225
+ def _load_config(self):
226
+ """Load model configuration from JSON file"""
227
+ try:
228
+ with open(CONFIG_PATH, 'r') as f:
229
+ return json.load(f)
230
+ except Exception as e:
231
+ print(f"Error loading config: {e}")
232
+ return {"models": []}
233
+
234
+ def _initialize_models(self):
235
+ """Initialize and verify all models from config"""
236
+ config_models = self.config.get("models", [])
237
+ available_models = {}
238
+
239
+ # Get currently available Ollama models
240
+ try:
241
+ current_models = {m['name']: m['name'] for m in ollama.list()['models']}
242
+ print(current_models)
243
+ except Exception as e:
244
+ print(f"Error fetching current models: {e}")
245
+ current_models = {}
246
+
247
+ # Check each configured model
248
+ for model_name in config_models:
249
+ if model_name not in current_models:
250
+ print(f"Model {model_name} not found locally. Attempting to pull...")
251
+ try:
252
+ ollama.pull(model_name)
253
+ available_models[model_name] = model_name
254
+ print(f"Successfully pulled model {model_name}")
255
+ except Exception as e:
256
+ print(f"Error pulling model {model_name}: {e}")
257
+ continue
258
+ else:
259
+ available_models[model_name] = current_models[model_name]
260
+
261
+ return available_models
262
+
263
+ def get_available_models(self):
264
+ """Return dictionary of available models"""
265
+ return self.available_models
266
+
267
+
268
+ def initialize_vector_store(self):
269
+ """Scan vector store directory for DuckDB files, supporting nested directories"""
270
+ vector_stores = {}
271
+ if os.path.exists(VECTOR_STORE_DIR):
272
+ # Add default store if it exists
273
+ cfir_path = os.path.join(VECTOR_STORE_DIR, f"{DEFAULT_VECTOR_STORE}.duckdb")
274
+ if os.path.exists(cfir_path):
275
+ vector_stores[DEFAULT_VECTOR_STORE] = {
276
+ "path": cfir_path,
277
+ "display_name": DEFAULT_VECTOR_STORE,
278
+ "data": DuckDBVectorStore.from_local(cfir_path)
279
+ }
280
+
281
+ # Scan for .duckdb files in root directory and subdirectories
282
+ for root, dirs, files in os.walk(VECTOR_STORE_DIR):
283
+ for file in files:
284
+ if file.endswith(".duckdb") and file != f"{DEFAULT_VECTOR_STORE}.duckdb":
285
+ # Skip the default store since we've already handled it
286
+ if root == VECTOR_STORE_DIR and file == f"{DEFAULT_VECTOR_STORE}.duckdb":
287
+ continue
288
+
289
+ # Get the full path to the file
290
+ file_path = os.path.join(root, file)
291
+
292
+ # Calculate store_name: combine category and subcategory
293
+ rel_path = os.path.relpath(file_path, VECTOR_STORE_DIR)
294
+ path_parts = rel_path.split(os.sep)
295
+
296
+ if len(path_parts) == 1:
297
+ # Files in the root directory
298
+ store_name = path_parts[0][:-7] # Remove .duckdb
299
+ display_name = store_name
300
+ else:
301
+ # Files in subdirectories
302
+ category = path_parts[0]
303
+ file_name = path_parts[-1][:-7] # Remove .duckdb
304
+ store_name = f"{category}_{file_name}"
305
+ display_name = f"{category} - {file_name}"
306
+
307
+ vector_stores[store_name] = {
308
+ "path": file_path,
309
+ "display_name": display_name,
310
+ "data": DuckDBVectorStore.from_local(file_path)
311
+ }
312
+
313
+ return vector_stores
314
+
315
+
316
+ def get_vector_store(self, vector_store_name):
317
+ return self.vector_stores[vector_store_name]["data"]
318
+
319
+
320
+ class DeKCIBChatEngine:
321
+ """
322
+ Manages the core components needed for chat functionality with RAG.
323
+ Handles LLM, vector store, memory, chat store, and indexes.
324
+ """
325
+
326
+ def __init__(self, user_id=None, llm_name=None, vector_store_name=None):
327
+ """Initialize the chat engine with all necessary components"""
328
+ self.user_id = user_id
329
+ self.llm = None
330
+ self.llm_name = llm_name
331
+ self.vector_store = None
332
+ self.vector_store_name = vector_store_name
333
+ self.storage_context = None
334
+ self.index = None
335
+ self.chat_store = None
336
+ self.memory = None
337
+ self.chat_engine = None
338
+ self.rebuild_chat_engine_flag = True
339
+
340
+
341
+ # Conversation metadata management
342
+ self.convs_metadata = {}
343
+ self.current_conv_id = None
344
+
345
+ if user_id:
346
+ self.initialize_chat_store()
347
+ self.initialize_convs_metadata()
348
+
349
+ # Set initial components if provided
350
+ if llm_name:
351
+ self.set_llm(llm_name)
352
+
353
+ if vector_store_name:
354
+ self.set_vector_store(vector_store_name)
355
+
356
+
357
+
358
+ def initialize_convs_metadata(self):
359
+ print(f"Initializing convs metadata for user {self.user_id}")
360
+ self.convs_metadata_file_path = os.path.join(CONVERSATION_HISTORY_PATH, self.user_id, f"{self.user_id}_metadata.json")
361
+ self.sorted_conversation_list = []
362
+ self.get_convs_metadata()
363
+
364
+
365
+
366
+ def get_convs_metadata(self):
367
+ if os.path.exists(self.convs_metadata_file_path):
368
+ with open(self.convs_metadata_file_path, "r") as f:
369
+ self.convs_metadata = json.load(f)
370
+ self.sorted_conversation_list = self.get_sorted_conversation_list()
371
+
372
+
373
+
374
+
375
+ def set_current_conv_id(self, input_value, type="index"):
376
+
377
+ if len(self.sorted_conversation_list) == 0:
378
+ self.current_conv_id = None
379
+ self.rebuild_chat_engine_flag = True
380
+ return
381
+
382
+ if type == "index" and self.current_conv_id != self.sorted_conversation_list[input_value]:
383
+ self.current_conv_id = self.sorted_conversation_list[input_value]
384
+ self.rebuild_chat_engine_flag = True
385
+ elif type == "id" and self.current_conv_id != input_value:
386
+ self.current_conv_id = input_value
387
+ self.rebuild_chat_engine_flag = True
388
+
389
+
390
+
391
+ def get_sorted_conversation_list(self):
392
+ """
393
+ Returns a list of conversation IDs sorted by update time,
394
+ with the most recently updated conversations first.
395
+ """
396
+ # Create a list of (conv_id, updated_at) tuples
397
+ conv_with_timestamps = []
398
+
399
+ for conv_id, metadata in self.convs_metadata.items():
400
+ # Use updated_at timestamp for sorting
401
+ if "updated_at" in metadata:
402
+ # Convert the ISO timestamp string to datetime object for comparison
403
+ update_time = datetime.fromisoformat(metadata["updated_at"])
404
+ conv_with_timestamps.append((conv_id, update_time))
405
+
406
+ # Sort by timestamp (descending order - newest first)
407
+ sorted_convs = sorted(conv_with_timestamps, key=lambda x: x[1], reverse=True)
408
+
409
+ # Return just the conversation IDs in the sorted order
410
+ return [conv_id for conv_id, _ in sorted_convs]
411
+
412
+
413
+ def get_sorted_conversation_list_for_ui(self):
414
+ new_list = []
415
+ for item in self.sorted_conversation_list:
416
+ new_list.append([self.convs_metadata[item]["title"]])
417
+ return new_list
418
+
419
+
420
+ def update_convs_metadata(self, conv_id, title=None, create_flag=False):
421
+ current_time = datetime.now().isoformat()
422
+ if title is not None:
423
+ self.convs_metadata[conv_id].update({"title":title})
424
+ self.convs_metadata[conv_id].update({"updated_at":current_time, "llm_name": self.llm_name, "vector_store_name": self.vector_store_name})
425
+
426
+ self.sorted_conversation_list = self.get_sorted_conversation_list()
427
+
428
+
429
+
430
+ def set_llm(self, llm_name):
431
+
432
+ self.llm = Ollama(
433
+ model=llm_name,
434
+ request_timeout=120,
435
+ temperature=0.3
436
+ )
437
+ self.set_rebuild_chat_engine_flag(True)
438
+
439
+ return self.llm
440
+
441
+ def set_vector_store(self, vector_store_name):
442
+
443
+ self.vector_store = vector_store_manager.get_vector_store_by_display_name(vector_store_name)
444
+
445
+ if self.vector_store:
446
+ self.initialize_index()
447
+ self.set_rebuild_chat_engine_flag(True)
448
+
449
+ return self.vector_store
450
+
451
+ def initialize_index(self):
452
+ """Initialize the index using the current vector store"""
453
+ if not self.vector_store:
454
+ raise ValueError("Vector store must be set before initializing index")
455
+
456
+ self.storage_context = StorageContext.from_defaults(vector_store=self.vector_store)
457
+ self.index = VectorStoreIndex.from_vector_store(
458
+ vector_store=self.vector_store,
459
+ storage_context=self.storage_context
460
+ )
461
+ return self.index
462
+
463
+ def initialize_chat_store(self):
464
+ """Initialize the chat store for the user"""
465
+ print(f"Initializing chat store for user {self.user_id}")
466
+
467
+ chat_store_file_path = os.path.join(CONVERSATION_HISTORY_PATH, self.user_id, f"{self.user_id}.json")
468
+
469
+ # Ensure directory exists
470
+ os.makedirs(os.path.dirname(chat_store_file_path), exist_ok=True)
471
+
472
+ # Create or load chat store
473
+ if not os.path.exists(chat_store_file_path):
474
+ self.chat_store = SimpleChatStore()
475
+ self.chat_store.persist(persist_path=chat_store_file_path)
476
+ else:
477
+ self.chat_store = SimpleChatStore.from_persist_path(chat_store_file_path)
478
+
479
+ self.chat_store_file_path = chat_store_file_path
480
+
481
+ return self.chat_store
482
+
483
+
484
+ def initialize_memory(self, conversation_id=None):
485
+ """Initialize or reinitialize memory with specified conversation ID"""
486
+ if not self.chat_store:
487
+ raise ValueError("Chat store must be initialized before memory")
488
+
489
+
490
+ print(f"Initializing memory for conversation {conversation_id}")
491
+ print(self.chat_store)
492
+
493
+ self.memory = ChatMemoryBuffer.from_defaults(
494
+ token_limit=3000,
495
+ chat_store=self.chat_store,
496
+ chat_store_key=conversation_id
497
+ )
498
+ return self.memory
499
+
500
+ def build_chat_engine(self, conversation_id=None):
501
+ """Build the chat engine with all components"""
502
+ if not all([self.llm, self.index, self.chat_store]):
503
+ raise ValueError("LLM, index, and chat store must be set before building chat engine")
504
+
505
+ # Initialize or update memory with conversation ID
506
+ # if conversation_id and self.current_conv_id != conversation_id:
507
+ self.initialize_memory(conversation_id)
508
+ self.current_conv_id = conversation_id
509
+
510
+ # Default system prompt if none provided
511
+
512
+ # Create the chat engine
513
+ self.chat_engine = self.index.as_chat_engine(
514
+ chat_mode="context",
515
+ llm=self.llm,
516
+ memory=self.memory,
517
+ system_prompt=SYSTEM_PROMPT
518
+ )
519
+
520
+ self.set_rebuild_chat_engine_flag(False)
521
+ return self.chat_engine
522
+
523
+ def save_chat_history(self):
524
+ """Save chat history to file"""
525
+ if self.chat_store and hasattr(self, 'chat_store_file_path'):
526
+ self.chat_store.persist(persist_path=self.chat_store_file_path)
527
+
528
+ def add_message(self, conversation_id, message):
529
+ """Add a message to the chat history"""
530
+ if self.chat_store:
531
+ self.chat_store.add_message(conversation_id, message)
532
+
533
+ def get_chat_history(self, conversation_id):
534
+ """Get chat history for a specific conversation"""
535
+ if conversation_id is None:
536
+ return []
537
+ if self.chat_store:
538
+ return self.chat_store.to_dict()["store"][conversation_id]
539
+ return []
540
+
541
+
542
+ def set_rebuild_chat_engine_flag(self, flag):
543
+ self.rebuild_chat_engine_flag = flag
544
+
545
+ def chat(self, message, conversation_id=None):
546
+
547
+
548
+ create_flag = False
549
+ if conversation_id is None:
550
+ conversation_id = self.create_conversation(message=message)
551
+ create_flag = True
552
+ print(f"Created new conversation {conversation_id}")
553
+ self.set_rebuild_chat_engine_flag(True)
554
+ elif self.current_conv_id != conversation_id:
555
+ self.set_rebuild_chat_engine_flag(True)
556
+
557
+
558
+
559
+ if self.rebuild_chat_engine_flag:
560
+ self.chat_engine = self.build_chat_engine(conversation_id)
561
+ self.rebuild_chat_engine_flag = False
562
+
563
+
564
+ print("user message")
565
+ # user_msg = ChatMessage(role=MessageRole.USER, content=message)
566
+ # self.add_message(conversation_id, user_msg)
567
+
568
+
569
+ print("L597")
570
+ print(message)
571
+ # Get response
572
+ response = self.chat_engine.chat(message)
573
+
574
+ answer = response.response
575
+
576
+ print(answer)
577
+ print(type(answer))
578
+ print("assistant message")
579
+ # assistant_msg = ChatMessage(role=MessageRole.ASSISTANT, content=answer)
580
+ # self.add_message(conversation_id, assistant_msg)
581
+
582
+
583
+ self.update_convs_metadata(conversation_id, create_flag=create_flag)
584
+ print("update_convs_metadata")
585
+ self.save_metadata()
586
+ print("save_metadata")
587
+ self.save_chat_history()
588
+ print("save_chat_history")
589
+
590
+ return response
591
+
592
+ def create_conversation(self, message=None):
593
+ """
594
+ Create a new conversation with metadata
595
+ Args:
596
+ title: Optional title for the conversation
597
+ message: First message to use for generating a title
598
+ Returns:
599
+ conversation_id: ID of the new conversation
600
+ """
601
+ # Generate a new unique conversation ID
602
+ conv_id = str(uuid.uuid4())
603
+
604
+ # Set as current conversation
605
+ self.current_conv_id = conv_id
606
+
607
+ # Generate title from message if not provided
608
+
609
+
610
+ title = message[:50] + ("..." if len(message) > 50 else "")
611
+
612
+ # Create timestamp
613
+ current_time = datetime.now().isoformat()
614
+
615
+ # Store metadata with resource information
616
+ self.convs_metadata[conv_id] = {
617
+ "title": title,
618
+ "created_at": current_time,
619
+ "updated_at": current_time,
620
+ "llm": self.llm_name,
621
+ "vector_store": self.vector_store_name,
622
+ "message_count": 0
623
+ }
624
+
625
+ # Initialize chat engine with the new conversation ID
626
+ # self.chat_engine = self.build_chat_engine(conv_id)
627
+
628
+ return conv_id
629
+
630
+ def update_conversation_metadata(self, conv_id, title=None, increment_message_count=True):
631
+ """
632
+ Update conversation metadata
633
+ Args:
634
+ conv_id: Conversation ID to update
635
+ title: Optional new title
636
+ increment_message_count: Whether to increment message count
637
+ """
638
+ if conv_id not in self.convs_metadata:
639
+ return
640
+
641
+ # Update timestamp
642
+ self.convs_metadata[conv_id]["updated_at"] = datetime.now().isoformat()
643
+
644
+ # Update title if provided
645
+ if title:
646
+ self.convs_metadata[conv_id]["title"] = title
647
+
648
+ # Increment message count if requested
649
+ if increment_message_count:
650
+ self.convs_metadata[conv_id]["message_count"] = self.convs_metadata[conv_id].get("message_count", 0) + 1
651
+
652
+ def get_sorted_conversations(self):
653
+ """
654
+ Returns a list of conversation IDs sorted by update time,
655
+ with the most recently updated conversations first.
656
+ """
657
+ # Create a list of (conv_id, updated_at) tuples
658
+ conv_with_timestamps = []
659
+
660
+ for conv_id, metadata in self.convs_metadata.items():
661
+ # Use updated_at timestamp for sorting
662
+ if "updated_at" in metadata:
663
+ # Convert the ISO timestamp string to datetime object for comparison
664
+ update_time = datetime.fromisoformat(metadata["updated_at"])
665
+ conv_with_timestamps.append((conv_id, update_time))
666
+
667
+ # Sort by timestamp (descending order - newest first)
668
+ sorted_convs = sorted(conv_with_timestamps, key=lambda x: x[1], reverse=True)
669
+
670
+ # Return just the conversation IDs in the sorted order
671
+ return [conv_id for conv_id, _ in sorted_convs]
672
+
673
+ def get_conversation_info(self, conv_id):
674
+ """Get conversation metadata"""
675
+ return self.convs_metadata.get(conv_id, {})
676
+
677
+ def switch_conversation(self, conv_id):
678
+ """
679
+ Switch to an existing conversation
680
+ Args:
681
+ conv_id: Conversation ID to switch to
682
+ Returns:
683
+ True if successful, False otherwise
684
+ """
685
+ if conv_id not in self.convs_metadata:
686
+ return False
687
+
688
+ # Set as current conversation
689
+ self.current_conv_id = conv_id
690
+
691
+ # Get the conversation's LLM and vector store
692
+ metadata = self.convs_metadata[conv_id]
693
+
694
+ # Switch to the conversation's resources if they're different
695
+ if metadata.get("llm") and metadata["llm"] != self.llm_name:
696
+ self.set_llm(metadata["llm"])
697
+
698
+ if metadata.get("vector_store") and metadata["vector_store"] != self.vector_store_name:
699
+ self.set_vector_store(metadata["vector_store"])
700
+
701
+ # Rebuild chat engine with this conversation ID
702
+ self.build_chat_engine(conv_id)
703
+
704
+ return True
705
+
706
+ def save_metadata(self):
707
+ """Save conversation metadata to file"""
708
+ if hasattr(self, 'chat_store_file_path') and self.user_id:
709
+ metadata_path = os.path.join(CONVERSATION_HISTORY_PATH, self.user_id, f"{self.user_id}_metadata.json")
710
+ os.makedirs(os.path.dirname(metadata_path), exist_ok=True)
711
+ with open(metadata_path, 'w') as f:
712
+ json.dump(self.convs_metadata, f)
713
+
714
+ def load_metadata(self):
715
+ """Load conversation metadata from file"""
716
+ if self.user_id:
717
+ metadata_path = os.path.join(CONVERSATION_HISTORY_PATH, self.user_id, f"{self.user_id}_metadata.json")
718
+ if os.path.exists(metadata_path):
719
+ try:
720
+ with open(metadata_path, 'r') as f:
721
+ self.convs_metadata = json.load(f)
722
+ except Exception as e:
723
+ print(f"Error loading metadata: {e}")
724
+
725
+ def get_or_build_chat_engine(self, conversation_id=None, llm_name=None, vector_store_name=None):
726
+ """
727
+ Check if the chat engine needs to be rebuilt based on changes to LLM, vector store or conversation ID.
728
+ Only rebuilds the chat engine if necessary to avoid performance overhead.
729
+
730
+ Args:
731
+ conversation_id: The conversation ID to use
732
+ llm_name: The LLM model name to use
733
+ vector_store_name: The vector store name to use
734
+ system_prompt: Custom system prompt (optional)
735
+
736
+ Returns:
737
+ The existing or newly built chat engine
738
+ """
739
+ rebuild_needed = False
740
+
741
+ # Check if conversation ID changed
742
+ if conversation_id is not None and self.conv_id != conversation_id:
743
+ print(f"Building chat engine: Conversation ID changed from {self.conv_id} to {conversation_id}")
744
+ self.conv_id = conversation_id
745
+ rebuild_needed = True
746
+
747
+ # Check if LLM changed
748
+ if llm_name is not None and self.llm_name != llm_name:
749
+ print(f"Building chat engine: LLM changed from {self.llm_name} to {llm_name}")
750
+ self.set_llm(llm_name)
751
+ rebuild_needed = True
752
+
753
+ # Check if vector store changed
754
+ if vector_store_name is not None and self.vector_store_name != vector_store_name:
755
+ print(f"Building chat engine: Vector store changed from {self.vector_store_name} to {vector_store_name}")
756
+ self.set_vector_store(vector_store_name)
757
+ rebuild_needed = True
758
+
759
+ # Rebuild only if needed
760
+ if rebuild_needed:
761
+ return self.build_chat_engine(conversation_id)
762
+ else:
763
+ print("Using existing chat engine: No changes detected")
764
+ return self.chat_engine
765
+
766
+
767
+
768
+
769
+
770
+ class SessionManager:
771
+ def __init__(self):
772
+ self.sessions = {}
773
+
774
+ def create_session(self, user_id=None):
775
+ if user_id is None:
776
+ return None
777
+
778
+ print(f"Creating session for user {user_id}")
779
+ if user_id not in self.sessions:
780
+ self.sessions[user_id] = DeKCIBChatEngine(user_id, llm_name=DEFAULT_LLM, vector_store_name=DEFAULT_VECTOR_STORE)
781
+ print(f"Session created for user {user_id}")
782
+ return self.sessions[user_id]
783
+
784
+
785
+
786
+
787
+
788
+ class ChatbotUI:
789
+ """UI handler for the chatbot application"""
790
+
791
+ def __init__(self, dekcib_chatbot):
792
+ """Initialize with a chat engine"""
793
+ self.dekcib_chatbot = dekcib_chatbot
794
+ self.init_attr()
795
+
796
+
797
+ def init_attr(self):
798
+ self.llm_options = self.dekcib_chatbot.llm_options
799
+ self.vector_stores = self.dekcib_chatbot.vector_stores
800
+ # self.vector_stores_options = [(v["display_name"], k) for k, v in self.dekcib_chatbot.vector_stores.items()]
801
+
802
+
803
+ # self.init_conversations_history()
804
+
805
+
806
+ # def init_conversations_history(self):
807
+ # chat_session = self.dekcib_chatbot.session_manager.sessions[USER_NAME]
808
+ # self.init_convs_list = chat_session.get_sorted_conversation_list_for_ui()
809
+ # if len(self.init_convs_list) > 0:
810
+ # self.init_chat_history = chat_session.get_chat_history(chat_session.sorted_conversation_list[0])
811
+ # self.init_convs_index = 0
812
+ # else:
813
+ # self.init_chat_history = []
814
+ # self.init_convs_index = None
815
+
816
+
817
+
818
+
819
+
820
+
821
+
822
+ def create_ui(self):
823
+ with gr.Blocks(title="De-KCIB(Deep Knowledge Center for Injury Biomechanics)") as demo:
824
+
825
+ user_id = gr.State(None)
826
+
827
+ with gr.Row():
828
+ with gr.Column(scale=6):
829
+ gr.Markdown("<img src='/gradio_api/file/logo.png' alt='Innovision Logo' height='150' width='390'>")
830
+ with gr.Column(scale=1):
831
+ login_btn = gr.LoginButton()
832
+
833
+ with gr.Row():
834
+ gr.Markdown("# De-KCIB(Deep Knowledge Center for Injury Biomechanics)")
835
+
836
+
837
+
838
+ # Move model selection to the top row
839
+ with gr.Row():
840
+ with gr.Column(scale=3):
841
+ llm_dropdown = gr.Dropdown(
842
+ label="Select Language Model",
843
+ choices=list(self.llm_options.values()),
844
+ value=next(iter(self.llm_options.values()), None)
845
+ )
846
+ with gr.Column(scale=3):
847
+ vector_dropdown = gr.Dropdown(
848
+ label="Injury Biomechanics Knowledge Base",
849
+ choices=[(v["display_name"], k) for k, v in self.vector_stores.items()],
850
+ value=next(iter(self.vector_stores.keys()), None)
851
+
852
+ )
853
+ with gr.Column(scale=1):
854
+ status_indicator = gr.HTML(
855
+ value='<div style="text-align:center; padding:8px; border-radius:4px; background-color:#f0f0f0; margin-top:18px;">✓ Ready</div>',
856
+ elem_id="status_indicator"
857
+ )
858
+
859
+ # Main content with sidebar and chat area
860
+ with gr.Row():
861
+ # Left sidebar for conversation history
862
+ with gr.Column(scale=1, elem_classes="sidebar"):
863
+ new_chat_btn = gr.Button("New Chat", size="sm")
864
+
865
+ # Hidden textbox for conversation data
866
+ conversation_data = gr.Textbox(visible=False)
867
+
868
+
869
+ # Dataset for conversation history
870
+ conversation_history = gr.Dataset(
871
+ components=[conversation_data],
872
+ label="Conversation History",
873
+ type="index",
874
+ layout="table"
875
+ )
876
+
877
+
878
+ # Main chat area
879
+ with gr.Column(scale=3):
880
+ chatbot = gr.Chatbot(
881
+ height=500,
882
+ render_markdown=True,
883
+ show_copy_button=True,
884
+ type="messages",
885
+ )
886
+
887
+ with gr.Row():
888
+ msg = gr.Textbox(label="Query", scale=5)
889
+ clear_btn = gr.Button("Clear Session", scale=1)
890
+
891
+
892
+
893
+
894
+ def get_auth_id(oauth_token: gr.OAuthToken | None) -> str:
895
+ if oauth_token is None:
896
+ return None
897
+ id = whoami(oauth_token.token)['id']
898
+ return id
899
+
900
+
901
+
902
+
903
+
904
+ def add_msg(msg, history):
905
+
906
+
907
+ history.append({"role": "user", "content": msg})
908
+ return history
909
+
910
+
911
+
912
+
913
+ def chat_with_dekcib(history, user_id, conv_idx):
914
+
915
+
916
+ msg = history[-1]["content"]
917
+
918
+ user_engine = self.dekcib_chatbot.session_manager.sessions[user_id]
919
+ # user_engine.che
920
+ history.append({"role": "assistant", "content": ""})
921
+
922
+ print("conv_idx")
923
+ print(conv_idx)
924
+
925
+
926
+ conv_id = None
927
+ if conv_idx is not None:
928
+ conv_id = user_engine.sorted_conversation_list[conv_idx]
929
+
930
+ if len(history) == 1:
931
+ conv_id = None
932
+
933
+
934
+
935
+ response = user_engine.chat(msg, conv_id)
936
+ answer = response.response
937
+ for character in answer:
938
+ history[-1]["content"] += character
939
+ yield history
940
+
941
+ def clear_msg():
942
+ print("clear_msg")
943
+ return ""
944
+
945
+
946
+ def update_conversation_history(user_id):
947
+ user_engine = self.dekcib_chatbot.session_manager.sessions[user_id]
948
+ ui_list = user_engine.get_sorted_conversation_list_for_ui()
949
+
950
+ if len(ui_list) > 0:
951
+ idx = 0
952
+ else:
953
+ idx = None
954
+
955
+ return gr.update(samples=ui_list, value=idx)
956
+
957
+
958
+
959
+ msg.submit(
960
+ add_msg,
961
+ [msg, chatbot],
962
+ [chatbot]
963
+ ).then(
964
+ clear_msg,
965
+ None,
966
+ [msg]
967
+ ).then(
968
+ chat_with_dekcib,
969
+ [chatbot, user_id, conversation_history],
970
+ [chatbot]
971
+ ).then(
972
+ update_conversation_history,
973
+ [user_id],
974
+ [conversation_history]
975
+ )
976
+
977
+
978
+ def click_to_select_conversation(conversation_history, user_id):
979
+ user_engine = self.dekcib_chatbot.session_manager.sessions[user_id]
980
+ user_engine.set_current_conv_id(conversation_history, type="index")
981
+
982
+ chat_history = user_engine.get_chat_history(user_engine.current_conv_id)
983
+
984
+ return gr.update(value=conversation_history), chat_history
985
+
986
+
987
+ conversation_history.click(
988
+ click_to_select_conversation,
989
+ [conversation_history, user_id],
990
+ [conversation_history, chatbot]
991
+ )
992
+
993
+
994
+ # msg.submit(
995
+ # chat_with_dekcib,
996
+ # [msg, chatbot, user_id_dropdown],
997
+ # [chatbot]
998
+ # )
999
+
1000
+
1001
+
1002
+ # msg.submit(
1003
+ # clear_msg,
1004
+ # None,
1005
+ # [msg]
1006
+ # ).then(
1007
+ # chat_with_dekcib,
1008
+ # [msg, chatbot, user_id_dropdown],
1009
+ # [chatbot]
1010
+ # )
1011
+
1012
+
1013
+ # clear_btn.click(
1014
+ # clear_session,
1015
+ # [session_state],
1016
+ # [chatbot, session_state],
1017
+ # queue=False
1018
+ # )
1019
+
1020
+
1021
+ def create_session(user_id):
1022
+ if user_id is None:
1023
+ return
1024
+
1025
+
1026
+ self.dekcib_chatbot.session_manager.create_session(user_id)
1027
+ user_engine = self.dekcib_chatbot.session_manager.sessions[user_id]
1028
+
1029
+ llm_name = user_engine.llm_name
1030
+ vector_store_name = user_engine.vector_store_name
1031
+ # chat_store = user_engine.chat_store
1032
+
1033
+ # convs = user_engine.convs
1034
+ # history = user_engine.history
1035
+
1036
+ sorted_conversation_list = user_engine.get_sorted_conversation_list_for_ui()
1037
+ print("sorted_conversation_list")
1038
+ print(sorted_conversation_list)
1039
+ # sorted_conversation_list = [
1040
+ # ["I think therefore I am."],
1041
+ # ["The unexamined life is not worth living."],
1042
+ # ["Test Item"]
1043
+ # ]
1044
+
1045
+ if len(sorted_conversation_list) > 0:
1046
+ index = 0
1047
+ else:
1048
+ index = None
1049
+ update_conversation_history = gr.update(samples=sorted_conversation_list, value=index)
1050
+
1051
+ user_engine.set_current_conv_id(0, type="index")
1052
+ chat_history = user_engine.get_chat_history(user_engine.current_conv_id)
1053
+
1054
+
1055
+ yield llm_name, vector_store_name, update_conversation_history, chat_history
1056
+
1057
+
1058
+ demo.load(
1059
+ get_auth_id,
1060
+ inputs=None,
1061
+ outputs=[user_id]
1062
+ ).then(
1063
+ create_session,
1064
+ [user_id],
1065
+ [llm_dropdown, vector_dropdown, conversation_history, chatbot]
1066
+ )
1067
+
1068
+
1069
+ def update_llm(user_id, llm_name):
1070
+ if user_id is None:
1071
+ return
1072
+
1073
+ user_engine = self.dekcib_chatbot.session_manager.sessions[user_id]
1074
+ user_engine.set_llm(llm_name)
1075
+
1076
+
1077
+ llm_dropdown.change(
1078
+ update_llm,
1079
+ [user_id, llm_dropdown],
1080
+ None
1081
+ )
1082
+
1083
+
1084
+ def update_vector_store(user_id, vector_store_name):
1085
+ if user_id is None:
1086
+ return
1087
+
1088
+ user_engine = self.dekcib_chatbot.session_manager.sessions[user_id]
1089
+ user_engine.set_vector_store(vector_store_name)
1090
+
1091
+
1092
+
1093
+ vector_dropdown.change(
1094
+ update_vector_store,
1095
+ [user_id, vector_dropdown],
1096
+ None
1097
+ )
1098
+
1099
+
1100
+
1101
+ # Create new conversation button should only clear the chat area, but not create a new conversation yet
1102
+ def prepare_new_chat():
1103
+ print("prepare_new_chat")
1104
+
1105
+ return [], gr.update(value=None)
1106
+
1107
+ def print_dataset(value):
1108
+ print("value")
1109
+ print(value)
1110
+
1111
+ # Create new conversation
1112
+ new_chat_btn.click(
1113
+ prepare_new_chat,
1114
+ None,
1115
+ [chatbot, conversation_history],
1116
+ ).then(
1117
+ print_dataset,
1118
+ conversation_history,
1119
+ None
1120
+ )
1121
+
1122
+ return demo
1123
+
1124
+
1125
+
1126
+ # Deployment settings
1127
+ if __name__ == "__main__":
1128
+ # Check chat store health
1129
+ # store_health_ok = check_chat_store_health()
1130
+ # if not store_health_ok:
1131
+ # print("WARNING: Chat store health check failed! Some functionality may not work correctly.")
1132
+
1133
+ # # Run warm-up to pre-initialize resources
1134
+ # warm_up_resources()
1135
+
1136
+ dekcib_chatbot = DEKCIBChatbot()
1137
+ ui = ChatbotUI(dekcib_chatbot)
1138
+ demo = ui.create_ui()
1139
+ demo.queue(max_size=10, default_concurrency_limit=3)
1140
+ demo.launch(allowed_paths=["logo.png"])
1141