15
15
16
16
from private_gpt .constants import PROJECT_ROOT_PATH
17
17
from private_gpt .di import global_injector
18
+ from private_gpt .open_ai .extensions .context_filter import ContextFilter
18
19
from private_gpt .server .chat .chat_service import ChatService , CompletionGen
19
20
from private_gpt .server .chunks .chunks_service import Chunk , ChunksService
20
21
from private_gpt .server .ingest .ingest_service import IngestService
31
32
32
33
SOURCES_SEPARATOR = "\n \n Sources: \n "
33
34
34
- MODES = ["Query Docs " , "Search in Docs " , "LLM Chat" ]
35
+ MODES = ["Query Files " , "Search Files " , "LLM Chat (no context from files) " ]
35
36
36
37
37
38
class Source (BaseModel ):
@@ -74,6 +75,8 @@ def __init__(
74
75
# Cache the UI blocks
75
76
self ._ui_block = None
76
77
78
+ self ._selected_filename = None
79
+
77
80
# Initialize system prompt based on default mode
78
81
self .mode = MODES [0 ]
79
82
self ._system_prompt = self ._get_default_system_prompt (self .mode )
@@ -132,20 +135,34 @@ def build_history() -> list[ChatMessage]:
132
135
),
133
136
)
134
137
match mode :
135
- case "Query Docs" :
138
+ case "Query Files" :
139
+
140
+ # Use only the selected file for the query
141
+ context_filter = None
142
+ if self ._selected_filename is not None :
143
+ docs_ids = []
144
+ for ingested_document in self ._ingest_service .list_ingested ():
145
+ if (
146
+ ingested_document .doc_metadata ["file_name" ]
147
+ == self ._selected_filename
148
+ ):
149
+ docs_ids .append (ingested_document .doc_id )
150
+ context_filter = ContextFilter (docs_ids = docs_ids )
151
+
136
152
query_stream = self ._chat_service .stream_chat (
137
153
messages = all_messages ,
138
154
use_context = True ,
155
+ context_filter = context_filter ,
139
156
)
140
157
yield from yield_deltas (query_stream )
141
- case "LLM Chat" :
158
+ case "LLM Chat (no context from files) " :
142
159
llm_stream = self ._chat_service .stream_chat (
143
160
messages = all_messages ,
144
161
use_context = False ,
145
162
)
146
163
yield from yield_deltas (llm_stream )
147
164
148
- case "Search in Docs " :
165
+ case "Search Files " :
149
166
response = self ._chunks_service .retrieve_relevant (
150
167
text = message , limit = 4 , prev_next_chunks = 0
151
168
)
@@ -166,10 +183,10 @@ def _get_default_system_prompt(mode: str) -> str:
166
183
p = ""
167
184
match mode :
168
185
# For query chat mode, obtain default system prompt from settings
169
- case "Query Docs " :
186
+ case "Query Files " :
170
187
p = settings ().ui .default_query_system_prompt
171
188
# For chat mode, obtain default system prompt from settings
172
- case "LLM Chat" :
189
+ case "LLM Chat (no context from files) " :
173
190
p = settings ().ui .default_chat_system_prompt
174
191
# For any other mode, clear the system prompt
175
192
case _:
@@ -205,8 +222,71 @@ def _list_ingested_files(self) -> list[list[str]]:
205
222
def _upload_file (self , files : list [str ]) -> None :
206
223
logger .debug ("Loading count=%s files" , len (files ))
207
224
paths = [Path (file ) for file in files ]
225
+
226
+ # remove all existing Documents with name identical to a new file upload:
227
+ file_names = [path .name for path in paths ]
228
+ doc_ids_to_delete = []
229
+ for ingested_document in self ._ingest_service .list_ingested ():
230
+ if (
231
+ ingested_document .doc_metadata
232
+ and ingested_document .doc_metadata ["file_name" ] in file_names
233
+ ):
234
+ doc_ids_to_delete .append (ingested_document .doc_id )
235
+ if len (doc_ids_to_delete ) > 0 :
236
+ logger .info (
237
+ "Uploading file(s) which were already ingested: %s document(s) will be replaced." ,
238
+ len (doc_ids_to_delete ),
239
+ )
240
+ for doc_id in doc_ids_to_delete :
241
+ self ._ingest_service .delete (doc_id )
242
+
208
243
self ._ingest_service .bulk_ingest ([(str (path .name ), path ) for path in paths ])
209
244
245
+ def _delete_all_files (self ) -> Any :
246
+ ingested_files = self ._ingest_service .list_ingested ()
247
+ logger .debug ("Deleting count=%s files" , len (ingested_files ))
248
+ for ingested_document in ingested_files :
249
+ self ._ingest_service .delete (ingested_document .doc_id )
250
+ return [
251
+ gr .List (self ._list_ingested_files ()),
252
+ gr .components .Button (interactive = False ),
253
+ gr .components .Button (interactive = False ),
254
+ gr .components .Textbox ("All files" ),
255
+ ]
256
+
257
+ def _delete_selected_file (self ) -> Any :
258
+ logger .debug ("Deleting selected %s" , self ._selected_filename )
259
+ # Note: keep looping for pdf's (each page became a Document)
260
+ for ingested_document in self ._ingest_service .list_ingested ():
261
+ if (
262
+ ingested_document .doc_metadata
263
+ and ingested_document .doc_metadata ["file_name" ]
264
+ == self ._selected_filename
265
+ ):
266
+ self ._ingest_service .delete (ingested_document .doc_id )
267
+ return [
268
+ gr .List (self ._list_ingested_files ()),
269
+ gr .components .Button (interactive = False ),
270
+ gr .components .Button (interactive = False ),
271
+ gr .components .Textbox ("All files" ),
272
+ ]
273
+
274
+ def _deselect_selected_file (self ) -> Any :
275
+ self ._selected_filename = None
276
+ return [
277
+ gr .components .Button (interactive = False ),
278
+ gr .components .Button (interactive = False ),
279
+ gr .components .Textbox ("All files" ),
280
+ ]
281
+
282
+ def _selected_a_file (self , select_data : gr .SelectData ) -> Any :
283
+ self ._selected_filename = select_data .value
284
+ return [
285
+ gr .components .Button (interactive = True ),
286
+ gr .components .Button (interactive = True ),
287
+ gr .components .Textbox (self ._selected_filename ),
288
+ ]
289
+
210
290
def _build_ui_blocks (self ) -> gr .Blocks :
211
291
logger .debug ("Creating the UI blocks" )
212
292
with gr .Blocks (
@@ -235,7 +315,7 @@ def _build_ui_blocks(self) -> gr.Blocks:
235
315
mode = gr .Radio (
236
316
MODES ,
237
317
label = "Mode" ,
238
- value = "Query Docs " ,
318
+ value = "Query Files " ,
239
319
)
240
320
upload_button = gr .components .UploadButton (
241
321
"Upload File(s)" ,
@@ -247,6 +327,7 @@ def _build_ui_blocks(self) -> gr.Blocks:
247
327
self ._list_ingested_files ,
248
328
headers = ["File name" ],
249
329
label = "Ingested Files" ,
330
+ height = 235 ,
250
331
interactive = False ,
251
332
render = False , # Rendered under the button
252
333
)
@@ -260,6 +341,57 @@ def _build_ui_blocks(self) -> gr.Blocks:
260
341
outputs = ingested_dataset ,
261
342
)
262
343
ingested_dataset .render ()
344
+ deselect_file_button = gr .components .Button (
345
+ "De-select selected file" , size = "sm" , interactive = False
346
+ )
347
+ selected_text = gr .components .Textbox (
348
+ "All files" , label = "Selected for Query or Deletion" , max_lines = 1
349
+ )
350
+ delete_file_button = gr .components .Button (
351
+ "🗑️ Delete selected file" ,
352
+ size = "sm" ,
353
+ visible = settings ().ui .delete_file_button_enabled ,
354
+ interactive = False ,
355
+ )
356
+ delete_files_button = gr .components .Button (
357
+ "⚠️ Delete ALL files" ,
358
+ size = "sm" ,
359
+ visible = settings ().ui .delete_all_files_button_enabled ,
360
+ )
361
+ deselect_file_button .click (
362
+ self ._deselect_selected_file ,
363
+ outputs = [
364
+ delete_file_button ,
365
+ deselect_file_button ,
366
+ selected_text ,
367
+ ],
368
+ )
369
+ ingested_dataset .select (
370
+ fn = self ._selected_a_file ,
371
+ outputs = [
372
+ delete_file_button ,
373
+ deselect_file_button ,
374
+ selected_text ,
375
+ ],
376
+ )
377
+ delete_file_button .click (
378
+ self ._delete_selected_file ,
379
+ outputs = [
380
+ ingested_dataset ,
381
+ delete_file_button ,
382
+ deselect_file_button ,
383
+ selected_text ,
384
+ ],
385
+ )
386
+ delete_files_button .click (
387
+ self ._delete_all_files ,
388
+ outputs = [
389
+ ingested_dataset ,
390
+ delete_file_button ,
391
+ deselect_file_button ,
392
+ selected_text ,
393
+ ],
394
+ )
263
395
system_prompt_input = gr .Textbox (
264
396
placeholder = self ._system_prompt ,
265
397
label = "System Prompt" ,
0 commit comments