--- name: "Directus AI Assistant Integration" description: "Build AI-powered features in Directus: chat interfaces, content generation, smart suggestions, and copilot functionality" version: 1.0.0 author: "Directus Development System" tags: ["directus", "ai", "openai", "anthropic", "chat", "assistant", "websocket", "real-time", "rag"] --- # Directus AI Assistant Integration ## Overview This skill provides comprehensive guidance for integrating AI assistants into Directus applications. Build intelligent chat interfaces, content generation systems, context-aware suggestions, and copilot features using OpenAI, Anthropic Claude, and other AI providers. Implement real-time communication, vector search, RAG (Retrieval Augmented Generation), and natural language interfaces. ## When to Use This Skill - Building AI chat interfaces in Directus panels - Implementing content generation workflows - Creating smart autocomplete and suggestions - Adding natural language query interfaces - Building AI-powered content moderation - Implementing semantic search with embeddings - Creating AI copilot features for users - Setting up RAG systems with vector databases - Building conversational interfaces - Implementing AI-driven automation ## Architecture Overview ### AI Integration Stack ``` ┌─────────────────────────────────────┐ │ Directus Frontend │ │ (Vue 3 Chat Components) │ └────────────┬────────────────────────┘ │ WebSocket / REST ┌────────────▼────────────────────────┐ │ Directus Backend │ │ (AI Service Layer) │ ├─────────────────────────────────────┤ │ • Request Queue │ │ • Context Management │ │ • Token Optimization │ │ • Response Streaming │ └────────────┬────────────────────────┘ │ ┌────────────▼────────────────────────┐ │ AI Providers │ ├─────────────────────────────────────┤ │ • OpenAI (GPT-4, Embeddings) │ │ • Anthropic (Claude) │ │ • Cohere (Reranking) │ │ • Hugging Face (Open Models) │ └─────────────────────────────────────┘ │ ┌────────────▼────────────────────────┐ │ Vector Database │ │ (Pinecone/Weaviate/pgvector) │ └─────────────────────────────────────┘ ``` ## Process: Building AI Chat Interface ### Step 1: Create Chat Panel Extension ```vue ``` ## Process: Implementing AI Service Layer ### Step 1: Create AI Service ```typescript // src/services/ai.service.ts import { BaseService } from '@directus/api/services'; import OpenAI from 'openai'; import Anthropic from '@anthropic-ai/sdk'; import { Pinecone } from '@pinecone-database/pinecone'; import { encoding_for_model } from 'tiktoken'; interface AIConfig { provider: 'openai' | 'anthropic' | 'custom'; model: string; apiKey: string; maxTokens?: number; temperature?: number; systemPrompt?: string; } interface EmbeddingOptions { text: string; model?: string; dimensions?: number; } export class AIService extends BaseService { private openai: OpenAI | null = null; private anthropic: Anthropic | null = null; private pinecone: Pinecone | null = null; private tokenEncoder: any; constructor(options: any) { super(options); this.initializeProviders(); } private initializeProviders() { // Initialize OpenAI if (process.env.OPENAI_API_KEY) { this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); this.tokenEncoder = encoding_for_model('gpt-4'); } // Initialize Anthropic if (process.env.ANTHROPIC_API_KEY) { this.anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }); } // Initialize Pinecone for vector search if (process.env.PINECONE_API_KEY) { this.pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY, environment: process.env.PINECONE_ENVIRONMENT || 'us-east-1', }); } } // Chat Completion async chat(options: { messages: any[]; model?: string; temperature?: number; maxTokens?: number; stream?: boolean; systemPrompt?: string; }): Promise { const model = options.model || 'gpt-4-turbo-preview'; const temperature = options.temperature ?? 0.7; const maxTokens = options.maxTokens || 2000; // Add system prompt if provided const messages = options.systemPrompt ? [{ role: 'system', content: options.systemPrompt }, ...options.messages] : options.messages; // Token counting and optimization const totalTokens = this.countTokens(messages); if (totalTokens > 8000) { // Truncate or summarize older messages messages.splice(1, messages.length - 10); } if (model.startsWith('claude')) { return this.anthropicChat(messages, model, temperature, maxTokens, options.stream); } else { return this.openaiChat(messages, model, temperature, maxTokens, options.stream); } } private async openaiChat( messages: any[], model: string, temperature: number, maxTokens: number, stream?: boolean ): Promise { if (!this.openai) throw new Error('OpenAI not configured'); if (stream) { const stream = await this.openai.chat.completions.create({ model, messages, temperature, max_tokens: maxTokens, stream: true, }); return stream; } else { const completion = await this.openai.chat.completions.create({ model, messages, temperature, max_tokens: maxTokens, response_format: { type: 'json_object' }, // For structured output }); return { content: completion.choices[0].message.content, usage: completion.usage, model: completion.model, }; } } private async anthropicChat( messages: any[], model: string, temperature: number, maxTokens: number, stream?: boolean ): Promise { if (!this.anthropic) throw new Error('Anthropic not configured'); // Convert messages to Claude format const systemPrompt = messages.find(m => m.role === 'system')?.content || ''; const conversationMessages = messages.filter(m => m.role !== 'system'); const response = await this.anthropic.messages.create({ model: model.replace('claude-', ''), max_tokens: maxTokens, temperature, system: systemPrompt, messages: conversationMessages, stream, }); if (stream) { return response; } else { return { content: response.content[0].text, usage: { prompt_tokens: response.usage.input_tokens, completion_tokens: response.usage.output_tokens, }, model: response.model, }; } } // Content Generation async generateContent(options: { type: 'article' | 'description' | 'summary' | 'translation'; input: string; targetLanguage?: string; tone?: 'formal' | 'casual' | 'technical' | 'creative'; length?: 'short' | 'medium' | 'long'; }): Promise { const prompts = { article: `Write a comprehensive article about: ${options.input} Tone: ${options.tone || 'formal'} Length: ${options.length || 'medium'} Include: Introduction, main points, and conclusion`, description: `Write a compelling product/service description for: ${options.input} Tone: ${options.tone || 'casual'} Focus on benefits and unique features`, summary: `Summarize the following content concisely: ${options.input} Length: ${options.length || 'short'} Keep key points and important information`, translation: `Translate the following to ${options.targetLanguage}: ${options.input} Maintain tone and context accurately`, }; const response = await this.chat({ messages: [{ role: 'user', content: prompts[options.type] }], temperature: options.type === 'translation' ? 0.3 : 0.7, }); return response.content; } // Embeddings and Vector Search async createEmbedding(options: EmbeddingOptions): Promise { if (!this.openai) throw new Error('OpenAI not configured'); const response = await this.openai.embeddings.create({ model: options.model || 'text-embedding-3-small', input: options.text, dimensions: options.dimensions || 1536, }); return response.data[0].embedding; } async vectorSearch(options: { query: string; collection: string; topK?: number; filter?: any; }): Promise { if (!this.pinecone) throw new Error('Pinecone not configured'); // Get query embedding const queryEmbedding = await this.createEmbedding({ text: options.query }); // Search in Pinecone const index = this.pinecone.index(options.collection); const results = await index.query({ vector: queryEmbedding, topK: options.topK || 10, filter: options.filter, includeMetadata: true, includeValues: false, }); return results.matches || []; } // RAG (Retrieval Augmented Generation) async ragQuery(options: { query: string; collection: string; systemContext?: string; topK?: number; }): Promise { // 1. Retrieve relevant documents const relevantDocs = await this.vectorSearch({ query: options.query, collection: options.collection, topK: options.topK || 5, }); // 2. Build context from retrieved documents const context = relevantDocs .map(doc => doc.metadata?.content || '') .join('\n\n---\n\n'); // 3. Generate response with context const systemPrompt = `${options.systemContext || 'You are a helpful assistant.'} Use the following context to answer questions. If the answer cannot be found in the context, say so. Context: ${context}`; const response = await this.chat({ messages: [{ role: 'user', content: options.query }], systemPrompt, temperature: 0.3, // Lower temperature for factual responses }); return { answer: response.content, sources: relevantDocs.map(doc => ({ id: doc.id, score: doc.score, metadata: doc.metadata, })), usage: response.usage, }; } // Content Moderation async moderateContent(content: string): Promise<{ safe: boolean; categories: any; flaggedTerms: string[]; }> { if (!this.openai) throw new Error('OpenAI not configured'); const moderation = await this.openai.moderations.create({ input: content, }); const result = moderation.results[0]; const flaggedCategories = Object.entries(result.categories) .filter(([_, flagged]) => flagged) .map(([category]) => category); return { safe: !result.flagged, categories: result.category_scores, flaggedTerms: flaggedCategories, }; } // Smart Suggestions async generateSuggestions(options: { context: string; type: 'autocomplete' | 'next_actions' | 'related_content'; count?: number; }): Promise { const prompts = { autocomplete: `Based on this context, suggest ${options.count || 5} possible completions: Context: ${options.context} Return as JSON array of strings`, next_actions: `Based on this context, suggest ${options.count || 5} logical next actions: Context: ${options.context} Return as JSON array of action strings`, related_content: `Based on this context, suggest ${options.count || 5} related topics: Context: ${options.context} Return as JSON array of topic strings`, }; const response = await this.chat({ messages: [{ role: 'user', content: prompts[options.type] }], temperature: 0.8, }); try { return JSON.parse(response.content); } catch { return []; } } // Function Calling for Structured Actions async functionCall(options: { query: string; functions: any[]; context?: any; }): Promise { if (!this.openai) throw new Error('OpenAI not configured'); const completion = await this.openai.chat.completions.create({ model: 'gpt-4-turbo-preview', messages: [ { role: 'system', content: 'You are an assistant that helps users interact with the Directus system.', }, { role: 'user', content: options.query, }, ], functions: options.functions, function_call: 'auto', }); const message = completion.choices[0].message; if (message.function_call) { return { function: message.function_call.name, arguments: JSON.parse(message.function_call.arguments), }; } return { message: message.content, }; } // Token Management private countTokens(messages: any[]): number { if (!this.tokenEncoder) return 0; let totalTokens = 0; for (const message of messages) { const content = typeof message === 'string' ? message : message.content; totalTokens += this.tokenEncoder.encode(content).length; } return totalTokens; } // Conversation Memory Management async storeConversation(conversation: { id: string; messages: any[]; metadata: any; }): Promise { await this.knex('ai_conversations').insert({ id: conversation.id, messages: JSON.stringify(conversation.messages), metadata: JSON.stringify(conversation.metadata), created_at: new Date(), user_id: this.accountability?.user, }); // Store embeddings for semantic search const summary = await this.generateContent({ type: 'summary', input: conversation.messages.map(m => m.content).join('\n'), length: 'short', }); const embedding = await this.createEmbedding({ text: summary }); if (this.pinecone) { const index = this.pinecone.index('conversations'); await index.upsert([ { id: conversation.id, values: embedding, metadata: { summary, user_id: this.accountability?.user, created_at: new Date().toISOString(), }, }, ]); } } async searchConversations(query: string, limit: number = 5): Promise { const results = await this.vectorSearch({ query, collection: 'conversations', topK: limit, filter: { user_id: this.accountability?.user, }, }); // Fetch full conversations const conversationIds = results.map(r => r.id); const conversations = await this.knex('ai_conversations') .whereIn('id', conversationIds) .select(); return conversations.map(conv => ({ ...conv, messages: JSON.parse(conv.messages), metadata: JSON.parse(conv.metadata), })); } } ``` ## Process: Implementing Real-time AI Features ### Step 1: WebSocket Handler ```typescript // src/websocket/ai-websocket.ts import { Server as SocketServer } from 'socket.io'; import { AIService } from '../services/ai.service'; import { Readable } from 'stream'; export function setupAIWebSocket(io: SocketServer, aiService: AIService) { const aiNamespace = io.of('/ai'); aiNamespace.on('connection', (socket) => { console.log('AI client connected:', socket.id); // Handle chat messages with streaming socket.on('ai:message', async (data) => { try { socket.emit('ai:typing'); const stream = await aiService.chat({ messages: data.history || [], model: data.config?.model || 'gpt-4-turbo-preview', temperature: data.config?.temperature || 0.7, maxTokens: data.config?.maxTokens || 2000, systemPrompt: data.config?.systemPrompt, stream: true, }); let fullResponse = ''; let tokenCount = 0; // Handle streaming response for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content || ''; if (content) { fullResponse += content; tokenCount++; socket.emit('ai:response', { chunk: content, tokens: tokenCount, }); } } // Generate suggestions based on response const suggestions = await aiService.generateSuggestions({ context: fullResponse, type: 'next_actions', count: 3, }); socket.emit('ai:complete', { content: fullResponse, tokens: tokenCount, suggestions, }); } catch (error) { socket.emit('ai:error', { error: error.message || 'An error occurred', }); } }); // Handle image generation socket.on('ai:generate-image', async (data) => { try { const imageUrl = await aiService.generateImage({ prompt: data.prompt, size: data.size || '1024x1024', style: data.style || 'vivid', }); socket.emit('ai:image', { url: imageUrl }); } catch (error) { socket.emit('ai:error', { error: error.message }); } }); // Handle voice transcription socket.on('ai:transcribe', async (data) => { try { const transcription = await aiService.transcribeAudio({ audio: data.audio, language: data.language, }); socket.emit('ai:transcription', { text: transcription }); } catch (error) { socket.emit('ai:error', { error: error.message }); } }); socket.on('disconnect', () => { console.log('AI client disconnected:', socket.id); }); }); } ``` ## AI-Powered Flows ### Custom AI Operations for Flows ```typescript // src/operations/ai-operation.ts import { defineOperationApi } from '@directus/extensions-sdk'; export default defineOperationApi({ id: 'ai-content-processor', handler: async (options, context) => { const { services } = context; const { AIService, ItemsService } = services; const aiService = new AIService({ knex: context.database }); const itemsService = new ItemsService(options.collection, { schema: await context.getSchema(), }); const results = []; // Fetch items to process const items = await itemsService.readByQuery({ filter: options.filter || {}, limit: options.batchSize || 10, }); for (const item of items) { try { let processedContent; switch (options.operation) { case 'summarize': processedContent = await aiService.generateContent({ type: 'summary', input: item[options.sourceField], length: options.summaryLength || 'short', }); break; case 'translate': processedContent = await aiService.generateContent({ type: 'translation', input: item[options.sourceField], targetLanguage: options.targetLanguage, }); break; case 'moderate': const moderation = await aiService.moderateContent( item[options.sourceField] ); processedContent = moderation.safe ? 'approved' : 'flagged'; break; case 'enrich': processedContent = await aiService.ragQuery({ query: `Enrich this content: ${item[options.sourceField]}`, collection: 'knowledge_base', }); break; case 'extract': processedContent = await aiService.functionCall({ query: `Extract ${options.extractType} from: ${item[options.sourceField]}`, functions: [ { name: 'extract_data', parameters: { type: 'object', properties: options.extractSchema, }, }, ], }); break; } // Update item with processed content await itemsService.updateOne(item.id, { [options.targetField]: processedContent, ai_processed_at: new Date(), }); results.push({ id: item.id, status: 'success', processed: processedContent, }); } catch (error) { results.push({ id: item.id, status: 'error', error: error.message, }); } } return { processed: results.length, results, }; }, }); ``` ## Natural Language Query Interface ### NLQ Implementation ```typescript // src/services/nlq.service.ts export class NaturalLanguageQueryService { constructor( private aiService: AIService, private database: any ) {} async processQuery(naturalQuery: string, collection: string): Promise { // Convert natural language to SQL/filter const structuredQuery = await this.convertToStructuredQuery( naturalQuery, collection ); // Execute query const results = await this.executeQuery(structuredQuery, collection); // Format response in natural language const nlResponse = await this.formatResponse( naturalQuery, results, collection ); return { query: structuredQuery, results, response: nlResponse, }; } private async convertToStructuredQuery(nlQuery: string, collection: string) { const schema = await this.getCollectionSchema(collection); const response = await this.aiService.functionCall({ query: nlQuery, functions: [ { name: 'create_query', description: 'Convert natural language to database query', parameters: { type: 'object', properties: { filter: { type: 'object', description: 'Directus filter object', }, sort: { type: 'array', items: { type: 'string' }, }, limit: { type: 'number', }, fields: { type: 'array', items: { type: 'string' }, }, aggregate: { type: 'object', }, }, }, }, ], context: { schema }, }); return response.arguments; } private async executeQuery(query: any, collection: string) { // Build Knex query based on structured query let knexQuery = this.database(collection); if (query.filter) { knexQuery = this.applyFilters(knexQuery, query.filter); } if (query.sort) { query.sort.forEach((sortField: string) => { const direction = sortField.startsWith('-') ? 'desc' : 'asc'; const field = sortField.replace(/^-/, ''); knexQuery = knexQuery.orderBy(field, direction); }); } if (query.limit) { knexQuery = knexQuery.limit(query.limit); } if (query.fields && query.fields.length > 0) { knexQuery = knexQuery.select(query.fields); } return await knexQuery; } private applyFilters(query: any, filters: any): any { Object.entries(filters).forEach(([field, condition]: [string, any]) => { if (typeof condition === 'object') { Object.entries(condition).forEach(([op, value]) => { switch (op) { case '_eq': query = query.where(field, '=', value); break; case '_neq': query = query.where(field, '!=', value); break; case '_lt': query = query.where(field, '<', value); break; case '_lte': query = query.where(field, '<=', value); break; case '_gt': query = query.where(field, '>', value); break; case '_gte': query = query.where(field, '>=', value); break; case '_contains': query = query.where(field, 'like', `%${value}%`); break; case '_in': query = query.whereIn(field, value); break; case '_nin': query = query.whereNotIn(field, value); break; } }); } else { query = query.where(field, '=', condition); } }); return query; } private async formatResponse( query: string, results: any[], collection: string ): Promise { const response = await this.aiService.chat({ messages: [ { role: 'user', content: `Original query: "${query}" Collection: ${collection} Results: ${JSON.stringify(results.slice(0, 10))} Please provide a natural language response summarizing these results.`, }, ], temperature: 0.7, }); return response.content; } private async getCollectionSchema(collection: string) { // Fetch and return collection schema const fields = await this.database('directus_fields') .where('collection', collection) .select('field', 'type', 'schema'); return fields; } } ``` ## AI Content Moderation Hook ```typescript // src/hooks/ai-moderation.ts import { defineHook } from '@directus/extensions-sdk'; export default defineHook(({ filter, action }, context) => { const { services, logger } = context; filter('items.create', async (payload, meta) => { // Check if content moderation is enabled for this collection const moderatedCollections = ['comments', 'posts', 'reviews']; if (moderatedCollections.includes(meta.collection)) { const aiService = new services.AIService({ knex: context.database }); // Combine all text fields for moderation const contentToModerate = Object.values(payload) .filter(value => typeof value === 'string') .join(' '); const moderation = await aiService.moderateContent(contentToModerate); if (!moderation.safe) { // Flag content for review payload.status = 'pending_review'; payload.moderation_flags = moderation.flaggedTerms; payload.moderation_scores = moderation.categories; // Log for audit logger.warn('Content flagged for moderation:', { collection: meta.collection, flags: moderation.flaggedTerms, }); } else { payload.status = 'approved'; } } return payload; }); action('items.create', async ({ payload, key, collection }) => { // Generate AI suggestions for new content if (collection === 'articles' && payload.status === 'draft') { const aiService = new services.AIService({ knex: context.database }); // Generate title suggestions if not provided if (!payload.title && payload.content) { const suggestions = await aiService.generateContent({ type: 'title', input: payload.content.substring(0, 500), }); // Store suggestions for user await context.database('content_suggestions').insert({ item_id: key, collection, type: 'title', suggestions: JSON.stringify(suggestions), }); } // Generate tags const tags = await aiService.functionCall({ query: `Extract relevant tags from: ${payload.content}`, functions: [ { name: 'extract_tags', parameters: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' }, maxItems: 10, }, }, }, }, ], }); if (tags.arguments?.tags) { await context.database('article_tags').insert( tags.arguments.tags.map((tag: string) => ({ article_id: key, tag, })) ); } } }); }); ``` ## Testing AI Features ```typescript // test/ai-service.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AIService } from '../src/services/ai.service'; describe('AI Service', () => { let aiService: AIService; beforeEach(() => { aiService = new AIService({ knex: vi.fn(), accountability: { user: 'test-user' }, }); }); describe('Chat Completion', () => { it('should generate chat response', async () => { const response = await aiService.chat({ messages: [ { role: 'user', content: 'What is Directus?' }, ], model: 'gpt-4', temperature: 0.7, }); expect(response).toHaveProperty('content'); expect(response).toHaveProperty('usage'); expect(response.content).toBeTruthy(); }); it('should handle streaming responses', async () => { const stream = await aiService.chat({ messages: [ { role: 'user', content: 'Tell me a story' }, ], stream: true, }); let fullContent = ''; for await (const chunk of stream) { fullContent += chunk.choices[0]?.delta?.content || ''; } expect(fullContent.length).toBeGreaterThan(0); }); }); describe('Content Generation', () => { it('should generate article content', async () => { const content = await aiService.generateContent({ type: 'article', input: 'Benefits of using Directus', tone: 'technical', length: 'medium', }); expect(content).toBeTruthy(); expect(content.length).toBeGreaterThan(100); }); it('should translate content', async () => { const translation = await aiService.generateContent({ type: 'translation', input: 'Hello world', targetLanguage: 'Spanish', }); expect(translation).toContain('Hola'); }); }); describe('Content Moderation', () => { it('should flag inappropriate content', async () => { const moderation = await aiService.moderateContent( 'This is a test of inappropriate content [insert bad words]' ); expect(moderation).toHaveProperty('safe'); expect(moderation).toHaveProperty('categories'); expect(moderation).toHaveProperty('flaggedTerms'); }); it('should pass safe content', async () => { const moderation = await aiService.moderateContent( 'This is a perfectly safe and appropriate message.' ); expect(moderation.safe).toBe(true); expect(moderation.flaggedTerms).toHaveLength(0); }); }); describe('Vector Search', () => { it('should create embeddings', async () => { const embedding = await aiService.createEmbedding({ text: 'Test content for embedding', }); expect(Array.isArray(embedding)).toBe(true); expect(embedding.length).toBeGreaterThan(0); }); it('should perform vector search', async () => { const results = await aiService.vectorSearch({ query: 'Find similar documents', collection: 'documents', topK: 5, }); expect(Array.isArray(results)).toBe(true); expect(results.length).toBeLessThanOrEqual(5); }); }); describe('RAG Queries', () => { it('should answer questions with context', async () => { const response = await aiService.ragQuery({ query: 'What is the pricing?', collection: 'knowledge_base', }); expect(response).toHaveProperty('answer'); expect(response).toHaveProperty('sources'); expect(response).toHaveProperty('usage'); }); }); }); ``` ## Performance Optimization ### Caching AI Responses ```typescript // src/cache/ai-cache.ts import { LRUCache } from 'lru-cache'; import crypto from 'crypto'; export class AICacheService { private cache: LRUCache; private embedCache: LRUCache; constructor() { // Response cache this.cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60, // 1 hour updateAgeOnGet: true, }); // Embedding cache (longer TTL) this.embedCache = new LRUCache({ max: 5000, ttl: 1000 * 60 * 60 * 24 * 7, // 7 days }); } getCacheKey(input: any): string { const hash = crypto.createHash('sha256'); hash.update(JSON.stringify(input)); return hash.digest('hex'); } async getCachedResponse(key: string): Promise { return this.cache.get(key); } async setCachedResponse(key: string, response: any): Promise { this.cache.set(key, response); } async getCachedEmbedding(text: string): Promise { const key = this.getCacheKey(text); return this.embedCache.get(key); } async setCachedEmbedding(text: string, embedding: number[]): Promise { const key = this.getCacheKey(text); this.embedCache.set(key, embedding); } // Batch processing optimization async batchProcess( items: T[], processor: (batch: T[]) => Promise, batchSize: number = 10 ): Promise { const results: R[] = []; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); const batchResults = await processor(batch); results.push(...batchResults); // Rate limiting if (i + batchSize < items.length) { await new Promise(resolve => setTimeout(resolve, 100)); } } return results; } } ``` ## Success Metrics - ✅ Chat interface responds in < 500ms (first token) - ✅ AI responses are contextually relevant 95%+ of the time - ✅ Content moderation catches inappropriate content with 98%+ accuracy - ✅ Vector search returns relevant results in < 200ms - ✅ RAG system provides accurate answers with source citations - ✅ Natural language queries convert correctly 90%+ of the time - ✅ WebSocket connections remain stable for extended sessions - ✅ Token usage is optimized with proper truncation - ✅ Embeddings are cached effectively reducing API calls by 70% - ✅ Error handling prevents AI failures from breaking workflows ## Resources - [OpenAI API Documentation](https://platform.openai.com/docs) - [Anthropic Claude API](https://docs.anthropic.com) - [Pinecone Vector Database](https://docs.pinecone.io) - [LangChain JS](https://js.langchain.com) - [Socket.io Documentation](https://socket.io/docs) - [Directus WebSockets](https://docs.directus.io/guides/real-time/websockets) - [Vue 3 Composition API](https://vuejs.org/guide/extras/composition-api-faq) - [TikToken for Token Counting](https://github.com/openai/tiktoken) ## Version History - **1.0.0** - Initial release with comprehensive AI integration patterns