InnovisionLLC commited on
Commit
976c3d3
·
verified ·
1 Parent(s): 9fc7180

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +78 -0
  2. app.py +1332 -0
  3. config.json +1 -0
  4. requirements.txt +18 -0
  5. start.sh +3 -0
Dockerfile ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image with CUDA 12.1 and Ubuntu 22.04
2
+ FROM nvidia/cuda:12.1.1-base-ubuntu22.04
3
+
4
+ # Install Python 3.10 and essential dependencies
5
+ RUN apt-get update && \
6
+ apt-get install -y --no-install-recommends \
7
+ python3.10 \
8
+ python3.10-dev \
9
+ python3.10-distutils \
10
+ curl \
11
+ git \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Make Python 3.10 the default
15
+ RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1
16
+
17
+ # Install pip for Python 3.10
18
+ RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10
19
+
20
+
21
+ # Install Ollama with GPU layers
22
+ ENV OLLAMA_GPU_LAYERS=100
23
+ RUN curl -fsSL https://ollama.com/install.sh | sh
24
+
25
+ # Set up application directory
26
+ WORKDIR /app
27
+
28
+
29
+
30
+
31
+ # Configure environment variables (FROM YOUR ORIGINAL SETUP)
32
+ ENV HOME=/home/user \
33
+ PATH=/home/user/.local/bin:$PATH \
34
+ # VECTOR_STORE_DIR=/app/vector_stores \
35
+ # EMBED_MODEL_PATH=/app/datas/bge_onnx \
36
+ PYTHONUNBUFFERED=1 \
37
+ GRADIO_SERVER_NAME="0.0.0.0" \
38
+ HF_HOME=/data/.huggingface \
39
+ HF_HUB_DISABLE_PROGRESS_BARS=1 \
40
+ OLLAMA_MODELS=/data/.ollama/models \
41
+ SYSTEM=spaces
42
+
43
+ # Set up a new user named "user" with user ID 1000
44
+ RUN useradd -m -u 1000 user
45
+
46
+ USER root
47
+ RUN mkdir -p /data && chown user:user /data
48
+
49
+ # Switch to the "user" user
50
+ WORKDIR /app
51
+ USER user
52
+ COPY --chown=user requirements.txt /app/
53
+ # Install Python dependencies
54
+ RUN pip install --no-cache-dir -r requirements.txt
55
+
56
+
57
+ COPY --chown=user . /app
58
+
59
+
60
+ # COPY --chown=user . $HOME/app
61
+ # RUN mkdir -p /data
62
+ # RUN chmod 777 /data
63
+
64
+
65
+ # Verify CUDA and Python versions
66
+ # RUN python3 -c "import torch; print(f'PyTorch CUDA available: {torch.cuda.is_available()}')" && \
67
+ # python3 --version
68
+
69
+ # Expose ports for Ollama and Gradio
70
+ EXPOSE 11434 7860
71
+
72
+
73
+ # Copy and set permissions for start script
74
+ # COPY start.sh /app/start.sh
75
+ RUN chmod +x /app/start.sh
76
+
77
+ # Start services using the startup script
78
+ CMD ["/app/start.sh"]
app.py ADDED
@@ -0,0 +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
+
config.json ADDED
@@ -0,0 +1 @@
 
 
1
+ { "models": ["hf.co/JatinkInnovision/ComFit4:Q4_K_M"]}
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu121
2
+ numpy==1.26.4
3
+ ollama==0.3.3
4
+ onnx==1.17.0
5
+ gradio==5.16.0
6
+ gradiologin
7
+ gradio[oauth]
8
+ huggingface-hub
9
+ hf-transfer
10
+ llama-index-core==0.11.18
11
+ llama-index-embeddings-huggingface-optimum==0.2.0
12
+ llama-index-llms-ollama==0.3.4
13
+ llama-index-vector-stores-duckdb==0.2.0
14
+ llama-index-llms-openai==0.2.5
15
+ duckdb==0.10.3
16
+ nest_asyncio
17
+ torch==2.5.0+cu121
18
+ pydantic==2.10.6
start.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/sh
2
+ ollama serve > /dev/null 2>&1 &
3
+ sleep 10 && python3 app.py