Skip to content

Commit ca95e26

Browse files
JslYoonclaude
andauthored
Lightspeed cherry pick clean (#3456)
* documents verified before adding another one in notebooks Signed-off-by: Lucas <lyoon@redhat.com> # Conflicts: # workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts * adding changeset Signed-off-by: Lucas <lyoon@redhat.com> * Update API reports for new translation key Add notebook.view.documents.uploadsInProgress to lightspeed translation API Signed-off-by: Lucas <lyoon@redhat.com> * address comments Signed-off-by: Lucas <lyoon@redhat.com> * fix document deletion Signed-off-by: Lucas <lyoon@redhat.com> * fix: restore SSE end event, fix upload gating, and refactor conversation deletion - Restore legacy 'end' event emission in notebooksRouters SSE transform - Extract citations from file_search tool calls for referenced_documents - Fix DocumentSidebar to respect hasUploadsInProgress when disabling Add button - Add conversations API to VectorStoresOperator for proper abstraction - Update SessionService to use conversations.delete instead of internal baseURL access Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(lightspeed): update changeset from minor to patch * fix(lightspeed): update changeset description for backport --------- Signed-off-by: Lucas <lyoon@redhat.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 230b90c commit ca95e26

11 files changed

Lines changed: 121 additions & 25 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-lightspeed': patch
3+
---
4+
5+
Backport: Fix document upload gating and conversation deletion in notebooks
6+
7+
This backports fixes from commits f7d96f8, e49fc2c, and d5199d6 on main to the 1.10 release line:
8+
9+
- Prevent additional document uploads while another document is still being processed, avoiding race conditions in the notebook vector store
10+
- Fix document deletion to use the proper conversations API abstraction instead of direct internal base URL access

workspaces/lightspeed/plugins/lightspeed-backend/src/service/constant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const DEFAULT_LIGHTSPEED_SERVICE_HOST = '127.0.0.1'; // Lightspeed core s
2626
export const DEFAULT_LIGHTSPEED_SERVICE_PORT = 8080; // Lightspeed service port
2727
export const DEFAULT_MAX_FILE_SIZE_MB = 20 * 1024 * 1024; // 20MB
2828
export const NOTEBOOKS_SYSTEM_PROMPT = `
29-
You are a helpful, analytical Senior Research Analyst assistant. Your primary objective is to synthesize cross-document information to answer user queries with 100% fidelity to the provided documents.
29+
You are a helpful, analytical Research Analyst assistant. Your primary objective is to synthesize cross-document information to answer user queries with 100% fidelity to the provided documents.
3030
3131
### QUERY TYPES - IMPORTANT
3232
* **Meta Queries ONLY:** ONLY when the user asks specifically about YOU as an assistant (e.g., "who are you", "what can you do", "hello"), respond naturally without requiring documents.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/VectorStoresOperator.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,4 +487,40 @@ export class VectorStoresOperator {
487487
return response.json();
488488
},
489489
};
490+
491+
/**
492+
* Conversations API
493+
*/
494+
conversations = {
495+
/**
496+
* Delete a conversation
497+
* DELETE /v2/conversations/{conversation_id}
498+
*/
499+
delete: async (conversationId: string, userId: string): Promise<void> => {
500+
this.logger.debug(
501+
`VectorStoresOperator: Deleting conversation ${conversationId}`,
502+
);
503+
504+
const response = await fetch(
505+
`${this.baseURL}/v2/conversations/${encodeURIComponent(conversationId)}?user_id=${encodeURIComponent(userId)}`,
506+
{
507+
method: 'DELETE',
508+
headers: {
509+
'Content-Type': 'application/json',
510+
},
511+
},
512+
);
513+
514+
if (!response.ok) {
515+
this.logger.warn(
516+
`Failed to delete conversation ${conversationId}: HTTP ${response.status}`,
517+
);
518+
throw new Error(
519+
`Failed to delete conversation: HTTP ${response.status}`,
520+
);
521+
}
522+
523+
this.logger.info(`Deleted conversation ${conversationId}`);
524+
},
525+
};
490526
}

workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ export async function createNotebooksRouter(
482482
input: query,
483483
instructions: systemPrompt,
484484
tools: [{ type: 'file_search', vector_store_ids: [sessionId] }],
485+
tool_choice: 'required',
485486
model: `${queryProvider}/${queryModel}`,
486487
stream: true,
487488
temperature: 0.35,

workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,32 +184,55 @@ export class SessionService {
184184
* @throws NotAllowedError if user does not own the session
185185
*/
186186
async deleteSession(sessionId: string, userId: string): Promise<void> {
187-
// Verify ownership before deletion
188-
await this.readSession(sessionId, userId);
187+
// Verify ownership before deletion and get session details
188+
const session = await this.readSession(sessionId, userId);
189+
const conversationId = session.metadata?.conversation_id;
189190

190-
// Delete all underlying files from Files API to prevent orphans
191+
// Delete associated conversation if it exists
192+
if (conversationId) {
193+
try {
194+
await this.client.conversations.delete(conversationId, userId);
195+
this.logger.info(
196+
`Deleted conversation ${conversationId} for session ${sessionId}`,
197+
);
198+
} catch (error) {
199+
this.logger.warn(
200+
`Failed to delete conversation ${conversationId}: ${error}`,
201+
);
202+
}
203+
}
204+
205+
// Delete all vector store files (same pattern as documentService.deleteDocument)
191206
try {
192207
const filesResponse =
193208
await this.client.vectorStores.files.list(sessionId);
194-
const fileIds = filesResponse.data?.map((f: any) => f.file_id) || [];
209+
const files = filesResponse.data || [];
210+
211+
this.logger.info(
212+
`Deleting ${files.length} vector store files for session ${sessionId}`,
213+
);
195214

196215
await Promise.all(
197-
fileIds.map(async (fileId: string) => {
216+
files.map(async (file: any) => {
198217
try {
199-
await this.client.files.delete(fileId);
200-
this.logger.info(`Deleted file ${fileId} from Files API`);
218+
await this.client.vectorStores.files.delete(sessionId, file.id);
219+
this.logger.info(
220+
`Deleted vector store file ${file.id} (${file.attributes?.title || 'untitled'})`,
221+
);
201222
} catch (error) {
202-
this.logger.warn(`Failed to delete file ${fileId}: ${error}`);
223+
this.logger.warn(
224+
`Failed to delete vector store file ${file.id}: ${error}`,
225+
);
203226
}
204227
}),
205228
);
206229
} catch (error) {
207230
this.logger.warn(
208-
`Failed to clean up files for session ${sessionId}: ${error}`,
231+
`Failed to clean up vector store files for session ${sessionId}: ${error}`,
209232
);
210233
}
211234

212-
// Delete the vector store (cascade deletes vector store files)
235+
// Delete the vector store itself
213236
await this.client.vectorStores.delete(sessionId);
214237
this.logger.info(`Session ${sessionId} deleted`);
215238
}

workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export const lightspeedTranslationRef: TranslationRef<
241241
readonly 'notebook.view.sidebar.resize': string;
242242
readonly 'notebook.view.documents.uploading': string;
243243
readonly 'notebook.view.documents.maxReached': string;
244+
readonly 'notebook.view.documents.uploadsInProgress': string;
244245
readonly 'notebook.upload.failed': string;
245246
readonly 'notebook.upload.modal.title': string;
246247
readonly 'notebook.upload.modal.dragDropTitle': string;

workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -694,9 +694,8 @@ export const LightspeedChat = ({
694694
[],
695695
);
696696
const createNotebookMutation = useCreateNotebook();
697-
const { data: notebookDocuments = [] } = useNotebookDocuments(
698-
activeNotebook?.session_id,
699-
);
697+
const { data: notebookDocuments = [], isFetching: isDocumentsFetching } =
698+
useNotebookDocuments(activeNotebook?.session_id);
700699
const [conversationId, setConversationId] = useState<string>('');
701700
const [requestId, setRequestId] = useState<string>('');
702701
const [newChatCreated, setNewChatCreated] = useState<boolean>(false);
@@ -2085,6 +2084,7 @@ export const LightspeedChat = ({
20852084
sessionId={activeNotebook.session_id}
20862085
notebookName={activeNotebook.name}
20872086
documents={notebookDocuments}
2087+
isDocumentsFetching={isDocumentsFetching}
20882088
metadata={activeNotebook.metadata}
20892089
topicSummary={
20902090
conversations.find(

workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ type AddDocumentModalProps = {
113113
onClose: () => void;
114114
sessionId: string;
115115
existingDocumentNames: string[];
116+
hasUploadsInProgress?: boolean;
116117
onFilesUploading?: (files: File[]) => void;
117118
onUploadStarted?: (info: { fileName: string; documentId: string }) => void;
118119
onUploadFailed?: (fileName: string) => void;
@@ -126,6 +127,7 @@ export const AddDocumentModal = ({
126127
onClose,
127128
sessionId,
128129
existingDocumentNames,
130+
hasUploadsInProgress,
129131
onFilesUploading,
130132
onUploadStarted,
131133
onUploadFailed,
@@ -261,6 +263,12 @@ export const AddDocumentModal = ({
261263
</Alert>
262264
)}
263265

266+
{hasUploadsInProgress && (
267+
<Alert severity="info" className={classes.errorAlert}>
268+
{t('notebook.view.documents.uploadsInProgress')}
269+
</Alert>
270+
)}
271+
264272
{remainingSlots > 0 && (
265273
<MultipleFileUpload
266274
className={classes.dropzone}
@@ -320,7 +328,7 @@ export const AddDocumentModal = ({
320328
className={classes.addButton}
321329
variant="contained"
322330
color="primary"
323-
disabled={selectedFiles.length === 0}
331+
disabled={selectedFiles.length === 0 || hasUploadsInProgress}
324332
>
325333
{(t as Function)('notebook.upload.modal.addButton', {
326334
count: selectedFiles.length,

workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ type DocumentSidebarProps = {
129129
completedFileNames?: Set<string>;
130130
deletingDocumentIds?: Set<string>;
131131
collapsed: boolean;
132+
hasUploadsInProgress?: boolean;
132133
onToggleCollapse: () => void;
133134
onAddDocument: () => void;
134135
onDeleteDocument?: (documentId: string) => void;
@@ -141,6 +142,7 @@ export const DocumentSidebar = ({
141142
completedFileNames,
142143
deletingDocumentIds,
143144
collapsed,
145+
hasUploadsInProgress,
144146
onToggleCollapse,
145147
onAddDocument,
146148
onDeleteDocument,
@@ -158,7 +160,8 @@ export const DocumentSidebar = ({
158160
name => !uploadedNames.has(name),
159161
);
160162
const totalCount = documents.length + activePending.length;
161-
const isAddDisabled = totalCount >= NOTEBOOK_MAX_FILES;
163+
const isAddDisabled =
164+
totalCount >= NOTEBOOK_MAX_FILES || hasUploadsInProgress;
162165

163166
return (
164167
<div className={classes.sidebar}>
@@ -184,10 +187,14 @@ export const DocumentSidebar = ({
184187
</Typography>
185188
{isAddDisabled ? (
186189
<Tooltip
187-
content={t('notebook.view.documents.maxReached')}
190+
content={
191+
hasUploadsInProgress
192+
? t('notebook.view.documents.uploadsInProgress')
193+
: t('notebook.view.documents.maxReached')
194+
}
188195
position="top"
189196
>
190-
<span>
197+
<Typography component="div">
191198
<Button
192199
variant="link"
193200
className={classes.addButton}
@@ -196,7 +203,7 @@ export const DocumentSidebar = ({
196203
>
197204
{t('notebook.view.documents.add')}
198205
</Button>
199-
</span>
206+
</Typography>
200207
</Tooltip>
201208
) : (
202209
<Button

workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ type NotebookViewProps = {
270270
sessionId: string;
271271
notebookName?: string;
272272
documents?: SessionDocument[];
273+
isDocumentsFetching?: boolean;
273274
metadata?: NotebookSessionMetadata;
274275
topicSummary?: string;
275276
userName?: string;
@@ -283,6 +284,7 @@ export const NotebookView = ({
283284
sessionId,
284285
notebookName = UNTITLED_NOTEBOOK_NAME,
285286
documents = [],
287+
isDocumentsFetching = false,
286288
metadata,
287289
topicSummary,
288290
userName,
@@ -548,7 +550,9 @@ export const NotebookView = ({
548550

549551
const hasDocuments = documents.length > 0 || uploadingFileNames.length > 0;
550552
const totalDocumentCount = documents.length + uploadingFileNames.length;
551-
const isAddDisabled = totalDocumentCount >= NOTEBOOK_MAX_FILES;
553+
const hasUploadsInProgress = pendingUploads.length > 0 || isDocumentsFetching;
554+
const isAddDisabled =
555+
totalDocumentCount >= NOTEBOOK_MAX_FILES || hasUploadsInProgress;
552556

553557
const panelContent = (
554558
<DrawerPanelContent
@@ -565,6 +569,7 @@ export const NotebookView = ({
565569
completedFileNames={completedFileNames}
566570
deletingDocumentIds={deletingDocumentIds}
567571
collapsed={sidebarCollapsed}
572+
hasUploadsInProgress={hasUploadsInProgress}
568573
onToggleCollapse={() => setSidebarCollapsed(prev => !prev)}
569574
onAddDocument={handleOpenUploadModal}
570575
onDeleteDocument={handleDeleteDocument}
@@ -694,11 +699,13 @@ export const NotebookView = ({
694699
</Button>
695700
</Tooltip>
696701
<Tooltip
697-
content={
698-
isAddDisabled
699-
? t('notebook.view.documents.maxReached')
700-
: t('notebook.view.documents.add')
701-
}
702+
content={(() => {
703+
if (hasUploadsInProgress)
704+
return t('notebook.view.documents.uploadsInProgress');
705+
if (isAddDisabled)
706+
return t('notebook.view.documents.maxReached');
707+
return t('notebook.view.documents.add');
708+
})()}
702709
position="right"
703710
>
704711
<Typography component="span">
@@ -778,6 +785,7 @@ export const NotebookView = ({
778785
onClose={handleCloseUploadModal}
779786
sessionId={sessionId}
780787
existingDocumentNames={documents.map(d => d.title)}
788+
hasUploadsInProgress={hasUploadsInProgress}
781789
onFilesUploading={handleFilesUploading}
782790
onUploadStarted={handleUploadStarted}
783791
onUploadFailed={handleUploadFailed}

0 commit comments

Comments
 (0)