orchid-ai 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,5 +3,9 @@ export type { ServerOptions, CommandServer, TrainingConfig, AIProvider, Contextu
3
3
  export { setupCommandServer, createCommandConfig, formatErrorForUser, } from './server';
4
4
  export { CommandService, } from './service';
5
5
  export { ContextualCommandService, } from './contextual-service';
6
+ export { MongoDBRAGPlugin, enrichContextWithRAG, } from './mongodb-rag';
7
+ export type { MongoDBConnection, RAGConfig, RAGContext, LookupResult, FindOptions, VectorSearchOptions, } from './mongodb-rag';
8
+ export { IntentDetectionPlugin, enrichContextWithIntent, } from './intent-detection';
9
+ export type { IntentDetectionConfig, DetectedIntent, DataNeed, FieldOperation, } from './intent-detection';
6
10
  export { getTrainingDataConfig, CommandTrainingCollector, } from './training';
7
11
  export { createTrainingConfig, validateTrainingConfig, getExpectedFileStructure, } from './training-schema';
@@ -13902,6 +13902,536 @@ ${additionalContext}
13902
13902
  }
13903
13903
  }
13904
13904
 
13905
+ /**
13906
+ * MongoDB RAG (Retrieval-Augmented Generation) Plugin
13907
+ *
13908
+ * This plugin analyzes user queries and automatically retrieves relevant data
13909
+ * from MongoDB collections to enrich the AI context before generating responses.
13910
+ *
13911
+ * Features:
13912
+ * - Automatic query intent detection
13913
+ * - Dynamic collection lookups based on schema metadata
13914
+ * - Configurable search strategies (vector, text, filter)
13915
+ * - Database-agnostic interface (bring your own MongoDB connection)
13916
+ */
13917
+ /**
13918
+ * MongoDB RAG Plugin for ContextualCommandService
13919
+ */
13920
+ class MongoDBRAGPlugin {
13921
+ config;
13922
+ constructor(config) {
13923
+ this.config = {
13924
+ maxDocuments: 10,
13925
+ useVectorSearch: false,
13926
+ verboseLogging: false,
13927
+ ...config,
13928
+ };
13929
+ }
13930
+ log(message, ...args) {
13931
+ if (this.config.verboseLogging) {
13932
+ console.log(`[MONGODB RAG]`, message, ...args);
13933
+ }
13934
+ }
13935
+ /**
13936
+ * Analyze user query and schema to determine what data needs to be retrieved
13937
+ */
13938
+ async analyzeAndRetrieve(userQuery, schema, currentFormData) {
13939
+ this.log('🔍 Analyzing query for data retrieval needs...');
13940
+ const lookups = [];
13941
+ let queriesExecuted = 0;
13942
+ // Extract collections from schema
13943
+ const collectionsToQuery = this.extractCollectionsFromSchema(schema);
13944
+ if (collectionsToQuery.length === 0) {
13945
+ this.log('⚠️ No collections defined in schema');
13946
+ return {
13947
+ lookups: [],
13948
+ enrichedContext: '',
13949
+ queriesExecuted: 0,
13950
+ };
13951
+ }
13952
+ this.log(`📚 Found ${collectionsToQuery.length} collections in schema:`, collectionsToQuery.map(c => c.name));
13953
+ // Determine query intent and build MongoDB queries
13954
+ const queryIntents = this.determineQueryIntent(userQuery, collectionsToQuery, currentFormData);
13955
+ // Execute queries
13956
+ for (const intent of queryIntents) {
13957
+ try {
13958
+ this.log(`🔎 Querying collection: ${intent.collection}`, intent.query);
13959
+ const documents = await this.config.connection.find(intent.collection, intent.query, {
13960
+ limit: this.config.maxDocuments,
13961
+ sort: intent.sort,
13962
+ projection: intent.projection,
13963
+ });
13964
+ lookups.push({
13965
+ collection: intent.collection,
13966
+ documents,
13967
+ query: intent.query,
13968
+ count: documents.length,
13969
+ });
13970
+ queriesExecuted++;
13971
+ this.log(`✅ Retrieved ${documents.length} documents from ${intent.collection}`);
13972
+ }
13973
+ catch (error) {
13974
+ this.log(`❌ Error querying ${intent.collection}:`, error);
13975
+ }
13976
+ }
13977
+ // Build enriched context
13978
+ const enrichedContext = this.buildEnrichedContext(lookups, schema);
13979
+ return {
13980
+ lookups,
13981
+ enrichedContext,
13982
+ queriesExecuted,
13983
+ };
13984
+ }
13985
+ /**
13986
+ * Extract collections referenced in schema
13987
+ */
13988
+ extractCollectionsFromSchema(schema) {
13989
+ const collections = new Map();
13990
+ const processProperty = (fieldName, property) => {
13991
+ if (property.collection) {
13992
+ if (!collections.has(property.collection)) {
13993
+ collections.set(property.collection, {
13994
+ name: property.collection,
13995
+ fields: [],
13996
+ searchable: property.searchable || false,
13997
+ vectorField: property.vectorField,
13998
+ });
13999
+ }
14000
+ collections.get(property.collection).fields.push(fieldName);
14001
+ }
14002
+ // Handle nested objects
14003
+ if (property.type === 'object' && property.properties) {
14004
+ for (const [nestedName, nestedProp] of Object.entries(property.properties)) {
14005
+ processProperty(`${fieldName}.${nestedName}`, nestedProp);
14006
+ }
14007
+ }
14008
+ // Handle arrays of objects
14009
+ if (property.type === 'array' && property.items && typeof property.items === 'object') {
14010
+ if (property.items.properties) {
14011
+ for (const [itemName, itemProp] of Object.entries(property.items.properties)) {
14012
+ processProperty(`${fieldName}.${itemName}`, itemProp);
14013
+ }
14014
+ }
14015
+ }
14016
+ };
14017
+ // Process all schema properties
14018
+ if (schema.properties) {
14019
+ for (const [fieldName, property] of Object.entries(schema.properties)) {
14020
+ processProperty(fieldName, property);
14021
+ }
14022
+ }
14023
+ return Array.from(collections.values());
14024
+ }
14025
+ /**
14026
+ * Determine query intent from user input
14027
+ */
14028
+ determineQueryIntent(userQuery, collections, currentFormData) {
14029
+ const intents = [];
14030
+ const queryLower = userQuery.toLowerCase();
14031
+ for (const collection of collections) {
14032
+ // Build query based on current form data and user query
14033
+ const query = {};
14034
+ // Check if collection name is mentioned in query
14035
+ const collectionMentioned = queryLower.includes(collection.name.toLowerCase());
14036
+ // Check for specific keywords that indicate data lookup
14037
+ const needsLookup = queryLower.includes('show') ||
14038
+ queryLower.includes('list') ||
14039
+ queryLower.includes('find') ||
14040
+ queryLower.includes('get') ||
14041
+ queryLower.includes('search') ||
14042
+ queryLower.includes('available') ||
14043
+ queryLower.includes('options') ||
14044
+ collectionMentioned;
14045
+ if (!needsLookup && !collectionMentioned) {
14046
+ // Skip collections that aren't relevant to this query
14047
+ continue;
14048
+ }
14049
+ // Add filters based on current form data
14050
+ if (currentFormData) {
14051
+ for (const field of collection.fields) {
14052
+ if (currentFormData[field] && typeof currentFormData[field] === 'string') {
14053
+ // Add partial text match for string fields
14054
+ query[field] = { $regex: currentFormData[field], $options: 'i' };
14055
+ }
14056
+ }
14057
+ }
14058
+ // Extract potential search terms from query
14059
+ const searchTerms = this.extractSearchTerms(queryLower);
14060
+ if (searchTerms.length > 0 && collection.searchable) {
14061
+ // Build text search query
14062
+ query.$text = { $search: searchTerms.join(' ') };
14063
+ }
14064
+ // Add intent
14065
+ intents.push({
14066
+ collection: collection.name,
14067
+ query: Object.keys(query).length > 0 ? query : {}, // Empty query = get all
14068
+ sort: { _id: -1 }, // Most recent first
14069
+ projection: undefined, // Get all fields
14070
+ });
14071
+ }
14072
+ return intents;
14073
+ }
14074
+ /**
14075
+ * Extract search terms from user query
14076
+ */
14077
+ extractSearchTerms(query) {
14078
+ // Remove common words
14079
+ const stopWords = ['the', 'a', 'an', 'in', 'on', 'at', 'for', 'to', 'of', 'with', 'show', 'me', 'list', 'find', 'get', 'search', 'available', 'options'];
14080
+ const words = query
14081
+ .toLowerCase()
14082
+ .replace(/[^\w\s]/g, ' ')
14083
+ .split(/\s+/)
14084
+ .filter(word => word.length > 2 && !stopWords.includes(word));
14085
+ return words;
14086
+ }
14087
+ /**
14088
+ * Build enriched context from retrieved documents
14089
+ */
14090
+ buildEnrichedContext(lookups, schema) {
14091
+ if (lookups.length === 0) {
14092
+ return '';
14093
+ }
14094
+ let context = '\n## 📊 RETRIEVED DATA FROM DATABASE\n\n';
14095
+ context += 'The following data was retrieved from your database to help answer the query:\n\n';
14096
+ for (const lookup of lookups) {
14097
+ if (lookup.documents.length === 0)
14098
+ continue;
14099
+ context += `### Collection: ${lookup.collection}\n`;
14100
+ context += `Found ${lookup.documents.length} matching documents:\n\n`;
14101
+ // Format documents for AI consumption
14102
+ lookup.documents.forEach((doc, index) => {
14103
+ context += `**Document ${index + 1}:**\n`;
14104
+ context += '```json\n';
14105
+ context += JSON.stringify(doc, null, 2);
14106
+ context += '\n```\n\n';
14107
+ });
14108
+ }
14109
+ context += '**IMPORTANT:** Use this retrieved data to provide accurate, data-driven suggestions. ';
14110
+ context += 'Reference actual values from these documents rather than making up data.\n';
14111
+ return context;
14112
+ }
14113
+ /**
14114
+ * Generate embeddings for vector search (requires OpenAI API)
14115
+ */
14116
+ async generateEmbedding(text) {
14117
+ if (!this.config.embeddingApiKey) {
14118
+ throw new Error('Embedding API key not configured');
14119
+ }
14120
+ // This would use OpenAI embeddings API
14121
+ // Implementation left as exercise - requires OpenAI SDK
14122
+ throw new Error('Vector search not yet implemented');
14123
+ }
14124
+ }
14125
+ /**
14126
+ * Helper function to create RAG-enhanced additional context
14127
+ */
14128
+ async function enrichContextWithRAG(ragPlugin, userQuery, schema, currentFormData, existingContext) {
14129
+ const ragContext = await ragPlugin.analyzeAndRetrieve(userQuery, schema, currentFormData);
14130
+ let enrichedContext = existingContext || '';
14131
+ if (ragContext.enrichedContext) {
14132
+ enrichedContext = ragContext.enrichedContext + '\n\n' + enrichedContext;
14133
+ }
14134
+ return enrichedContext;
14135
+ }
14136
+
14137
+ /**
14138
+ * Intent Detection Plugin for ContextualCommandService
14139
+ *
14140
+ * Uses the AI model to analyze user queries and detect intent, entities,
14141
+ * and data retrieval needs before the main AI processing. This enables:
14142
+ * - Faster responses with pre-fetched relevant data
14143
+ * - More accurate AI suggestions based on real database values
14144
+ * - Context-aware data retrieval
14145
+ * - Automatic understanding of your domain without manual configuration
14146
+ *
14147
+ * The AI analyzes the query and schema to determine what data to retrieve.
14148
+ */
14149
+ /**
14150
+ * Intent Detection Plugin (AI-Powered)
14151
+ */
14152
+ class IntentDetectionPlugin {
14153
+ config;
14154
+ aiModel;
14155
+ constructor(config) {
14156
+ this.config = {
14157
+ verboseLogging: false,
14158
+ temperature: 0.3, // Low temperature for consistent intent detection
14159
+ ...config,
14160
+ };
14161
+ // Create AI model instance
14162
+ this.aiModel = this.createModelInstance();
14163
+ }
14164
+ createModelInstance() {
14165
+ const { provider, apiKey, model } = this.config;
14166
+ switch (provider.toLowerCase()) {
14167
+ case 'openai':
14168
+ return {
14169
+ client: new OpenAI({ apiKey }),
14170
+ model: model || 'gpt-3.5-turbo',
14171
+ provider: 'openai',
14172
+ };
14173
+ case 'claude':
14174
+ return {
14175
+ client: new Anthropic({ apiKey }),
14176
+ model: model || 'claude-3-haiku-20240307',
14177
+ provider: 'claude',
14178
+ };
14179
+ case 'gemini':
14180
+ return {
14181
+ client: new GoogleGenerativeAI(apiKey),
14182
+ model: model || 'gemini-1.5-flash',
14183
+ provider: 'gemini',
14184
+ };
14185
+ default:
14186
+ throw new Error(`Unsupported provider: ${provider}`);
14187
+ }
14188
+ }
14189
+ log(message, ...args) {
14190
+ if (this.config.verboseLogging) {
14191
+ console.log(`[INTENT DETECTION]`, message, ...args);
14192
+ }
14193
+ }
14194
+ /**
14195
+ * Detect user intent from a query using AI
14196
+ */
14197
+ async detectIntent(query, currentFormData = {}, schema) {
14198
+ this.log('🔍 AI Analyzing query:', query);
14199
+ // Build prompt for AI
14200
+ const systemPrompt = this.buildIntentAnalysisPrompt(schema);
14201
+ const userPrompt = this.buildUserPrompt(query, currentFormData, schema);
14202
+ try {
14203
+ // Call AI to analyze intent
14204
+ const response = await this.callAI(systemPrompt, userPrompt);
14205
+ // Parse AI response
14206
+ const intent = this.parseIntentResponse(response, query);
14207
+ this.log(' Action:', intent.actionType);
14208
+ this.log(' Entities:', intent.entities);
14209
+ this.log(' Data needs:', intent.dataNeeds.map(d => d.collection));
14210
+ this.log(' Confidence:', `${(intent.confidence * 100).toFixed(0)}%`);
14211
+ return intent;
14212
+ }
14213
+ catch (error) {
14214
+ this.log('❌ Error detecting intent:', error);
14215
+ // Fallback to empty intent
14216
+ return {
14217
+ actionType: 'unknown',
14218
+ entities: [],
14219
+ dataNeeds: [],
14220
+ fieldOperations: [],
14221
+ confidence: 0.3,
14222
+ query,
14223
+ };
14224
+ }
14225
+ }
14226
+ /**
14227
+ * Build system prompt for intent analysis
14228
+ */
14229
+ buildIntentAnalysisPrompt(schema) {
14230
+ let prompt = `You are an intent analysis assistant. Analyze user queries to determine:
14231
+ 1. What action they want to perform
14232
+ 2. What entities/data they're referencing
14233
+ 3. What database collections should be queried for relevant data
14234
+
14235
+ Common action types: create, update, search, view, delete, calculate, analyze, help
14236
+
14237
+ Return your analysis as JSON with this exact structure:
14238
+ {
14239
+ "actionType": "create|update|search|view|delete|calculate|analyze|help",
14240
+ "entities": ["entity1", "entity2"],
14241
+ "dataNeeds": [
14242
+ {
14243
+ "collection": "collection_name",
14244
+ "reason": "why this data is needed",
14245
+ "query": {},
14246
+ "limit": 20
14247
+ }
14248
+ ],
14249
+ "fieldOperations": [
14250
+ {
14251
+ "operation": "set|add|remove",
14252
+ "field": "fieldName",
14253
+ "value": "optional value"
14254
+ }
14255
+ ],
14256
+ "confidence": 0.8
14257
+ }`;
14258
+ if (schema) {
14259
+ prompt += `\n\nAvailable schema (use 'collection' properties to determine what data to retrieve):
14260
+ \`\`\`json
14261
+ ${JSON.stringify(schema, null, 2)}
14262
+ \`\`\`
14263
+
14264
+ CRITICAL: For any field with a "collection" property, add it to dataNeeds if the user query references that field.`;
14265
+ }
14266
+ return prompt;
14267
+ }
14268
+ /**
14269
+ * Build user prompt with query and context
14270
+ */
14271
+ buildUserPrompt(query, currentFormData, schema) {
14272
+ let prompt = `User Query: "${query}"`;
14273
+ if (Object.keys(currentFormData).length > 0) {
14274
+ prompt += `\n\nCurrent Form Data:
14275
+ \`\`\`json
14276
+ ${JSON.stringify(currentFormData, null, 2)}
14277
+ \`\`\``;
14278
+ }
14279
+ prompt += `\n\nAnalyze this query and return the JSON intent analysis.`;
14280
+ return prompt;
14281
+ }
14282
+ /**
14283
+ * Call AI model to analyze intent
14284
+ */
14285
+ async callAI(systemPrompt, userPrompt) {
14286
+ const { client, model, provider } = this.aiModel;
14287
+ switch (provider) {
14288
+ case 'openai': {
14289
+ const response = await client.chat.completions.create({
14290
+ model,
14291
+ messages: [
14292
+ { role: 'system', content: systemPrompt },
14293
+ { role: 'user', content: userPrompt },
14294
+ ],
14295
+ temperature: this.config.temperature,
14296
+ max_tokens: 1000,
14297
+ });
14298
+ return response.choices[0]?.message?.content || '{}';
14299
+ }
14300
+ case 'claude': {
14301
+ const response = await client.messages.create({
14302
+ model,
14303
+ system: systemPrompt,
14304
+ messages: [{ role: 'user', content: userPrompt }],
14305
+ temperature: this.config.temperature,
14306
+ max_tokens: 1000,
14307
+ });
14308
+ const firstBlock = response.content[0];
14309
+ return firstBlock && firstBlock.type === 'text' ? firstBlock.text : '{}';
14310
+ }
14311
+ case 'gemini': {
14312
+ const geminiModel = client.getGenerativeModel({
14313
+ model,
14314
+ systemInstruction: systemPrompt,
14315
+ generationConfig: {
14316
+ temperature: this.config.temperature,
14317
+ maxOutputTokens: 1000,
14318
+ },
14319
+ });
14320
+ const result = await geminiModel.generateContent(userPrompt);
14321
+ return result.response?.text() || '{}';
14322
+ }
14323
+ default:
14324
+ throw new Error(`Unsupported provider: ${provider}`);
14325
+ }
14326
+ }
14327
+ /**
14328
+ * Parse AI response into DetectedIntent
14329
+ */
14330
+ parseIntentResponse(response, query) {
14331
+ try {
14332
+ // Extract JSON from response
14333
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
14334
+ if (!jsonMatch) {
14335
+ throw new Error('No JSON found in response');
14336
+ }
14337
+ const parsed = JSON.parse(jsonMatch[0]);
14338
+ return {
14339
+ actionType: parsed.actionType || 'unknown',
14340
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
14341
+ dataNeeds: Array.isArray(parsed.dataNeeds) ? parsed.dataNeeds : [],
14342
+ fieldOperations: Array.isArray(parsed.fieldOperations) ? parsed.fieldOperations : [],
14343
+ confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5,
14344
+ query,
14345
+ };
14346
+ }
14347
+ catch (error) {
14348
+ this.log('Failed to parse AI response:', response);
14349
+ return {
14350
+ actionType: 'unknown',
14351
+ entities: [],
14352
+ dataNeeds: [],
14353
+ fieldOperations: [],
14354
+ confidence: 0.3,
14355
+ query,
14356
+ };
14357
+ }
14358
+ }
14359
+ /**
14360
+ * Build enhanced context for AI from intent and retrieved data
14361
+ */
14362
+ buildContextFromIntent(intent, retrievedData) {
14363
+ let context = '';
14364
+ // Add user intent section
14365
+ context += `\n## 🎯 USER INTENT\n`;
14366
+ context += `The user wants to: **${intent.actionType}**\n`;
14367
+ if (intent.entities.length > 0) {
14368
+ context += `Entities mentioned: ${intent.entities.join(', ')}\n`;
14369
+ }
14370
+ context += `Confidence: ${(intent.confidence * 100).toFixed(0)}%\n`;
14371
+ // Add field operations if any
14372
+ if (intent.fieldOperations.length > 0) {
14373
+ context += `\n### Specific Field Operations:\n`;
14374
+ intent.fieldOperations.forEach(op => {
14375
+ context += `- ${op.operation} ${op.field}`;
14376
+ if (op.value)
14377
+ context += ` to "${op.value}"`;
14378
+ context += '\n';
14379
+ });
14380
+ }
14381
+ // Add retrieved data
14382
+ if (retrievedData && Object.keys(retrievedData).length > 0) {
14383
+ context += `\n## 📊 AVAILABLE DATA\n`;
14384
+ for (const [collection, data] of Object.entries(retrievedData)) {
14385
+ if (!data || data.length === 0)
14386
+ continue;
14387
+ context += `\n### ${collection.toUpperCase()} (${data.length} items):\n`;
14388
+ // Use custom formatter if available
14389
+ if (this.config.contextFormatters && this.config.contextFormatters[collection]) {
14390
+ context += this.config.contextFormatters[collection](data);
14391
+ }
14392
+ else {
14393
+ // Default formatting: JSON
14394
+ context += '```json\n';
14395
+ context += JSON.stringify(data.slice(0, 5), null, 2); // Limit to first 5 items
14396
+ context += '\n```\n';
14397
+ }
14398
+ }
14399
+ context += `\n**💡 TIP:** Use the actual data above in your suggestions instead of making up values.\n`;
14400
+ }
14401
+ return context;
14402
+ }
14403
+ }
14404
+ /**
14405
+ * Helper function to create intent-enhanced context (AI-Powered)
14406
+ */
14407
+ async function enrichContextWithIntent(intentPlugin, dataRetriever, userQuery, schema, currentFormData, existingContext) {
14408
+ // AI analyzes the query to detect intent
14409
+ const intent = await intentPlugin.detectIntent(userQuery, currentFormData, schema);
14410
+ // Retrieve data based on AI-detected needs
14411
+ const retrievedData = {};
14412
+ for (const need of intent.dataNeeds) {
14413
+ try {
14414
+ const data = await dataRetriever(need);
14415
+ retrievedData[need.collection] = data;
14416
+ }
14417
+ catch (error) {
14418
+ console.error(`Failed to retrieve data for ${need.collection}:`, error);
14419
+ retrievedData[need.collection] = [];
14420
+ }
14421
+ }
14422
+ // Build context from intent
14423
+ const intentContext = intentPlugin.buildContextFromIntent(intent, retrievedData);
14424
+ // Merge with existing context
14425
+ let enrichedContext = existingContext || '';
14426
+ if (intentContext) {
14427
+ enrichedContext = intentContext + '\n\n' + enrichedContext;
14428
+ }
14429
+ return {
14430
+ context: enrichedContext,
14431
+ intent,
14432
+ };
14433
+ }
14434
+
13905
14435
  /**
13906
14436
  * Validate training configuration
13907
14437
  */
@@ -14004,5 +14534,5 @@ function getExpectedFileStructure() {
14004
14534
  };
14005
14535
  }
14006
14536
 
14007
- export { CommandService, CommandTrainingCollector, ContextualCommandService, createCommandConfig, createTrainingConfig, formatErrorForUser, getExpectedFileStructure, getTrainingDataConfig, setupCommandServer, validateTrainingConfig };
14537
+ export { CommandService, CommandTrainingCollector, ContextualCommandService, IntentDetectionPlugin, MongoDBRAGPlugin, createCommandConfig, createTrainingConfig, enrichContextWithIntent, enrichContextWithRAG, formatErrorForUser, getExpectedFileStructure, getTrainingDataConfig, setupCommandServer, validateTrainingConfig };
14008
14538
  //# sourceMappingURL=index.esm.js.map