Skip to content

Commit aa13afd

Browse files
imartinezRobinsane
andauthored
feat(UI): Select file to Query or Delete + Delete ALL (#1612)
--------- Co-authored-by: Robin Boone <[email protected]>
1 parent 24fb80c commit aa13afd

File tree

6 files changed

+161
-18
lines changed

6 files changed

+161
-18
lines changed

poetry.lock

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

private_gpt/settings/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ class UISettings(BaseModel):
189189
default_query_system_prompt: str = Field(
190190
None, description="The default system prompt to use for the query mode."
191191
)
192+
delete_file_button_enabled: bool = Field(
193+
True, description="If the button to delete a file is enabled or not."
194+
)
195+
delete_all_files_button_enabled: bool = Field(
196+
False, description="If the button to delete all files is enabled or not."
197+
)
192198

193199

194200
class QdrantSettings(BaseModel):

private_gpt/ui/ui.py

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from private_gpt.constants import PROJECT_ROOT_PATH
1717
from private_gpt.di import global_injector
18+
from private_gpt.open_ai.extensions.context_filter import ContextFilter
1819
from private_gpt.server.chat.chat_service import ChatService, CompletionGen
1920
from private_gpt.server.chunks.chunks_service import Chunk, ChunksService
2021
from private_gpt.server.ingest.ingest_service import IngestService
@@ -31,7 +32,7 @@
3132

3233
SOURCES_SEPARATOR = "\n\n Sources: \n"
3334

34-
MODES = ["Query Docs", "Search in Docs", "LLM Chat"]
35+
MODES = ["Query Files", "Search Files", "LLM Chat (no context from files)"]
3536

3637

3738
class Source(BaseModel):
@@ -74,6 +75,8 @@ def __init__(
7475
# Cache the UI blocks
7576
self._ui_block = None
7677

78+
self._selected_filename = None
79+
7780
# Initialize system prompt based on default mode
7881
self.mode = MODES[0]
7982
self._system_prompt = self._get_default_system_prompt(self.mode)
@@ -132,20 +135,34 @@ def build_history() -> list[ChatMessage]:
132135
),
133136
)
134137
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+
136152
query_stream = self._chat_service.stream_chat(
137153
messages=all_messages,
138154
use_context=True,
155+
context_filter=context_filter,
139156
)
140157
yield from yield_deltas(query_stream)
141-
case "LLM Chat":
158+
case "LLM Chat (no context from files)":
142159
llm_stream = self._chat_service.stream_chat(
143160
messages=all_messages,
144161
use_context=False,
145162
)
146163
yield from yield_deltas(llm_stream)
147164

148-
case "Search in Docs":
165+
case "Search Files":
149166
response = self._chunks_service.retrieve_relevant(
150167
text=message, limit=4, prev_next_chunks=0
151168
)
@@ -166,10 +183,10 @@ def _get_default_system_prompt(mode: str) -> str:
166183
p = ""
167184
match mode:
168185
# For query chat mode, obtain default system prompt from settings
169-
case "Query Docs":
186+
case "Query Files":
170187
p = settings().ui.default_query_system_prompt
171188
# For chat mode, obtain default system prompt from settings
172-
case "LLM Chat":
189+
case "LLM Chat (no context from files)":
173190
p = settings().ui.default_chat_system_prompt
174191
# For any other mode, clear the system prompt
175192
case _:
@@ -205,8 +222,71 @@ def _list_ingested_files(self) -> list[list[str]]:
205222
def _upload_file(self, files: list[str]) -> None:
206223
logger.debug("Loading count=%s files", len(files))
207224
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+
208243
self._ingest_service.bulk_ingest([(str(path.name), path) for path in paths])
209244

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+
210290
def _build_ui_blocks(self) -> gr.Blocks:
211291
logger.debug("Creating the UI blocks")
212292
with gr.Blocks(
@@ -235,7 +315,7 @@ def _build_ui_blocks(self) -> gr.Blocks:
235315
mode = gr.Radio(
236316
MODES,
237317
label="Mode",
238-
value="Query Docs",
318+
value="Query Files",
239319
)
240320
upload_button = gr.components.UploadButton(
241321
"Upload File(s)",
@@ -247,6 +327,7 @@ def _build_ui_blocks(self) -> gr.Blocks:
247327
self._list_ingested_files,
248328
headers=["File name"],
249329
label="Ingested Files",
330+
height=235,
250331
interactive=False,
251332
render=False, # Rendered under the button
252333
)
@@ -260,6 +341,57 @@ def _build_ui_blocks(self) -> gr.Blocks:
260341
outputs=ingested_dataset,
261342
)
262343
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+
)
263395
system_prompt_input = gr.Textbox(
264396
placeholder=self._system_prompt,
265397
label="System Prompt",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ types-pyyaml = "^6.0.12.12"
3131
[tool.poetry.group.ui]
3232
optional = true
3333
[tool.poetry.group.ui.dependencies]
34-
gradio = "^4.4.1"
34+
gradio = "^4.19.0"
3535

3636
[tool.poetry.group.local]
3737
optional = true

scripts/ingest_folder.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ def __init__(self, ingest_service: IngestService) -> None:
1818
self.total_documents = 0
1919
self.current_document_count = 0
2020

21-
self._files_under_root_folder: list[Path] = list()
21+
self._files_under_root_folder: list[Path] = []
2222

2323
def _find_all_files_in_folder(self, root_path: Path, ignored: list[str]) -> None:
2424
"""Search all files under the root folder recursively.
25+
2526
Count them at the same time
2627
"""
2728
for file_path in root_path.iterdir():

settings.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ ui:
3131
You can only answer questions about the provided context.
3232
If you know the answer but it is not based in the provided context, don't provide
3333
the answer, just state the answer is not in the context provided.
34+
delete_file_button_enabled: true
35+
delete_all_files_button_enabled: true
36+
3437

3538
llm:
3639
mode: local

0 commit comments

Comments
 (0)