Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/lightspeed/.changeset/khaki-tomatoes-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': minor
---

documents cannot be uploaded when a document processing is in progress
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const DEFAULT_LIGHTSPEED_SERVICE_HOST = '127.0.0.1'; // Lightspeed core s
export const DEFAULT_LIGHTSPEED_SERVICE_PORT = 8080; // Lightspeed service port
export const DEFAULT_MAX_FILE_SIZE_MB = 20 * 1024 * 1024; // 20MB
export const NOTEBOOKS_SYSTEM_PROMPT = `
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.
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.

### QUERY TYPES - IMPORTANT
* **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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,49 +226,14 @@ export async function createNotebooksRouter(
};
this.push(`data: ${JSON.stringify(legacy)}\n\n`);
} else if (eventType === 'response.completed') {
const usage = parsed?.response?.usage;

// Log the full response to see what we're getting
logger.info(
`Full response.completed event: ${JSON.stringify(parsed?.response, null, 2)}`,
);

// Extract citations/sources from tool calls (file_search results)
const toolCalls = parsed?.response?.tool_calls || [];
logger.info(
`Tool calls received: ${JSON.stringify(toolCalls, null, 2)}`,
);

const referencedDocuments: any[] = [];

for (const toolCall of toolCalls) {
if (toolCall.tool_name === 'file_search') {
logger.info(
`Found file_search tool call: ${JSON.stringify(toolCall, null, 2)}`,
);
const citations = toolCall.content?.citations || [];
for (const citation of citations) {
referencedDocuments.push({
document_id: citation.document_id || citation.file_id,
content: citation.text || citation.content,
});
}
}
}

logger.info(
`Referenced documents: ${JSON.stringify(referencedDocuments)}`,
);

const legacy = {
event: 'end',
data: {
referenced_documents: referencedDocuments,
input_tokens: usage?.input_tokens,
output_tokens: usage?.output_tokens,
},
};
this.push(`data: ${JSON.stringify(legacy)}\n\n`);
// this.push(`data: ${JSON.stringify(legacy)}\n\n`);
} else {
// Log unhandled event types to help identify what we're missing
logger.debug(`Unhandled SSE event type: ${eventType}`, parsed);
Expand Down Expand Up @@ -482,6 +447,7 @@ export async function createNotebooksRouter(
input: query,
instructions: systemPrompt,
tools: [{ type: 'file_search', vector_store_ids: [sessionId] }],
tool_choice: 'required',
model: `${queryProvider}/${queryModel}`,
stream: true,
temperature: 0.35,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,32 +184,72 @@ export class SessionService {
* @throws NotAllowedError if user does not own the session
*/
async deleteSession(sessionId: string, userId: string): Promise<void> {
// Verify ownership before deletion
await this.readSession(sessionId, userId);
// Verify ownership before deletion and get session details
const session = await this.readSession(sessionId, userId);
const conversationId = session.metadata?.conversation_id;

// Delete all underlying files from Files API to prevent orphans
// Delete associated conversation if it exists
if (conversationId) {
try {
// Access the baseURL from the VectorStoresOperator client
const baseURL = (this.client as any).baseURL;
const response = await fetch(
`${baseURL}/v2/conversations/${encodeURIComponent(conversationId)}?user_id=${encodeURIComponent(userId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
},
);

if (!response.ok) {
this.logger.warn(
`Failed to delete conversation ${conversationId}: HTTP ${response.status}`,
);
} else {
this.logger.info(
`Deleted conversation ${conversationId} for session ${sessionId}`,
);
}
} catch (error) {
this.logger.warn(
`Failed to delete conversation ${conversationId}: ${error}`,
);
}
}

// Delete all vector store files (same pattern as documentService.deleteDocument)
try {
const filesResponse =
await this.client.vectorStores.files.list(sessionId);
const fileIds = filesResponse.data?.map((f: any) => f.file_id) || [];
const files = filesResponse.data || [];

this.logger.info(
`Deleting ${files.length} vector store files for session ${sessionId}`,
);

await Promise.all(
fileIds.map(async (fileId: string) => {
files.map(async (file: any) => {
try {
await this.client.files.delete(fileId);
this.logger.info(`Deleted file ${fileId} from Files API`);
await this.client.vectorStores.files.delete(sessionId, file.id);
this.logger.info(
`Deleted vector store file ${file.id} (${file.attributes?.title || 'untitled'})`,
);
} catch (error) {
this.logger.warn(`Failed to delete file ${fileId}: ${error}`);
this.logger.warn(
`Failed to delete vector store file ${file.id}: ${error}`,
);
}
}),
);
} catch (error) {
this.logger.warn(
`Failed to clean up files for session ${sessionId}: ${error}`,
`Failed to clean up vector store files for session ${sessionId}: ${error}`,
);
}

// Delete the vector store (cascade deletes vector store files)
// Delete the vector store itself
await this.client.vectorStores.delete(sessionId);
this.logger.info(`Session ${sessionId} deleted`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const lightspeedTranslationRef: TranslationRef<
readonly 'notebook.view.sidebar.resize': string;
readonly 'notebook.view.documents.uploading': string;
readonly 'notebook.view.documents.maxReached': string;
readonly 'notebook.view.documents.uploadsInProgress': string;
readonly 'notebook.upload.failed': string;
readonly 'notebook.upload.modal.title': string;
readonly 'notebook.upload.modal.dragDropTitle': string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,9 +694,8 @@ export const LightspeedChat = ({
[],
);
const createNotebookMutation = useCreateNotebook();
const { data: notebookDocuments = [] } = useNotebookDocuments(
activeNotebook?.session_id,
);
const { data: notebookDocuments = [], isFetching: isDocumentsFetching } =
useNotebookDocuments(activeNotebook?.session_id);
const [conversationId, setConversationId] = useState<string>('');
const [requestId, setRequestId] = useState<string>('');
const [newChatCreated, setNewChatCreated] = useState<boolean>(false);
Expand Down Expand Up @@ -2085,6 +2084,7 @@ export const LightspeedChat = ({
sessionId={activeNotebook.session_id}
notebookName={activeNotebook.name}
documents={notebookDocuments}
isDocumentsFetching={isDocumentsFetching}
metadata={activeNotebook.metadata}
topicSummary={
conversations.find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ type AddDocumentModalProps = {
onClose: () => void;
sessionId: string;
existingDocumentNames: string[];
hasUploadsInProgress?: boolean;
onFilesUploading?: (files: File[]) => void;
onUploadStarted?: (info: { fileName: string; documentId: string }) => void;
onUploadFailed?: (fileName: string) => void;
Expand All @@ -126,6 +127,7 @@ export const AddDocumentModal = ({
onClose,
sessionId,
existingDocumentNames,
hasUploadsInProgress,
onFilesUploading,
onUploadStarted,
onUploadFailed,
Expand Down Expand Up @@ -261,6 +263,12 @@ export const AddDocumentModal = ({
</Alert>
)}

{hasUploadsInProgress && (
<Alert severity="info" className={classes.errorAlert}>
{t('notebook.view.documents.uploadsInProgress')}
</Alert>
)}

{remainingSlots > 0 && (
<MultipleFileUpload
className={classes.dropzone}
Expand Down Expand Up @@ -320,7 +328,7 @@ export const AddDocumentModal = ({
className={classes.addButton}
variant="contained"
color="primary"
disabled={selectedFiles.length === 0}
disabled={selectedFiles.length === 0 || hasUploadsInProgress}
>
{(t as Function)('notebook.upload.modal.addButton', {
count: selectedFiles.length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type DocumentSidebarProps = {
completedFileNames?: Set<string>;
deletingDocumentIds?: Set<string>;
collapsed: boolean;
hasUploadsInProgress?: boolean;
onToggleCollapse: () => void;
onAddDocument: () => void;
onDeleteDocument?: (documentId: string) => void;
Expand All @@ -141,6 +142,7 @@ export const DocumentSidebar = ({
completedFileNames,
deletingDocumentIds,
collapsed,
hasUploadsInProgress,
onToggleCollapse,
onAddDocument,
onDeleteDocument,
Expand Down Expand Up @@ -184,10 +186,14 @@ export const DocumentSidebar = ({
</Typography>
{isAddDisabled ? (
<Tooltip
content={t('notebook.view.documents.maxReached')}
content={
hasUploadsInProgress
? t('notebook.view.documents.uploadsInProgress')
: t('notebook.view.documents.maxReached')
}
position="top"
>
<span>
<Typography component="div">
<Button
variant="link"
className={classes.addButton}
Expand All @@ -196,7 +202,7 @@ export const DocumentSidebar = ({
>
{t('notebook.view.documents.add')}
</Button>
</span>
</Typography>
</Tooltip>
) : (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ type NotebookViewProps = {
sessionId: string;
notebookName?: string;
documents?: SessionDocument[];
isDocumentsFetching?: boolean;
metadata?: NotebookSessionMetadata;
topicSummary?: string;
userName?: string;
Expand All @@ -283,6 +284,7 @@ export const NotebookView = ({
sessionId,
notebookName = UNTITLED_NOTEBOOK_NAME,
documents = [],
isDocumentsFetching = false,
metadata,
topicSummary,
userName,
Expand Down Expand Up @@ -548,7 +550,9 @@ export const NotebookView = ({

const hasDocuments = documents.length > 0 || uploadingFileNames.length > 0;
const totalDocumentCount = documents.length + uploadingFileNames.length;
const isAddDisabled = totalDocumentCount >= NOTEBOOK_MAX_FILES;
const hasUploadsInProgress = pendingUploads.length > 0 || isDocumentsFetching;
const isAddDisabled =
totalDocumentCount >= NOTEBOOK_MAX_FILES || hasUploadsInProgress;

const panelContent = (
<DrawerPanelContent
Expand All @@ -565,6 +569,7 @@ export const NotebookView = ({
completedFileNames={completedFileNames}
deletingDocumentIds={deletingDocumentIds}
collapsed={sidebarCollapsed}
hasUploadsInProgress={hasUploadsInProgress}
onToggleCollapse={() => setSidebarCollapsed(prev => !prev)}
onAddDocument={handleOpenUploadModal}
onDeleteDocument={handleDeleteDocument}
Expand Down Expand Up @@ -694,11 +699,13 @@ export const NotebookView = ({
</Button>
</Tooltip>
<Tooltip
content={
isAddDisabled
? t('notebook.view.documents.maxReached')
: t('notebook.view.documents.add')
}
content={(() => {
if (hasUploadsInProgress)
return t('notebook.view.documents.uploadsInProgress');
if (isAddDisabled)
return t('notebook.view.documents.maxReached');
return t('notebook.view.documents.add');
})()}
position="right"
>
<Typography component="span">
Expand Down Expand Up @@ -778,6 +785,7 @@ export const NotebookView = ({
onClose={handleCloseUploadModal}
sessionId={sessionId}
existingDocumentNames={documents.map(d => d.title)}
hasUploadsInProgress={hasUploadsInProgress}
onFilesUploading={handleFilesUploading}
onUploadStarted={handleUploadStarted}
onUploadFailed={handleUploadFailed}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export const lightspeedMessages = {
'notebook.view.documents.uploading': 'Uploading document',
'notebook.view.documents.maxReached':
'Maximum 10 documents are allowed. Delete a document to upload a new document.',
'notebook.view.documents.uploadsInProgress':
'Please wait for current uploads to complete before adding more documents.',
'notebook.upload.failed': '"{{fileName}}" upload failed.',

// Notebook upload modal
Expand Down
Loading