Skip to content

Commit f4d366d

Browse files
authored
Merge pull request #177 from rh-ai-quickstart/release/v0.2.41
Release v0.2.41
2 parents df06d61 + ccf4b5b commit f4d366d

10 files changed

Lines changed: 261 additions & 20 deletions

File tree

deploy/helm/rag-values.yaml.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ llama-stack:
180180
# OPENAI_API_KEY: "your_openai_key_here"
181181
# ANTHROPIC_API_KEY: "your_anthropic_key_here"
182182

183+
# File Processors Configuration
184+
#
185+
# Available providers:
186+
# inline::pypdf - PDF extraction (lightweight, uses PyPDF)
187+
#
188+
fileProcessors:
189+
enabled: true
190+
providers:
191+
- provider_id: pypdf
192+
provider_type: inline::pypdf
193+
183194
# Suggested Questions Configuration
184195
# These questions appear in the chat UI when users select a database
185196
# The key should match the vector_store_name (identifier) of the database

deploy/helm/rag/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ apiVersion: v2
22
name: rag
33
description: A Helm chart for Kubernetes
44
type: application
5-
version: 0.2.40
6-
appVersion: "0.2.40"
5+
version: 0.2.41
6+
appVersion: "0.2.41"
77

88
dependencies:
99
- name: pgvector

deploy/helm/rag/values.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ replicaCount: 1
33
image:
44
repository: quay.io/rh-ai-quickstart/llamastack-dist-ui
55
pullPolicy: Always
6-
tag: 0.2.40
6+
tag: 0.2.41
77

88
service:
99
type: ClusterIP
@@ -165,6 +165,19 @@ pgvector:
165165
host: pgvector
166166
port: "5432"
167167

168+
# Create a separate vector database for each ingestion pipeline
169+
extraDatabases:
170+
- name: hr_vector_db
171+
vectordb: true
172+
- name: legal_vector_db
173+
vectordb: true
174+
- name: sales_vector_db
175+
vectordb: true
176+
- name: procurement_vector_db
177+
vectordb: true
178+
- name: techsupport_vector_db
179+
vectordb: true
180+
168181
# Upload sample files to the minio bucket
169182
sampleFileUpload:
170183
enabled: true
@@ -176,6 +189,12 @@ pgvector:
176189

177190
llama-stack:
178191
enabled: true
192+
fileProcessors:
193+
enabled: true
194+
providers:
195+
- provider_id: pypdf
196+
provider_type: inline::pypdf
197+
179198
secrets:
180199
TAVILY_SEARCH_API_KEY: "Paste-your-key-here"
181200

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the terms described in the LICENSE file in
5+
# the root directory of this source tree.
6+
7+
import io
8+
import logging
9+
import os
10+
11+
from docx import Document
12+
from openpyxl import load_workbook
13+
14+
logger = logging.getLogger(__name__)
15+
16+
LOCAL_SUPPORTED_EXTENSIONS = [".docx", ".xlsx"]
17+
PROVIDER_SUPPORTED_EXTENSIONS = [".txt", ".pdf", ".md"]
18+
19+
20+
def extract_text_from_docx(file) -> str:
21+
"""Extract all text content from a .docx file.
22+
23+
Reads paragraph text and table cell text from the document.
24+
25+
Args:
26+
file: File-like object containing .docx data
27+
28+
Returns:
29+
str: Extracted text with paragraphs separated by newlines
30+
"""
31+
doc = Document(file)
32+
parts = [p.text for p in doc.paragraphs]
33+
34+
for table in doc.tables:
35+
for row in table.rows:
36+
for cell in row.cells:
37+
parts.append(cell.text)
38+
39+
return "\n".join(parts)
40+
41+
42+
def extract_text_from_xlsx(file) -> str:
43+
"""Extract all text content from an .xlsx file.
44+
45+
Reads each sheet and converts rows to tab-separated values.
46+
47+
Args:
48+
file: File-like object containing .xlsx data
49+
50+
Returns:
51+
str: Extracted text with sheet headers and tab-separated row values
52+
"""
53+
wb = load_workbook(file, read_only=True)
54+
parts = []
55+
56+
for sheet_name in wb.sheetnames:
57+
ws = wb[sheet_name]
58+
parts.append(f"Sheet: {sheet_name}")
59+
for row in ws.iter_rows(values_only=True):
60+
row_text = "\t".join(
61+
str(cell) if cell is not None else "" for cell in row
62+
)
63+
parts.append(row_text)
64+
65+
wb.close()
66+
return "\n".join(parts)
67+
68+
69+
def extract_text(file, filename: str) -> str:
70+
"""Extract text from a locally supported file type.
71+
72+
Routes to the appropriate extractor based on file extension.
73+
74+
Args:
75+
file: File-like object with document data
76+
filename: Original filename used to determine the file type
77+
78+
Returns:
79+
str: Extracted plain text content
80+
81+
Raises:
82+
ValueError: If the file extension is not locally supported
83+
"""
84+
ext = os.path.splitext(filename)[1].lower()
85+
86+
if ext == ".docx":
87+
return extract_text_from_docx(file)
88+
elif ext == ".xlsx":
89+
return extract_text_from_xlsx(file)
90+
else:
91+
raise ValueError(f"Unsupported file type for local extraction: {ext}")
92+
93+
94+
def create_text_file_from_extracted_content(
95+
content: str, original_filename: str
96+
) -> io.BytesIO:
97+
"""Wrap extracted text as an in-memory .txt file for the Llama Stack API.
98+
99+
Creates a BytesIO object with .name and .size attributes so it can be
100+
passed directly to the files.create API endpoint.
101+
102+
Args:
103+
content: Extracted plain text to wrap
104+
original_filename: Original filename; the stem is reused with a .txt extension
105+
106+
Returns:
107+
io.BytesIO: In-memory text file ready for upload
108+
"""
109+
text_bytes = content.encode("utf-8")
110+
text_file = io.BytesIO(text_bytes)
111+
stem = os.path.splitext(original_filename)[0]
112+
text_file.name = f"{stem}.txt"
113+
text_file.size = len(text_bytes)
114+
return text_file

frontend/llama_stack_ui/distribution/ui/modules/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def strip_file_citations(text):
7979
"""
8080
text = re.sub(r'file<[^>]+>', '', text)
8181
text = re.sub(r'<\|file-[^|]*\|>', '', text)
82+
text = re.sub(r'<\|[0-9a-fA-F-]{8,}\|>', '', text)
8283
text = re.sub(r'【[^】]*†[^】]*】', '', text)
8384
text = re.sub(r' +', ' ', text)
8485
return text
@@ -92,6 +93,7 @@ def strip_file_citations_streaming(text):
9293
"""
9394
text = strip_file_citations(text)
9495
text = re.sub(r'<\|(?:f(?:i(?:l(?:e(?:-[^|]*)?)?)?)?)?\s*$', '', text)
96+
text = re.sub(r'<\|[0-9a-fA-F-]*$', '', text)
9597
text = re.sub(r'\bfile<[^>]*$', '', text)
9698
text = re.sub(r'【[^】]*$', '', text)
9799
return text

frontend/llama_stack_ui/distribution/ui/page/playground/chat.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,21 @@ def fetch_models_and_tools():
8686

8787
# Fetch models, excluding guardrail/shield models
8888
models = client.models.list()
89+
90+
def _get_model_id(model):
91+
return getattr(model, "identifier", None) or model.id
92+
93+
def _get_model_type(model):
94+
for attr in ("model_type", "api_model_type"):
95+
val = getattr(model, attr, None)
96+
if val is not None:
97+
return val
98+
meta = getattr(model, "custom_metadata", None) or {}
99+
return meta.get("model_type")
100+
89101
model_list = [
90-
model.identifier for model in models
91-
if model.api_model_type == "llm" and model.identifier not in shields_set
102+
_get_model_id(model) for model in models
103+
if _get_model_type(model) == "llm" and _get_model_id(model) not in shields_set
92104
]
93105

94106
# Fetch and categorize toolgroups

frontend/llama_stack_ui/distribution/ui/page/upload/upload.py

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
import streamlit as st
1212

1313
from llama_stack_ui.distribution.ui.modules.api import llama_stack_api
14+
from llama_stack_ui.distribution.ui.modules.local_extractors import (
15+
LOCAL_SUPPORTED_EXTENSIONS,
16+
PROVIDER_SUPPORTED_EXTENSIONS,
17+
create_text_file_from_extracted_content,
18+
extract_text,
19+
)
1420
from llama_stack_ui.distribution.ui.modules.utils import get_vector_db_name
1521

1622

@@ -21,6 +27,7 @@ def _init_upload_page_session_state():
2127
"creation_message": "",
2228
"selected_vector_db": "",
2329
"newly_created_vdb": None,
30+
"extraction_method": "provider",
2431
}
2532
for key, value in defaults.items():
2633
if key not in st.session_state:
@@ -189,6 +196,9 @@ def _create_vector_database(vdb_name):
189196
def _show_document_upload_ui(vector_db_name, vector_db_obj=None):
190197
"""Display UI for uploading documents to an existing vector database.
191198
199+
Shows an extraction method toggle that determines which file types are
200+
accepted and how they are processed before ingestion.
201+
192202
Args:
193203
vector_db_name (str): Name of the selected vector database
194204
vector_db_obj: The actual vector database object with identifier
@@ -200,44 +210,82 @@ def _show_document_upload_ui(vector_db_name, vector_db_obj=None):
200210

201211
_show_status("upload_status", "upload_message")
202212

213+
local_label = (
214+
"Docling ("
215+
+ ", ".join(LOCAL_SUPPORTED_EXTENSIONS) + ")"
216+
)
217+
provider_label = (
218+
"LlamaStack Provider ("
219+
+ ", ".join(PROVIDER_SUPPORTED_EXTENSIONS) + ")"
220+
)
221+
method_options = [provider_label, local_label]
222+
223+
selected_label = st.radio(
224+
"Extraction method",
225+
method_options,
226+
key="extraction_method_radio",
227+
horizontal=False,
228+
help="Local extraction converts .docx/.xlsx to text in the browser. "
229+
"LlamaStack Provider sends files directly to the server.",
230+
)
231+
232+
is_local = selected_label == local_label
233+
st.session_state["extraction_method"] = "local" if is_local else "provider"
234+
235+
if is_local:
236+
accepted_types = [ext.lstrip(".") for ext in LOCAL_SUPPORTED_EXTENSIONS]
237+
else:
238+
accepted_types = [ext.lstrip(".") for ext in PROVIDER_SUPPORTED_EXTENSIONS]
239+
203240
upload_key = f"processed_files_{vector_db_name}"
204241
if upload_key not in st.session_state:
205242
st.session_state[upload_key] = set()
206243

207244
uploaded_files = st.file_uploader(
208245
"Browse and select files to upload (files will upload automatically)",
209246
accept_multiple_files=True,
210-
type=["txt", "pdf", "doc", "docx", "md"],
211-
key=f"uploader_{vector_db_name}",
247+
type=accepted_types,
248+
key=f"uploader_{vector_db_name}_{st.session_state['extraction_method']}",
212249
help=(
213-
"Select one or more documents - they will be uploaded "
250+
"Select one or more documents they will be uploaded "
214251
"automatically to this vector database"
215252
),
216253
)
217254

218255
if uploaded_files:
219-
file_set_id = frozenset([f.name + str(f.size) for f in uploaded_files])
256+
new_files = [
257+
f for f in uploaded_files
258+
if f.name + str(f.size) not in st.session_state[upload_key]
259+
]
220260

221-
if file_set_id not in st.session_state[upload_key]:
222-
st.session_state[upload_key].add(file_set_id)
261+
if new_files:
262+
for f in new_files:
263+
st.session_state[upload_key].add(f.name + str(f.size))
223264

224265
if vector_db_obj and hasattr(vector_db_obj, 'id'):
225266
vector_db_id = vector_db_obj.id
226267
else:
227268
vector_db_id = vector_db_name
228269

229270
_upload_documents_to_database(
230-
vector_db_name, uploaded_files, vector_db_id
271+
vector_db_name,
272+
new_files,
273+
vector_db_id,
274+
extraction_method=st.session_state["extraction_method"],
231275
)
232276

233-
234-
def _upload_documents_to_database(vector_db_name, uploaded_files, vector_db_id=None):
277+
def _upload_documents_to_database(vector_db_name, uploaded_files, vector_db_id=None, extraction_method="provider"):
235278
"""Upload documents to an existing vector database.
236279
280+
When extraction_method is "local", files are first converted to plain text
281+
using the local extractors and the resulting .txt content is uploaded.
282+
When "provider", files are sent directly to the LlamaStack server.
283+
237284
Args:
238285
vector_db_name (str): Name of the target vector database
239286
uploaded_files: List of uploaded files from Streamlit file uploader
240287
vector_db_id (str): The actual database identifier for API calls
288+
extraction_method (str): "local" for client-side extraction, "provider" for server-side
241289
"""
242290
try:
243291
st.session_state["upload_status"] = None
@@ -251,16 +299,37 @@ def _upload_documents_to_database(vector_db_name, uploaded_files, vector_db_id=N
251299
actual_db_id = vector_db_id or vector_db_name
252300
uploaded_file_ids = []
253301

254-
with st.spinner(f"Uploading {len(uploaded_files)} file(s)..."):
302+
spinner_msg = (
303+
f"Extracting and uploading {len(uploaded_files)} file(s)..."
304+
if extraction_method == "local"
305+
else f"Uploading {len(uploaded_files)} file(s)..."
306+
)
307+
308+
with st.spinner(spinner_msg):
255309
for uploaded_file in uploaded_files:
310+
original_filename = uploaded_file.name
311+
312+
if extraction_method == "local":
313+
text_content = extract_text(uploaded_file, original_filename)
314+
file_to_upload = create_text_file_from_extracted_content(
315+
text_content, original_filename
316+
)
317+
else:
318+
file_to_upload = uploaded_file
319+
256320
file_response = llama_stack_api.client.files.create(
257-
file=uploaded_file,
321+
file=file_to_upload,
258322
purpose="assistants"
259323
)
260-
llama_stack_api.client.vector_stores.files.create(
261-
vector_store_id=actual_db_id,
262-
file_id=file_response.id,
263-
)
324+
325+
vs_file_kwargs = {
326+
"vector_store_id": actual_db_id,
327+
"file_id": file_response.id,
328+
}
329+
if extraction_method == "local":
330+
vs_file_kwargs["attributes"] = {"source": original_filename}
331+
332+
llama_stack_api.client.vector_stores.files.create(**vs_file_kwargs)
264333
uploaded_file_ids.append(file_response.id)
265334

266335
st.session_state["upload_status"] = "success"

0 commit comments

Comments
 (0)