InnovisionLLC commited on
Commit
ea9586f
·
verified ·
1 Parent(s): bddcc90

Update app.py

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