|
@@ -452,6 +452,43 @@ export class QdrantEndpoint extends BaseEndpointComponent {
|
|
|
}
|
|
}
|
|
|
),
|
|
),
|
|
|
|
|
|
|
|
|
|
+ this.createEndpoint(
|
|
|
|
|
+ 'POST',
|
|
|
|
|
+ '/api/shops/:shopId/qdrant/sync',
|
|
|
|
|
+ this.forceQdrantSync.bind(this),
|
|
|
|
|
+ {
|
|
|
|
|
+ summary: 'Force Qdrant sync',
|
|
|
|
|
+ description: 'Force synchronization of all shop content to Qdrant',
|
|
|
|
|
+ tags: ['Qdrant'],
|
|
|
|
|
+ requiresAuth: true,
|
|
|
|
|
+ parameters: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'shopId',
|
|
|
|
|
+ in: 'path',
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ description: 'Shop ID (internal UUID or custom UUID)'
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ responses: {
|
|
|
|
|
+ '202': {
|
|
|
|
|
+ description: 'Sync started',
|
|
|
|
|
+ schema: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ message: { type: 'string' },
|
|
|
|
|
+ items_queued: { type: 'number' },
|
|
|
|
|
+ shop_id: { type: 'string' }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ '404': { description: 'Shop not found' },
|
|
|
|
|
+ '400': { description: 'Qdrant not enabled for shop or no custom_id set' },
|
|
|
|
|
+ '503': { description: 'Embedding workflow not available' }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ),
|
|
|
|
|
+
|
|
|
// Error Management
|
|
// Error Management
|
|
|
this.createEndpoint(
|
|
this.createEndpoint(
|
|
|
'GET',
|
|
'GET',
|
|
@@ -1005,4 +1042,123 @@ export class QdrantEndpoint extends BaseEndpointComponent {
|
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private async forceQdrantSync(req: Request, res: Response): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { shopId } = req.params;
|
|
|
|
|
+
|
|
|
|
|
+ // Get shop from database - try both internal ID and custom ID
|
|
|
|
|
+ let shop = this.db.getShopByAnyId(shopId);
|
|
|
|
|
+ if (!shop) {
|
|
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if Qdrant is enabled
|
|
|
|
|
+ if (!shop.qdrant_enabled) {
|
|
|
|
|
+ res.status(400).json({ error: 'Qdrant is not enabled for this shop' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if shop has custom_id
|
|
|
|
|
+ if (!shop.custom_id) {
|
|
|
|
|
+ res.status(400).json({ error: 'Shop must have a custom_id to sync with Qdrant' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if embedding workflow is available
|
|
|
|
|
+ if (!this.embeddingWorkflow) {
|
|
|
|
|
+ res.status(503).json({ error: 'Embedding workflow not available' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get all latest content for this shop
|
|
|
|
|
+ const latestContent = this.db.getLatestContentByType(shop.id);
|
|
|
|
|
+
|
|
|
|
|
+ // Prepare batch of content items to sync
|
|
|
|
|
+ const embeddingBatch: any[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // Process all content types
|
|
|
|
|
+ for (const content of latestContent.shipping) {
|
|
|
|
|
+ embeddingBatch.push({
|
|
|
|
|
+ contentId: content.id,
|
|
|
|
|
+ contentType: 'shipping' as const,
|
|
|
|
|
+ url: content.url,
|
|
|
|
|
+ content: content.content,
|
|
|
|
|
+ title: content.title,
|
|
|
|
|
+ contentHash: content.content_hash,
|
|
|
|
|
+ contentChanged: true // Force re-sync
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const content of latestContent.contacts) {
|
|
|
|
|
+ embeddingBatch.push({
|
|
|
|
|
+ contentId: content.id,
|
|
|
|
|
+ contentType: 'contacts' as const,
|
|
|
|
|
+ url: content.url,
|
|
|
|
|
+ content: content.content,
|
|
|
|
|
+ title: content.title,
|
|
|
|
|
+ contentHash: content.content_hash,
|
|
|
|
|
+ contentChanged: true
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const content of latestContent.terms) {
|
|
|
|
|
+ embeddingBatch.push({
|
|
|
|
|
+ contentId: content.id,
|
|
|
|
|
+ contentType: 'terms' as const,
|
|
|
|
|
+ url: content.url,
|
|
|
|
|
+ content: content.content,
|
|
|
|
|
+ title: content.title,
|
|
|
|
|
+ contentHash: content.content_hash,
|
|
|
|
|
+ contentChanged: true
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const content of latestContent.faq) {
|
|
|
|
|
+ embeddingBatch.push({
|
|
|
|
|
+ contentId: content.id,
|
|
|
|
|
+ contentType: 'faq' as const,
|
|
|
|
|
+ url: content.url,
|
|
|
|
|
+ content: content.content,
|
|
|
|
|
+ title: content.title,
|
|
|
|
|
+ contentHash: content.content_hash,
|
|
|
|
|
+ contentChanged: true
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (embeddingBatch.length === 0) {
|
|
|
|
|
+ res.status(200).json({
|
|
|
|
|
+ message: 'No content available to sync',
|
|
|
|
|
+ items_queued: 0,
|
|
|
|
|
+ shop_id: shop.id
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Start the sync process in background
|
|
|
|
|
+ logger.info(`Force syncing ${embeddingBatch.length} items to Qdrant for shop ${shop.id}`);
|
|
|
|
|
+
|
|
|
|
|
+ // Process the batch asynchronously (don't await)
|
|
|
|
|
+ this.embeddingWorkflow.processContentBatch(shop.id, embeddingBatch)
|
|
|
|
|
+ .then(results => {
|
|
|
|
|
+ const successCount = results.filter(r => r.embedded).length;
|
|
|
|
|
+ const failCount = results.filter(r => !r.embedded).length;
|
|
|
|
|
+ logger.info(`Qdrant sync completed for shop ${shop.id}: ${successCount} success, ${failCount} failed`);
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ logger.error(`Qdrant sync failed for shop ${shop.id}:`, error);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ res.status(202).json({
|
|
|
|
|
+ message: 'Qdrant sync started',
|
|
|
|
|
+ items_queued: embeddingBatch.length,
|
|
|
|
|
+ shop_id: shop.id
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to force Qdrant sync:', error);
|
|
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|