diff --git a/workspaces/lightspeed/.changeset/chilled-guests-search.md b/workspaces/lightspeed/.changeset/chilled-guests-search.md new file mode 100644 index 00000000000..79b28823f10 --- /dev/null +++ b/workspaces/lightspeed/.changeset/chilled-guests-search.md @@ -0,0 +1,6 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed-backend': minor +--- + +All lightspeed query is now called with rhdh-docs vector_store. Notebooks app-config only now requires queryDefaults model and provider +All files uploaded to lightspeed-stack will be converted to .txt diff --git a/workspaces/lightspeed/app-config.yaml b/workspaces/lightspeed/app-config.yaml index 31819e32c4a..2f65ace298a 100644 --- a/workspaces/lightspeed/app-config.yaml +++ b/workspaces/lightspeed/app-config.yaml @@ -21,12 +21,8 @@ lightspeed: notebooks: enabled: false queryDefaults: - model: redhataillama-31-8b-instruct - provider_id: vllm - sessionDefaults: - provider_id: notebooks - embedding_model: ${LLAMA_STACK_EMBEDDING_MODEL} - embedding_dimension: 768 + model: ${NOTEBOOKS_QUERY_MODEL} + provider_id: ${NOTEBOOKS_QUERY_PROVIDER_ID} backend: # Used for enabling authentication, secret is shared by all backend plugins diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/README.md b/workspaces/lightspeed/plugins/lightspeed-backend/README.md index f46cbd3eb39..a05d27622a5 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/README.md +++ b/workspaces/lightspeed/plugins/lightspeed-backend/README.md @@ -110,15 +110,8 @@ lightspeed: # Required: Query defaults for RAG queries # Both model and provider_id must be configured together queryDefaults: - model: llama3.1-8b-instruct # Model to use for answering queries - provider_id: ollama # AI provider for the query model - - # Required: Session defaults for creating vector stores - # All three fields are required when Notebooks is enabled - sessionDefaults: - provider_id: notebooks # Vector store provider ID (must match Llama Stack config) - embedding_model: sentence-transformers/all-mpnet-base-v2 # Model for generating embeddings - embedding_dimension: 768 # Embedding vector dimension (must match model output) + model: ${NOTEBOOKS_QUERY_MODEL} # Model to use for answering queries. Must map to a model available through the provider set in $NOTEBOOKS_QUERY_PROVIDER_ID + provider_id: ${NOTEBOOKS_QUERY_PROVIDER_ID} # AI provider for the query model. Must map to a provider enabled in your Lightspeed config.yaml # Optional: Chunking strategy for document processing chunkingStrategy: @@ -143,11 +136,7 @@ lightspeed: - **`queryDefaults.model`** _(required)_: The LLM model to use for answering RAG queries. Must be available in the configured provider. - **`queryDefaults.provider_id`** _(required)_: The AI provider identifier for the query model (e.g., `ollama`, `vllm`). Both `model` and `provider_id` must be configured together. -**Session Defaults** _(required when enabled)_: - -- **`sessionDefaults.provider_id`** _(required)_: Vector store provider identifier. Must match a provider configured in your Llama Stack instance (e.g., `notebooks`, `chromadb`). This determines where document embeddings are stored. -- **`sessionDefaults.embedding_model`** _(required)_: The embedding model to use for converting documents to vectors (e.g., `sentence-transformers/all-mpnet-base-v2`). Must be available in Llama Stack. -- **`sessionDefaults.embedding_dimension`** _(required)_: Dimension of the embedding vectors produced by the embedding model. Must match the model's output dimension (commonly `768`, `384`, or `1536`). +> **Important**: The `model` and `provider_id` values must map to a provider and model that are actually enabled in your Lightspeed config.yaml configuration. If the provider or model is not available in Lightspeed, queries will fail. For example, if `openai` enabled in Lightspeed via ENABLE_OPENAI, then model must be available, e.g (model=gpt-4o-mini). **Chunking Strategy** _(optional)_: diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/__fixtures__/lcsHandlers.ts b/workspaces/lightspeed/plugins/lightspeed-backend/__fixtures__/lcsHandlers.ts index 2bec5a88a31..c731fc463b6 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/__fixtures__/lcsHandlers.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/__fixtures__/lcsHandlers.ts @@ -268,6 +268,20 @@ export const lcsHandlers: HttpHandler[] = [ }); }), + // Vector stores list endpoint - returns mock RHDH product docs vector store + http.get(`${LOCAL_LCS_ADDR}/v1/vector-stores`, () => { + return HttpResponse.json({ + data: [ + { + id: 'vs-rhdh-product-docs', + name: 'rhdh-product-docs', + provider_id: 'notebooks', + metadata: {}, + }, + ], + }); + }), + // Catch-all handler for unknown paths http.all(`${LOCAL_LCS_ADDR}/*`, ({ request }) => { console.log(`Caught request to unknown path: ${request.url}`); diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/app-config.yaml b/workspaces/lightspeed/plugins/lightspeed-backend/app-config.yaml index 144f61822de..760aaf519ea 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/app-config.yaml +++ b/workspaces/lightspeed/plugins/lightspeed-backend/app-config.yaml @@ -13,13 +13,6 @@ # provider_id: ollama # AI provider for query model (e.g., ollama, vllm) # model: llama3.1-8b-instruct # Model to use for answering queries # -# # REQUIRED when enabled: Session defaults for vector stores -# # All three fields are required -# sessionDefaults: -# provider_id: notebooks # Vector store provider ID (must match Llama Stack config) -# embedding_model: sentence-transformers/all-mpnet-base-v2 # Embedding model for documents -# embedding_dimension: 768 # Vector dimension (must match embedding model output) -# # # OPTIONAL: Chunking strategy for document processing # chunkingStrategy: # type: auto # 'auto' (default) or 'static' diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/constant.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/constant.ts index b08666f1231..2c74b9d4d45 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/constant.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/constant.ts @@ -22,6 +22,7 @@ export const DEFAULT_CHUNKING_STRATEGY_TYPE = 'auto'; // auto chunking export const DEFAULT_MAX_CHUNK_SIZE_TOKENS = 512; // 512 tokens export const DEFAULT_CHUNK_OVERLAP_TOKENS = 50; // 50 tokens export const DEFAULT_LLAMA_STACK_PORT = 8321; // Llama Stack port +export const DEFAULT_LIGHTSPEED_SERVICE_HOST = '0.0.0.0'; // Lightspeed core service host 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 = @@ -36,16 +37,16 @@ Constraints: Output Format: 1. Summary: A 1-2 sentence high-level answer. 2. Detailed Analysis: A structured breakdown using bullet points. -3. References: A list of sources used. +3. References: A list of sources used. References should be in the format of [Document Title] in a new line for each reference. Disclaimer: Your answers **MUST** be grounded in the provided documents. If the answer isn't present, state: "I don't know based on the provided documents." +Remember, **ALL** references must be from the provided documents and provided documents only. Make no mistakes. `.trim(); /** * HTTP and networking constants */ -export const LIGHTSPEED_SERVICE_HOST = '0.0.0.0'; // Lightspeed core service host export const URL_FETCH_TIMEOUT_MS = 30000; // 30 second timeout for URL fetching export const USER_AGENT = 'RHDH-AI-Notebooks-Bot/1.0'; // User agent for HTTP requests export const MAX_URL_CONTENT_SIZE = 10 * 1024 * 1024; // 10MB max for URL fetched content diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/VectorStoresOperator.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/VectorStoresOperator.ts index a69fdcb8c93..a20a412801d 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/VectorStoresOperator.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/VectorStoresOperator.ts @@ -82,16 +82,45 @@ async function handleHttpError( * * This class provides the same interface as LlamaStackClient but proxies calls through * lightspeed-core REST API instead of calling llama stack directly. + * + * Implemented as a singleton to ensure single instance across the application. */ export class VectorStoresOperator { + private static instance: VectorStoresOperator | null = null; private baseURL: string; private logger: LoggerService; - constructor(lightspeedCoreUrl: string, logger: LoggerService) { + private constructor(lightspeedCoreUrl: string, logger: LoggerService) { this.baseURL = lightspeedCoreUrl; this.logger = logger; } + /** + * Get the singleton instance of VectorStoresOperator + * @param lightspeedCoreUrl - Lightspeed core URL (required on first call) + * @param logger - Logger service (required on first call) + * @returns The singleton instance + */ + static getInstance( + lightspeedCoreUrl: string, + logger: LoggerService, + ): VectorStoresOperator { + if (!VectorStoresOperator.instance) { + VectorStoresOperator.instance = new VectorStoresOperator( + lightspeedCoreUrl, + logger, + ); + } + return VectorStoresOperator.instance; + } + + /** + * Reset the singleton instance (primarily for testing) + */ + static resetInstance(): void { + VectorStoresOperator.instance = null; + } + /** * Vector Stores API - mirrors LlamaStackClient.vectorStores structure */ diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.test.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.test.ts index 740defde6e5..84692639433 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.test.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.test.ts @@ -62,9 +62,10 @@ describe('DocumentService', () => { }, }, }); - operator = new VectorStoresOperator(LIGHTSPEED_CORE_ADDR, logger); + VectorStoresOperator.resetInstance(); // Reset singleton before each test + operator = VectorStoresOperator.getInstance(LIGHTSPEED_CORE_ADDR, logger); documentService = new DocumentService(operator, logger, config); - sessionService = new SessionService(operator, logger, config); + sessionService = new SessionService(operator, logger); // Create a test session for document operations const session = await sessionService.createSession( @@ -84,7 +85,6 @@ describe('DocumentService', () => { const fileId = await documentService.uploadFile( 'Test content', 'test-file.txt', - 'txt', ); expect(fileId).toBeDefined(); @@ -93,23 +93,13 @@ describe('DocumentService', () => { it('should handle upload errors', async () => { // Mock a failure by passing invalid content - await expect( - documentService.uploadFile('', '', 'txt'), - ).resolves.toBeDefined(); + await expect(documentService.uploadFile('', '')).resolves.toBeDefined(); }); it('should use correct MIME type based on file type', async () => { - const fileId1 = await documentService.uploadFile( - '{}', - 'test.json', - 'json', - ); - const fileId2 = await documentService.uploadFile( - 'text', - 'test.txt', - 'txt', - ); - const fileId3 = await documentService.uploadFile('# MD', 'test.md', 'md'); + const fileId1 = await documentService.uploadFile('{}', 'test.json'); + const fileId2 = await documentService.uploadFile('text', 'test.txt'); + const fileId3 = await documentService.uploadFile('# MD', 'test.md'); expect(fileId1).toBeDefined(); expect(fileId2).toBeDefined(); @@ -119,11 +109,7 @@ describe('DocumentService', () => { describe('getFileStatus', () => { it('should get file status for existing document', async () => { - const fileId = await documentService.uploadFile( - 'Content', - 'Test Doc', - 'text', - ); + const fileId = await documentService.uploadFile('Content', 'Test Doc'); await documentService.upsertDocument( sessionId, 'Test Doc', @@ -149,7 +135,6 @@ describe('DocumentService', () => { const fileId = await documentService.uploadFile( 'This is test content', 'Test Document', - 'text', ); const result = await documentService.upsertDocument( @@ -169,7 +154,6 @@ describe('DocumentService', () => { const fileId1 = await documentService.uploadFile( 'Original content', 'Original Title', - 'text', ); await documentService.upsertDocument( sessionId, @@ -181,7 +165,6 @@ describe('DocumentService', () => { const fileId2 = await documentService.uploadFile( 'Updated content', 'Original Title', - 'text', ); const result = await documentService.upsertDocument( sessionId, @@ -199,7 +182,6 @@ describe('DocumentService', () => { const fileId1 = await documentService.uploadFile( 'Content', 'Original Title', - 'text', ); await documentService.upsertDocument( sessionId, @@ -211,7 +193,6 @@ describe('DocumentService', () => { const fileId2 = await documentService.uploadFile( 'Updated content', 'New Title', - 'text', ); const result = await documentService.upsertDocument( sessionId, @@ -230,7 +211,6 @@ describe('DocumentService', () => { const fileId1 = await documentService.uploadFile( 'Content 1', 'Document 1', - 'text', ); await documentService.upsertDocument( sessionId, @@ -242,7 +222,6 @@ describe('DocumentService', () => { const fileId2 = await documentService.uploadFile( 'Content 2', 'Document 2', - 'text', ); await documentService.upsertDocument( sessionId, @@ -265,11 +244,7 @@ describe('DocumentService', () => { }); it('should filter documents by file type', async () => { - const fileId1 = await documentService.uploadFile( - 'Content', - 'Text Doc', - 'text', - ); + const fileId1 = await documentService.uploadFile('Content', 'Text Doc'); await documentService.upsertDocument( sessionId, 'Text Doc', @@ -277,11 +252,7 @@ describe('DocumentService', () => { fileId1, ); - const fileId2 = await documentService.uploadFile( - 'Content', - 'PDF Doc', - 'pdf', - ); + const fileId2 = await documentService.uploadFile('Content', 'PDF Doc'); await documentService.upsertDocument( sessionId, 'PDF Doc', @@ -299,7 +270,6 @@ describe('DocumentService', () => { const fileId = await documentService.uploadFile( 'Content', 'Test Document', - 'text', ); await documentService.upsertDocument( sessionId, @@ -323,7 +293,6 @@ describe('DocumentService', () => { const fileId = await documentService.uploadFile( 'Content', 'Test Document', - 'text', ); await documentService.upsertDocument( sessionId, diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.ts index b98b0a02e5b..37865739614 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/documents/documentService.ts @@ -22,7 +22,6 @@ import { DEFAULT_CHUNK_OVERLAP_TOKENS, DEFAULT_CHUNKING_STRATEGY_TYPE, DEFAULT_MAX_CHUNK_SIZE_TOKENS, - FILE_TYPE_TO_MIME, } from '../../constant'; import { SessionDocument, UpsertResult } from '../types/notebooksTypes'; import { VectorStoresOperator } from '../VectorStoresOperator'; @@ -98,27 +97,13 @@ export class DocumentService { * @returns File ID from the Files API * @throws Error if upload fails */ - /** - * Upload a file to the Files API - * @param content - File content as string - * @param title - File title/name - * @param fileType - Optional file type for MIME type detection - * @returns File ID from the Files API - * @throws Error if upload fails - */ - async uploadFile( - content: string, - title: string, - fileType?: string, - ): Promise { + async uploadFile(content: string, title: string): Promise { try { // Determine MIME type from file type or default to text/plain - const mimeType = fileType - ? FILE_TYPE_TO_MIME[fileType] || 'text/plain' - : 'text/plain'; - + const mimeType = 'text/plain'; + const txtFilename = `${title.replace(/\.[^.]+$/, '')}.txt`; const file = await this.client.files.create({ - file: await toFile(Buffer.from(content, 'utf-8'), title, { + file: await toFile(Buffer.from(content, 'utf-8'), txtFilename, { type: mimeType, }), purpose: 'assistants', diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouter.test.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouter.test.ts index cb43d27b99b..1407111a334 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouter.test.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouter.test.ts @@ -26,6 +26,7 @@ import { resetMockStorage, } from '../../../__fixtures__/lightspeedCoreHandlers'; import { createNotebooksRouter } from './notebooksRouters'; +import { VectorStoresOperator } from './VectorStoresOperator'; const mockUserId = 'user:default/guest'; @@ -53,6 +54,7 @@ describe('Notebooks Router', () => { beforeEach(async () => { resetMockStorage(); + VectorStoresOperator.resetInstance(); // Reset singleton before each test const logger = mockServices.logger.mock(); const config = mockServices.rootConfig({ data: { diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouters.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouters.ts index f60356a3aca..182269659a1 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouters.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/notebooksRouters.ts @@ -29,10 +29,10 @@ import { lightspeedNotebooksUsePermission } from '@red-hat-developer-hub/backsta import { Readable } from 'stream'; import { + DEFAULT_LIGHTSPEED_SERVICE_HOST, DEFAULT_LIGHTSPEED_SERVICE_PORT, HTTP_STATUS_ACCEPTED, HTTP_STATUS_INTERNAL_ERROR, - LIGHTSPEED_SERVICE_HOST, MAX_QUERY_RETRIES, NOTEBOOKS_SYSTEM_PROMPT, upload, @@ -68,7 +68,7 @@ export async function createNotebooksRouter( const lightSpeedPort = config.getOptionalNumber('lightspeed.servicePort') ?? DEFAULT_LIGHTSPEED_SERVICE_PORT; - const lightspeedBaseUrl = `http://${LIGHTSPEED_SERVICE_HOST}:${lightSpeedPort}`; + const lightspeedBaseUrl = `http://${DEFAULT_LIGHTSPEED_SERVICE_HOST}:${lightSpeedPort}`; const queryModel = config.getOptionalString( 'lightspeed.notebooks.queryDefaults.model', ); @@ -85,15 +85,11 @@ export async function createNotebooksRouter( `AI Notebooks connecting to Lightspeed-Core at ${lightspeedBaseUrl}`, ); - const vectorStoresOperator = new VectorStoresOperator( + const vectorStoresOperator = VectorStoresOperator.getInstance( lightspeedBaseUrl, logger, ); - const sessionService = new SessionService( - vectorStoresOperator, - logger, - config, - ); + const sessionService = new SessionService(vectorStoresOperator, logger); const documentService = new DocumentService( vectorStoresOperator, logger, @@ -314,7 +310,6 @@ export async function createNotebooksRouter( const fileId = await documentService.uploadFile( parsedDocument.content, title, - fileType, ); res.status(HTTP_STATUS_ACCEPTED).json({ diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/openapi.yaml b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/openapi.yaml deleted file mode 100644 index 8f6e5fb59fe..00000000000 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/openapi.yaml +++ /dev/null @@ -1,1147 +0,0 @@ -openapi: 3.0.3 -info: - title: AI Notebooks API - description: | - AI Notebooks API for managing sessions (vector stores) and documents for RAG (Retrieval-Augmented Generation) queries. - - ## Features - - **Session Management**: Create, read, update, and delete notebook sessions (vector stores) - - **Document Management**: Upload and manage documents in sessions for RAG queries - - **Query**: Execute RAG queries against session documents - - **File Support**: md, txt, pdf, json, yaml, yml, log, url - - **SSRF Protection**: Built-in security against Server-Side Request Forgery attacks - - ## Authentication - All endpoints (except `/health`) require Backstage authentication and the `lightspeed.notebooks.use` permission. - version: 1.0.0 - contact: - name: Red Hat Developer Hub - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 - -servers: - - url: /api/lightspeed/ai-notebooks - description: AI Notebooks API - -tags: - - name: Health - description: Health check endpoint - - name: Sessions - description: Session (vector store) management operations - - name: Documents - description: Document management within sessions - - name: Query - description: RAG query operations - -paths: - /health: - get: - tags: - - Health - summary: Health check - description: Check if the AI Notebooks service is running - operationId: getHealth - security: [] - responses: - '200': - description: Service is healthy - content: - application/json: - schema: - type: object - required: - - status - properties: - status: - type: string - enum: [ok] - example: - status: ok - - /v1/sessions: - post: - tags: - - Sessions - summary: Create a new session - description: Create a new notebook session (vector store) for organizing documents - operationId: createSession - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - name - properties: - name: - type: string - description: Session name (required, cannot be empty) - example: My Research Project - minLength: 1 - description: - type: string - description: Session description (optional) - example: Documents for machine learning research - metadata: - type: object - description: Additional custom metadata fields (optional) - additionalProperties: true - example: - custom_field: custom-value - status: active - responses: - '200': - description: Session created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SessionResponse' - example: - status: success - session: - session_id: vs-abc123def456 - user_id: user:default/johndoe - name: My Research Project - description: Documents for machine learning research - created_at: '2024-01-15T10:30:00.000Z' - updated_at: '2024-01-15T10:30:00.000Z' - metadata: - custom_field: custom-value - status: active - conversation_id: null - document_ids: [] - message: Session created successfully - '400': - description: Bad request - validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - missingName: - summary: Missing required name field - value: - status: error - error: name is required - '403': - description: Forbidden - user lacks lightspeed.notebooks.use permission - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User lacks permission - '500': - description: Internal server error during session creation - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to create vector store - - get: - tags: - - Sessions - summary: List all sessions - description: Get all notebook sessions for the authenticated user, sorted by created_at (newest first) - operationId: listSessions - responses: - '200': - description: Sessions retrieved successfully (returns empty array if user has no sessions) - content: - application/json: - schema: - $ref: '#/components/schemas/SessionListResponse' - examples: - withSessions: - summary: User has sessions - value: - status: success - sessions: - - session_id: vs-def456 - user_id: user:default/johndoe - name: Recent Project - description: Latest work - created_at: '2024-01-16T14:00:00.000Z' - updated_at: '2024-01-16T14:00:00.000Z' - metadata: - conversation_id: conv-xyz - document_ids: [doc-1, doc-2] - - session_id: vs-abc123 - user_id: user:default/johndoe - name: Old Project - description: Archived work - created_at: '2024-01-15T10:30:00.000Z' - updated_at: '2024-01-15T10:30:00.000Z' - metadata: - conversation_id: null - document_ids: [] - count: 2 - noSessions: - summary: User has no sessions - value: - status: success - sessions: [] - count: 0 - '403': - description: Forbidden - user lacks lightspeed.notebooks.use permission - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User lacks permission - '500': - description: Internal server error during session retrieval - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to list vector stores - - /v1/sessions/{sessionId}: - put: - tags: - - Sessions - summary: Update a session - description: Update session name, description, or metadata (all fields optional) - operationId: updateSession - parameters: - - $ref: '#/components/parameters/SessionId' - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: New session name (optional) - example: Updated Project Name - description: - type: string - description: New session description (optional) - example: Updated description - metadata: - type: object - description: Updated metadata (optional, merged with existing) - additionalProperties: true - example: - custom_field: updated-value - status: active - responses: - '200': - description: Session updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SessionResponse' - example: - status: success - session: - session_id: vs-abc123 - user_id: user:default/johndoe - name: Updated Project Name - description: Updated description - created_at: '2024-01-15T10:30:00.000Z' - updated_at: '2024-01-16T15:20:00.000Z' - metadata: - custom_field: updated-value - status: active - conversation_id: conv-123 - document_ids: [doc-1] - message: Session updated successfully - '403': - description: Forbidden - user does not own this session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Vector store not found - '500': - description: Internal server error during update - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to update vector store - - delete: - tags: - - Sessions - summary: Delete a session - description: Permanently delete a session and all its documents - operationId: deleteSession - parameters: - - $ref: '#/components/parameters/SessionId' - responses: - '200': - description: Session deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SessionResponse' - example: - status: success - session: - session_id: vs-abc123 - user_id: user:default/johndoe - name: Deleted Session - description: This session was deleted - created_at: '2024-01-15T10:30:00.000Z' - updated_at: '2024-01-15T10:30:00.000Z' - metadata: - conversation_id: null - document_ids: [] - message: Session deleted successfully - '403': - description: Forbidden - user does not own this session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Vector store not found - '500': - description: Internal server error during deletion - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to delete vector store - - /v1/sessions/{sessionId}/documents: - get: - tags: - - Documents - summary: List documents in a session - description: Get all documents in the specified session (optionally filtered by file type) - operationId: listDocuments - parameters: - - $ref: '#/components/parameters/SessionId' - - name: fileType - in: query - required: false - description: Filter documents by source type - schema: - type: string - enum: [text, pdf, url, md, json, yaml, log] - responses: - '200': - description: Documents retrieved successfully (returns empty array if no documents match) - content: - application/json: - schema: - $ref: '#/components/schemas/DocumentListResponse' - examples: - withDocuments: - summary: Session has documents - value: - status: success - session_id: vs-abc123 - documents: - - document_id: research-paper - title: Research Paper - session_id: vs-abc123 - user_id: user:default/johndoe - source_type: pdf - created_at: '2024-01-15T11:00:00.000Z' - metadata: - fileName: paper.pdf - fileType: pdf - pageCount: 25 - parseTimestamp: '2024-01-15T11:00:00.000Z' - - document_id: readme - title: README - session_id: vs-abc123 - user_id: user:default/johndoe - source_type: md - created_at: '2024-01-15T11:05:00.000Z' - metadata: - fileName: README.md - fileType: md - parseTimestamp: '2024-01-15T11:05:00.000Z' - count: 2 - noDocuments: - summary: Session has no documents - value: - status: success - session_id: vs-abc123 - documents: [] - count: 0 - '403': - description: Forbidden - user does not own this session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Vector store not found - '500': - description: Internal server error during document retrieval - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to retrieve documents - - put: - tags: - - Documents - summary: Upload a document - description: | - Upload a document to the session. Supports multiple file types: - - **Text files**: md, txt, log - - **Structured data**: json, yaml, yml - - **Binary**: pdf - - **Web content**: url (fetches and parses remote content) - - **Security**: URLs are validated against SSRF attacks (blocks private IPs, localhost, cloud metadata endpoints) - - **File Size Limit**: Maximum preset limit - operationId: uploadDocument - parameters: - - $ref: '#/components/parameters/SessionId' - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - required: - - title - - fileType - properties: - title: - type: string - description: Document title (used to generate document_id, must be unique within session) - example: My Research Paper - fileType: - $ref: '#/components/schemas/FileType' - file: - type: string - format: binary - description: File to upload (required for all types except 'url') - encoding: - file: - contentType: application/octet-stream, text/plain, application/pdf, application/json, text/yaml, text/markdown - responses: - '202': - description: Document upload started (processing asynchronously) - content: - application/json: - schema: - type: object - required: - - status - - file_id - - document_id - - session_id - - message - properties: - status: - type: string - enum: [processing] - file_id: - type: string - description: Llama Stack file ID for tracking - document_id: - type: string - description: Document identifier (sanitized from title) - session_id: - type: string - description: Parent session ID - message: - type: string - example: - status: processing - file_id: file_xyz789abc - document_id: my-research-paper - session_id: vs-abc123 - message: Document upload started - '400': - description: Bad request - validation or parsing error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - missingTitle: - summary: Missing required title - value: - status: error - error: title is required - unsupportedFileType: - summary: Unsupported or missing file type - value: - status: error - error: 'Unsupported file type: exe. Supported types: md, txt, pdf, json, yaml, yml, log, url' - noFile: - summary: No file uploaded (for non-url types) - value: - status: error - error: No file uploaded - fileTooLarge: - summary: File exceeds preset limit - value: - status: error - error: File size exceeds preset limit - invalidJSON: - summary: Invalid JSON file - value: - status: error - error: 'Invalid JSON file: SyntaxError: Unexpected token' - invalidYAML: - summary: Invalid YAML file - value: - status: error - error: 'Invalid YAML file: YAMLException: bad indentation' - invalidURL: - summary: Invalid URL format - value: - status: error - error: 'Invalid URL format: not-a-url' - ssrfBlocked: - summary: SSRF attack blocked - value: - status: error - error: Access to private/internal IP addresses is not allowed - noURLParam: - summary: Missing URL parameter for url type - value: - status: error - error: URL is required when fileType is "url" - '403': - description: Forbidden - user lacks permission or does not own session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Vector store not found - '409': - description: Conflict - document with this title already exists in session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Document with title 'My Research Paper' already exists in this session - '500': - description: Internal server error during upload or processing - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - pdfParsingError: - summary: PDF parsing failed - value: - status: error - error: 'Error parsing PDF: Invalid PDF structure' - urlFetchError: - summary: Failed to fetch URL - value: - status: error - error: 'Error fetching URL: Failed to fetch URL: 500 Internal Server Error' - serverError: - summary: Generic server error - value: - status: error - error: Failed to upload document to vector store - - /v1/sessions/{sessionId}/documents/{documentId}/status: - get: - tags: - - Documents - summary: Get document processing status - description: Check the status of a document upload/processing operation - operationId: getDocumentStatus - parameters: - - $ref: '#/components/parameters/SessionId' - - $ref: '#/components/parameters/DocumentId' - responses: - '200': - description: Document status retrieved successfully - content: - application/json: - schema: - type: object - required: - - status - - document_id - - session_id - properties: - status: - type: string - enum: [processing, completed, failed] - description: Current processing status - document_id: - type: string - session_id: - type: string - error: - type: string - description: Error message (only present if status is 'failed') - examples: - completed: - summary: Document processing completed - value: - status: completed - document_id: my-research-paper - session_id: vs-abc123 - processing: - summary: Document still processing - value: - status: processing - document_id: my-research-paper - session_id: vs-abc123 - failed: - summary: Document processing failed - value: - status: failed - document_id: my-research-paper - session_id: vs-abc123 - error: Failed to extract text from PDF - '400': - description: Bad request - missing document_id parameter - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: document_id query parameter is required - '403': - description: Forbidden - user does not own this session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session or document does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - sessionNotFound: - summary: Session not found - value: - status: error - error: Vector store not found - documentNotFound: - summary: Document not found - value: - status: error - error: File not found - '500': - description: Internal server error during status check - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to retrieve file status - - /v1/sessions/{sessionId}/documents/{documentId}: - delete: - tags: - - Documents - summary: Delete a document - description: Remove a document from the session's vector store - operationId: deleteDocument - parameters: - - $ref: '#/components/parameters/SessionId' - - $ref: '#/components/parameters/DocumentId' - responses: - '200': - description: Document deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DocumentResponse' - example: - status: success - document_id: my-research-paper - session_id: vs-abc123 - message: Document deleted successfully - '403': - description: Forbidden - user does not own this session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session or document does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - sessionNotFound: - summary: Session not found - value: - status: error - error: Vector store not found - documentNotFound: - summary: Document not found - value: - status: error - error: Document not found - '500': - description: Internal server error during deletion - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Failed to delete document from vector store - - /v1/sessions/{sessionId}/query: - post: - tags: - - Query - summary: Execute RAG query - description: | - Execute a RAG (Retrieval-Augmented Generation) query against the session's documents. - Returns a streaming response (Server-Sent Events) with the AI-generated answer. - - **Conversation Management**: - - First query creates a new conversation_id (automatically captured) - - Subsequent queries reuse the conversation_id for context continuity - - The conversation_id is stored in session metadata - - **Streaming Response**: - - Content-Type: text/event-stream - - Each chunk sent as SSE format: `data: {JSON}\n\n` - - Response includes conversation_id and text chunks - operationId: querySession - parameters: - - $ref: '#/components/parameters/SessionId' - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - query - properties: - query: - type: string - description: Question or prompt to ask about the documents - example: What are the main findings in the research papers? - minLength: 1 - responses: - '200': - description: Streaming response with query results - content: - text/event-stream: - schema: - type: string - description: | - Server-Sent Events stream. Each event is formatted as: - ``` - data: {"data":{"conversation_id":"conv-123","chunk":"text..."}} - - ``` - example: | - data: {"data":{"conversation_id":"conv-abc123","chunk":"Based on the research papers,"}} - - data: {"data":{"conversation_id":"conv-abc123","chunk":" the main findings include:"}} - - data: {"data":{"conversation_id":"conv-abc123","chunk":" improved accuracy by 15%"}} - - data: {"data":{"conversation_id":"conv-abc123","chunk":" and reduced latency."}} - '400': - description: Bad request - missing or invalid query - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: query is required - '403': - description: Forbidden - user lacks permission or does not own session - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: User does not have access to this session - '404': - description: Not found - session does not exist - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - status: error - error: Vector store not found - '500': - description: Internal server error during query processing - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - lightspeedError: - summary: Error from lightspeed-core service - value: - status: error - error: 'Error from Llama Stack server: validation error' - genericError: - summary: Generic server error - value: - status: error - error: Failed to process query - -components: - parameters: - SessionId: - name: sessionId - in: path - required: true - description: Session identifier (Llama Stack vector store ID, format 'vs-{alphanumeric}') - schema: - type: string - example: vs-abc123def456 - pattern: '^vs-[a-zA-Z0-9]+$' - - DocumentId: - name: documentId - in: path - required: true - description: Document identifier (sanitized from title, lowercase with hyphens) - schema: - type: string - example: my-research-paper - pattern: '^[a-z0-9-]+$' - - schemas: - FileType: - type: string - enum: - - md - - txt - - pdf - - json - - yaml - - yml - - log - - url - description: | - Supported file types: - - `md`: Markdown files - - `txt`: Plain text files - - `pdf`: PDF documents (text extraction) - - `json`: JSON files (validated before upload) - - `yaml`/`yml`: YAML files (validated before upload) - - `log`: Log files - - `url`: Remote web content (automatically fetched, SSRF-protected) - - NotebookSession: - type: object - required: - - session_id - - user_id - - name - - description - - created_at - - updated_at - properties: - session_id: - type: string - description: Unique session identifier (Llama Stack vector store ID) - example: vs-abc123def456 - user_id: - type: string - description: User entity reference (Backstage user) - example: user:default/johndoe - name: - type: string - description: Session name - example: My Research Project - description: - type: string - description: Session description - example: Documents for machine learning research - created_at: - type: string - format: date-time - description: Session creation timestamp (ISO 8601) - example: '2024-01-15T10:30:00.000Z' - updated_at: - type: string - format: date-time - description: Last update timestamp (ISO 8601) - example: '2024-01-16T14:20:00.000Z' - metadata: - type: object - description: Session metadata (all fields optional) - properties: - document_ids: - type: array - items: - type: string - description: List of document IDs in this session - example: [doc-1, doc-2] - conversation_id: - type: string - nullable: true - description: Active conversation ID for RAG queries (null until first query) - example: conv-abc123 - embedding_model: - type: string - description: Embedding model used - example: text-embedding-ada-002 - embedding_dimension: - type: integer - description: Embedding vector dimension - example: 1536 - provider_id: - type: string - description: AI provider identifier - example: openai - additionalProperties: true - example: - document_ids: [doc-1, doc-2] - conversation_id: conv-abc123 - embedding_model: text-embedding-ada-002 - embedding_dimension: 1536 - provider_id: openai - custom_field: custom-value - - SessionDocument: - type: object - required: - - document_id - - title - - session_id - - user_id - - source_type - - created_at - properties: - document_id: - type: string - description: Document identifier (sanitized from title) - example: my-research-paper - title: - type: string - description: Original document title - example: My Research Paper - session_id: - type: string - description: Parent session ID - example: vs-abc123 - user_id: - type: string - description: Document owner (same as session owner) - example: user:default/johndoe - source_type: - type: string - enum: [text, pdf, url, md, json, yaml, log] - description: Document source/file type - example: pdf - created_at: - type: string - format: date-time - description: Document creation timestamp (ISO 8601) - example: '2024-01-15T10:30:00.000Z' - metadata: - type: object - description: Document metadata from parsing - additionalProperties: true - example: - fileName: research.pdf - fileType: pdf - pageCount: 25 - parseTimestamp: '2024-01-15T10:30:00.000Z' - - SessionResponse: - type: object - required: - - status - properties: - status: - type: string - enum: [success, error] - example: success - session: - $ref: '#/components/schemas/NotebookSession' - message: - type: string - description: Success message - example: Session created successfully - error: - type: string - description: Error message (only present when status is 'error') - - SessionListResponse: - type: object - required: - - status - properties: - status: - type: string - enum: [success, error] - example: success - sessions: - type: array - items: - $ref: '#/components/schemas/NotebookSession' - description: Array of sessions (empty array if user has no sessions) - count: - type: integer - description: Number of sessions returned - example: 5 - error: - type: string - description: Error message (only present when status is 'error') - - DocumentResponse: - type: object - required: - - status - properties: - status: - type: string - enum: [success, error] - example: success - document_id: - type: string - example: my-research-paper - title: - type: string - example: My Research Paper - session_id: - type: string - example: vs-abc123 - replaced: - type: boolean - description: Whether this operation replaced an existing document - example: false - message: - type: string - description: Success message - example: Document deleted successfully - error: - type: string - description: Error message (only present when status is 'error') - - DocumentListResponse: - type: object - required: - - status - properties: - status: - type: string - enum: [success, error] - example: success - session_id: - type: string - example: vs-abc123 - documents: - type: array - items: - $ref: '#/components/schemas/SessionDocument' - description: Array of documents (empty array if session has no documents) - count: - type: integer - description: Number of documents returned - example: 3 - error: - type: string - description: Error message (only present when status is 'error') - - ErrorResponse: - type: object - required: - - status - - error - properties: - status: - type: string - enum: [error] - example: error - error: - type: string - description: Human-readable error message - example: Session not found - - securitySchemes: - BackstageAuth: - type: http - scheme: bearer - description: Backstage authentication token. User must have `lightspeed.notebooks.use` permission. - -security: - - BackstageAuth: [] diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.test.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.test.ts index 0521d9b103c..e103a239e6a 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.test.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.test.ts @@ -47,21 +47,9 @@ describe('SessionService', () => { beforeEach(() => { resetMockStorage(); - const config = mockServices.rootConfig({ - data: { - lightspeed: { - notebooks: { - sessionDefaults: { - provider_id: 'test-notebooks', - embedding_model: 'test-embedding-model', - embedding_dimension: 768, - }, - }, - }, - }, - }); - operator = new VectorStoresOperator(LIGHTSPEED_CORE_ADDR, logger); - service = new SessionService(operator, logger, config); + VectorStoresOperator.resetInstance(); // Reset singleton before each test + operator = VectorStoresOperator.getInstance(LIGHTSPEED_CORE_ADDR, logger); + service = new SessionService(operator, logger); }); afterEach(() => { diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.ts index 7aeb891c2bb..b0a10f8645f 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/notebooks/sessions/sessionService.ts @@ -15,7 +15,6 @@ */ import { LoggerService } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; import { NotAllowedError, NotFoundError } from '@backstage/errors'; import { NotebookSession, SessionMetadata } from '../types/notebooksTypes'; @@ -32,36 +31,11 @@ export class SessionService { private logger: LoggerService; private client: VectorStoresOperator; private providerId: string; - private embeddingModel: string; - private embeddingDimension: number; - constructor( - client: VectorStoresOperator, - logger: LoggerService, - config?: Config, - ) { + constructor(client: VectorStoresOperator, logger: LoggerService) { this.client = client; this.logger = logger; - - const requireConfig = (value: T | undefined, key: string): T => { - if (value === undefined) throw new Error(`${key} is required in config`); - return value; - }; - - this.providerId = requireConfig( - config?.getString('lightspeed.notebooks.sessionDefaults.provider_id'), - 'lightspeed.notebooks.sessionDefaults.provider_id', - ); - this.embeddingModel = requireConfig( - config?.getString('lightspeed.notebooks.sessionDefaults.embedding_model'), - 'lightspeed.notebooks.sessionDefaults.embedding_model', - ); - this.embeddingDimension = requireConfig( - config?.getNumber( - 'lightspeed.notebooks.sessionDefaults.embedding_dimension', - ), - 'lightspeed.notebooks.sessionDefaults.embedding_dimension', - ); + this.providerId = 'notebooks'; } /** @@ -94,16 +68,12 @@ export class SessionService { ...metadata, conversation_id: null, provider_id: this.providerId, - embedding_model: this.embeddingModel, - embedding_dimension: this.embeddingDimension, }, }; const vectorStore = await this.client.vectorStores.create({ name: name || `Session for ${userId}`, provider_id: this.providerId, - embedding_model: this.embeddingModel, - embedding_dimension: this.embeddingDimension, metadata: buildVectorStoreMetadata(tempSession), }); diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.test.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.test.ts index 6df5eff2a3c..c857df309dc 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.test.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.test.ts @@ -29,6 +29,7 @@ import request from 'supertest'; import { handlers, LOCAL_AI_ADDR } from '../../__fixtures__/handlers'; import { lcsHandlers, LOCAL_LCS_ADDR } from '../../__fixtures__/lcsHandlers'; import { lightspeedPlugin } from '../plugin'; +import { VectorStoresOperator } from './notebooks/VectorStoresOperator'; const mockUserId = `user: default/user1`; const mockConversationId = 'conversation-id-1'; @@ -108,6 +109,10 @@ describe('lightspeed router tests', () => { rcs.close(); }); + beforeEach(() => { + VectorStoresOperator.resetInstance(); // Reset singleton before each test + }); + afterEach(() => { jest.clearAllMocks(); server.resetHandlers(); diff --git a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts index bba1caa65c5..7625cbe087d 100644 --- a/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts +++ b/workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts @@ -40,6 +40,7 @@ import { McpValidationResult, } from './mcp-server-types'; import { McpServerValidator } from './mcp-server-validator'; +import { VectorStoresOperator } from './notebooks/VectorStoresOperator'; import { userPermissionAuthorization } from './permission'; import { createTokenEncryptor } from './token-encryption'; import { @@ -107,6 +108,12 @@ export async function createRouter( DEFAULT_LIGHTSPEED_SERVICE_PORT; const system_prompt = config.getOptionalString('lightspeed.systemPrompt'); + const vectorStoresOperator = VectorStoresOperator.getInstance( + `http://0.0.0.0:${port}`, + logger, + ); + let lightspeed_vector_store_id: string = ''; + // Parse admin-configured MCP servers from app-config. // Only name is required; token is optional (users can provide their own via the UI). // URLs come from LCS (GET /v1/mcp-servers), not from app-config. @@ -589,6 +596,20 @@ export async function createRouter( lightspeedChatCreatePermission, credentials, ); + + // get the vector store id for the rhdh-product-docs vector store + if (lightspeed_vector_store_id === '') { + const vectorStores = await vectorStoresOperator.vectorStores.list(); + lightspeed_vector_store_id = + vectorStores.data.find((v: any) => + v.name.startsWith('rhdh-product-docs'), + )?.id || ''; + } + + if (lightspeed_vector_store_id !== '') { + request.body.vector_store_ids = [lightspeed_vector_store_id]; + } + const userQueryParam = `user_id=${encodeURIComponent(user_id)}`; request.body.media_type = 'application/json'; // set media_type to receive start and end event // if system_prompt is defined in lightspeed config