| 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
<!-- src/ai-chat-panel.vue -->
<template>
<div class="ai-chat-panel">
<div class="chat-header">
<div class="chat-title">
<v-icon name="smart_toy" />
<span>AI Assistant</span>
</div>
<div class="chat-actions">
<v-button
v-tooltip="'Clear conversation'"
icon
x-small
@click="clearChat"
>
<v-icon name="clear_all" />
</v-button>
<v-button
v-tooltip="'Export conversation'"
icon
x-small
@click="exportChat"
>
<v-icon name="download" />
</v-button>
</div>
</div>
<div class="chat-messages" ref="messagesContainer">
<transition-group name="message-fade">
<div
v-for="message in messages"
:key="message.id"
class="message"
:class="message.role"
>
<div class="message-avatar">
<v-icon
:name="message.role === 'user' ? 'person' : 'smart_toy'"
/>
</div>
<div class="message-content">
<div class="message-text" v-html="renderMarkdown(message.content)"></div>
<div class="message-metadata">
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
<span v-if="message.tokens" class="message-tokens">
{{ message.tokens }} tokens
</span>
</div>
<div v-if="message.suggestions" class="message-suggestions">
<v-chip
v-for="suggestion in message.suggestions"
:key="suggestion"
clickable
@click="sendMessage(suggestion)"
>
{{ suggestion }}
</v-chip>
</div>
</div>
</div>
</transition-group>
<div v-if="isTyping" class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
<div v-if="streamingResponse" class="streaming-message">
<div class="message-content">
<div class="message-text" v-html="renderMarkdown(streamingResponse)"></div>
</div>
</div>
</div>
<div class="chat-input">
<div class="input-container">
<v-textarea
v-model="inputMessage"
placeholder="Type your message..."
:disabled="isProcessing"
@keydown.enter.prevent="handleEnter"
auto-grow
:rows="1"
:max-rows="4"
/>
<div class="input-actions">
<v-menu placement="top">
<template #activator="{ toggle }">
<v-button
v-tooltip="'Add context'"
icon
x-small
@click="toggle"
>
<v-icon name="attach_file" />
</v-button>
</template>
<v-list>
<v-list-item
v-for="ctx in contextOptions"
:key="ctx.value"
clickable
@click="addContext(ctx)"
>
<v-list-item-icon>
<v-icon :name="ctx.icon" />
</v-list-item-icon>
<v-list-item-content>{{ ctx.label }}</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<v-button
v-tooltip="'Voice input'"
icon
x-small
@click="startVoiceInput"
:disabled="!speechRecognitionSupported"
>
<v-icon :name="isRecording ? 'mic' : 'mic_none'" />
</v-button>
</div>
</div>
<v-button
@click="sendMessage()"
:loading="isProcessing"
:disabled="!inputMessage.trim()"
icon
>
<v-icon name="send" />
</v-button>
</div>
<div v-if="activeContext.length > 0" class="context-display">
<div class="context-header">Active Context:</div>
<div class="context-items">
<v-chip
v-for="(ctx, index) in activeContext"
:key="index"
closable
@close="removeContext(index)"
>
<v-icon :name="ctx.icon" x-small />
{{ ctx.label }}
</v-chip>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useApi, useStores } from '@directus/extensions-sdk';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { io, Socket } from 'socket.io-client';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
tokens?: number;
suggestions?: string[];
context?: any[];
}
interface Props {
collection?: string;
systemPrompt?: string;
model?: string;
maxTokens?: number;
temperature?: number;
}
const props = withDefaults(defineProps<Props>(), {
model: 'gpt-4-turbo-preview',
maxTokens: 2000,
temperature: 0.7,
});
// Composables
const api = useApi();
const { useItemsStore, useCollectionsStore } = useStores();
// State
const messages = ref<Message[]>([]);
const inputMessage = ref('');
const isProcessing = ref(false);
const isTyping = ref(false);
const streamingResponse = ref('');
const messagesContainer = ref<HTMLElement>();
const activeContext = ref<any[]>([]);
const socket = ref<Socket | null>(null);
const isRecording = ref(false);
const speechRecognition = ref<any>(null);
// Computed
const speechRecognitionSupported = computed(() => {
return 'webkitSpeechRecognition' in window || 'SpeechRecognition' in window;
});
const contextOptions = computed(() => [
{ label: 'Current Collection', value: 'collection', icon: 'folder' },
{ label: 'Selected Items', value: 'items', icon: 'check_box' },
{ label: 'Current View', value: 'view', icon: 'visibility' },
{ label: 'User Profile', value: 'profile', icon: 'person' },
{ label: 'Schema Info', value: 'schema', icon: 'schema' },
]);
// WebSocket Setup
function initializeWebSocket() {
const baseURL = api.defaults.baseURL || window.location.origin;
socket.value = io(baseURL, {
path: '/ai/socket',
transports: ['websocket'],
auth: {
access_token: api.defaults.headers.common['Authorization']?.replace('Bearer ', ''),
},
});
socket.value.on('connect', () => {
console.log('AI WebSocket connected');
});
socket.value.on('ai:response', handleStreamingResponse);
socket.value.on('ai:complete', handleResponseComplete);
socket.value.on('ai:error', handleResponseError);
socket.value.on('ai:typing', () => {
isTyping.value = true;
});
}
// Message Handling
async function sendMessage(content?: string) {
const messageContent = content || inputMessage.value.trim();
if (!messageContent || isProcessing.value) return;
isProcessing.value = true;
inputMessage.value = '';
// Add user message
const userMessage: Message = {
id: generateId(),
role: 'user',
content: messageContent,
timestamp: new Date(),
context: [...activeContext.value],
};
messages.value.push(userMessage);
scrollToBottom();
try {
// Prepare context
const context = await prepareContext();
// Send via WebSocket for streaming
if (socket.value?.connected) {
socket.value.emit('ai:message', {
message: messageContent,
context,
history: messages.value.slice(-10), // Last 10 messages
config: {
model: props.model,
maxTokens: props.maxTokens,
temperature: props.temperature,
systemPrompt: props.systemPrompt,
},
});
isTyping.value = true;
streamingResponse.value = '';
} else {
// Fallback to REST API
const response = await api.post('/ai/chat', {
message: messageContent,
context,
history: messages.value.slice(-10),
config: {
model: props.model,
maxTokens: props.maxTokens,
temperature: props.temperature,
},
});
handleResponseComplete(response.data);
}
} catch (error) {
console.error('Error sending message:', error);
handleResponseError({ error: 'Failed to send message' });
}
}
function handleStreamingResponse(data: { chunk: string; tokens?: number }) {
isTyping.value = false;
streamingResponse.value += data.chunk;
scrollToBottom();
}
function handleResponseComplete(data: any) {
isTyping.value = false;
const assistantMessage: Message = {
id: generateId(),
role: 'assistant',
content: streamingResponse.value || data.content,
timestamp: new Date(),
tokens: data.tokens,
suggestions: data.suggestions,
};
messages.value.push(assistantMessage);
streamingResponse.value = '';
isProcessing.value = false;
scrollToBottom();
// Store conversation
storeConversation();
}
function handleResponseError(data: { error: string }) {
isTyping.value = false;
isProcessing.value = false;
streamingResponse.value = '';
messages.value.push({
id: generateId(),
role: 'system',
content: `Error: ${data.error}`,
timestamp: new Date(),
});
}
// Context Management
async function prepareContext(): Promise<any> {
const context: any = {
timestamp: new Date().toISOString(),
user: api.defaults.headers.common['User-Agent'],
};
for (const ctx of activeContext.value) {
switch (ctx.value) {
case 'collection':
if (props.collection) {
const itemsStore = useItemsStore();
const items = await itemsStore.getItems(props.collection, {
limit: 5,
fields: ['*'],
});
context.collection = {
name: props.collection,
items,
};
}
break;
case 'schema':
if (props.collection) {
const collectionsStore = useCollectionsStore();
const collection = collectionsStore.getCollection(props.collection);
context.schema = collection;
}
break;
case 'profile':
context.user = await fetchUserProfile();
break;
}
}
return context;
}
function addContext(option: any) {
if (!activeContext.value.find(c => c.value === option.value)) {
activeContext.value.push(option);
}
}
function removeContext(index: number) {
activeContext.value.splice(index, 1);
}
// Voice Input
function startVoiceInput() {
if (!speechRecognitionSupported.value) return;
const SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition;
speechRecognition.value = new SpeechRecognition();
speechRecognition.value.continuous = false;
speechRecognition.value.interimResults = true;
speechRecognition.value.onstart = () => {
isRecording.value = true;
};
speechRecognition.value.onresult = (event: any) => {
const transcript = Array.from(event.results)
.map((result: any) => result[0])
.map((result: any) => result.transcript)
.join('');
inputMessage.value = transcript;
};
speechRecognition.value.onerror = (event: any) => {
console.error('Speech recognition error:', event.error);
isRecording.value = false;
};
speechRecognition.value.onend = () => {
isRecording.value = false;
};
speechRecognition.value.start();
}
// Utility Functions
function renderMarkdown(content: string): string {
const rendered = marked(content, {
breaks: true,
gfm: true,
highlight: (code, lang) => {
// Add syntax highlighting if available
return `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`;
},
});
return DOMPurify.sanitize(rendered);
}
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatTime(timestamp: Date): string {
return new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
}).format(timestamp);
}
function handleEnter(event: KeyboardEvent) {
if (!event.shiftKey) {
sendMessage();
}
}
function scrollToBottom() {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
});
}
function generateId(): string {
return `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
function clearChat() {
messages.value = [];
activeContext.value = [];
streamingResponse.value = '';
}
async function exportChat() {
const conversation = messages.value.map(msg => ({
role: msg.role,
content: msg.content,
timestamp: msg.timestamp.toISOString(),
}));
const blob = new Blob([JSON.stringify(conversation, null, 2)], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat-export-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
async function storeConversation() {
try {
await api.post('/ai/conversations', {
messages: messages.value,
context: activeContext.value,
metadata: {
model: props.model,
collection: props.collection,
},
});
} catch (error) {
console.error('Failed to store conversation:', error);
}
}
async function fetchUserProfile() {
try {
const response = await api.get('/users/me');
return response.data.data;
} catch (error) {
return null;
}
}
// Load previous conversation
async function loadConversation() {
try {
const response = await api.get('/ai/conversations/latest');
if (response.data.data) {
messages.value = response.data.data.messages.map((msg: any) => ({
...msg,
timestamp: new Date(msg.timestamp),
}));
scrollToBottom();
}
} catch (error) {
console.error('Failed to load conversation:', error);
}
}
// Lifecycle
onMounted(() => {
initializeWebSocket();
loadConversation();
});
onUnmounted(() => {
if (socket.value) {
socket.value.disconnect();
}
if (speechRecognition.value) {
speechRecognition.value.stop();
}
});
</script>
<style scoped>
.ai-chat-panel {
height: 100%;
display: flex;
flex-direction: column;
background: var(--theme--background);
border-radius: var(--theme--border-radius);
border: 1px solid var(--theme--border-color-subdued);
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-m);
border-bottom: 1px solid var(--theme--border-color-subdued);
background: var(--theme--background-accent);
}
.chat-title {
display: flex;
align-items: center;
gap: var(--spacing-s);
font-weight: 600;
color: var(--theme--foreground);
}
.chat-actions {
display: flex;
gap: var(--spacing-xs);
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: var(--spacing-m);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.message {
display: flex;
gap: var(--spacing-m);
animation: messageSlide 0.3s ease-out;
}
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--theme--primary-background);
color: var(--theme--primary);
flex-shrink: 0;
}
.message.user .message-avatar {
background: var(--theme--background-accent);
color: var(--theme--foreground);
}
.message-content {
max-width: 70%;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.message.user .message-content {
align-items: flex-end;
}
.message-text {
padding: var(--spacing-m);
background: var(--theme--background-accent);
border-radius: var(--theme--border-radius);
color: var(--theme--foreground);
line-height: 1.5;
}
.message.user .message-text {
background: var(--theme--primary);
color: white;
}
/* Markdown styling */
.message-text :deep(p) {
margin: 0 0 var(--spacing-s) 0;
}
.message-text :deep(p:last-child) {
margin-bottom: 0;
}
.message-text :deep(pre) {
background: var(--theme--background);
padding: var(--spacing-s);
border-radius: var(--theme--border-radius);
overflow-x: auto;
margin: var(--spacing-s) 0;
}
.message-text :deep(code) {
background: var(--theme--background);
padding: 2px 4px;
border-radius: 3px;
font-size: 0.9em;
}
.message-text :deep(ul),
.message-text :deep(ol) {
margin: var(--spacing-s) 0;
padding-left: var(--spacing-l);
}
.message-metadata {
display: flex;
gap: var(--spacing-m);
font-size: 0.75rem;
color: var(--theme--foreground-subdued);
}
.message-suggestions {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
margin-top: var(--spacing-s);
}
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
padding: var(--spacing-m);
background: var(--theme--background-accent);
border-radius: var(--theme--border-radius);
width: fit-content;
}
.typing-indicator span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--theme--foreground-subdued);
animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.5;
}
30% {
transform: translateY(-10px);
opacity: 1;
}
}
.streaming-message {
display: flex;
gap: var(--spacing-m);
}
.chat-input {
display: flex;
gap: var(--spacing-s);
padding: var(--spacing-m);
border-top: 1px solid var(--theme--border-color-subdued);
background: var(--theme--background-accent);
}
.input-container {
flex: 1;
display: flex;
align-items: flex-end;
gap: var(--spacing-xs);
background: var(--theme--background);
border-radius: var(--theme--border-radius);
padding: var(--spacing-s);
}
.input-container :deep(.v-textarea) {
flex: 1;
background: transparent;
border: none;
}
.input-actions {
display: flex;
gap: var(--spacing-xs);
}
.context-display {
padding: var(--spacing-s) var(--spacing-m);
background: var(--theme--background-subdued);
border-top: 1px solid var(--theme--border-color-subdued);
}
.context-header {
font-size: 0.875rem;
color: var(--theme--foreground-subdued);
margin-bottom: var(--spacing-xs);
}
.context-items {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
/* Mobile responsive */
@media (max-width: 768px) {
.message-content {
max-width: 85%;
}
.chat-messages {
padding: var(--spacing-s);
}
.message-text {
padding: var(--spacing-s);
}
}
/* Message fade transition */
.message-fade-enter-active,
.message-fade-leave-active {
transition: all 0.3s ease;
}
.message-fade-enter-from {
opacity: 0;
transform: translateY(20px);
}
.message-fade-leave-to {
opacity: 0;
transform: translateX(-20px);
}
</style>
Process: Implementing AI Service Layer
Step 1: Create AI Service
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 = ;
: | = ;
: | = ;
: ;
() {
(options);
.();
}
() {
(process..) {
. = ({
: process..,
});
. = ();
}
(process..) {
. = ({
: process..,
});
}
(process..) {
. = ({
: process..,
: process.. || ,
});
}
}
(: {
: [];
?: ;
?: ;
?: ;
?: ;
?: ;
}): <> {
model = options. || ;
temperature = options. ?? ;
maxTokens = options. || ;
messages = options.
? [{ : , : options. }, ...options.]
: options.;
totalTokens = .(messages);
(totalTokens > ) {
messages.(, messages. - );
}
(model.()) {
.(messages, model, temperature, maxTokens, options.);
} {
.(messages, model, temperature, maxTokens, options.);
}
}
(
: [],
: ,
: ,
: ,
?:
): <> {
(!.) ();
(stream) {
stream = ....({
model,
messages,
temperature,
: maxTokens,
: ,
});
stream;
} {
completion = ....({
model,
messages,
temperature,
: maxTokens,
: { : },
});
{
: completion.[]..,
: completion.,
: completion.,
};
}
}
(
: [],
: ,
: ,
: ,
?:
): <> {
(!.) ();
systemPrompt = messages.( m. === )?. || ;
conversationMessages = messages.( m. !== );
response = ...({
: model.(, ),
: maxTokens,
temperature,
: systemPrompt,
: conversationMessages,
stream,
});
(stream) {
response;
} {
{
: response.[].,
: {
: response..,
: response..,
},
: response.,
};
}
}
(: {
: | | | ;
: ;
?: ;
?: | | | ;
?: | | ;
}): <> {
prompts = {
: ,
: ,
: ,
: ,
};
response = .({
: [{ : , : prompts[options.] }],
: options. === ? : ,
});
response.;
}
(: ): <[]> {
(!.) ();
response = ...({
: options. || ,
: options.,
: options. || ,
});
response.[].;
}
(: {
: ;
: ;
?: ;
?: ;
}): <[]> {
(!.) ();
queryEmbedding = .({ : options. });
index = ..(options.);
results = index.({
: queryEmbedding,
: options. || ,
: options.,
: ,
: ,
});
results. || [];
}
(: {
: ;
: ;
?: ;
?: ;
}): <> {
relevantDocs = .({
: options.,
: options.,
: options. || ,
});
context = relevantDocs
.( doc.?. || )
.();
systemPrompt = ;
response = .({
: [{ : , : options. }],
systemPrompt,
: ,
});
{
: response.,
: relevantDocs.( ({
: doc.,
: doc.,
: doc.,
})),
: response.,
};
}
(: ): <{
: ;
: ;
: [];
}> {
(!.) ();
moderation = ...({
: content,
});
result = moderation.[];
flaggedCategories = .(result.)
.( flagged)
.( category);
{
: !result.,
: result.,
: flaggedCategories,
};
}
(: {
: ;
: | | ;
?: ;
}): <[]> {
prompts = {
: ,
: ,
: ,
};
response = .({
: [{ : , : prompts[options.] }],
: ,
});
{
.(response.);
} {
[];
}
}
(: {
: ;
: [];
?: ;
}): <> {
(!.) ();
completion = ....({
: ,
: [
{
: ,
: ,
},
{
: ,
: options.,
},
],
: options.,
: ,
});
message = completion.[].;
(message.) {
{
: message..,
: .(message..),
};
}
{
: message.,
};
}
(: []): {
(!.) ;
totalTokens = ;
( message messages) {
content = message === ? message : message.;
totalTokens += ..(content).;
}
totalTokens;
}
(: {
: ;
: [];
: ;
}): <> {
.().({
: conversation.,
: .(conversation.),
: .(conversation.),
: (),
: .?.,
});
summary = .({
: ,
: conversation..( m.).(),
: ,
});
embedding = .({ : summary });
(.) {
index = ..();
index.([
{
: conversation.,
: embedding,
: {
summary,
: .?.,
: ().(),
},
},
]);
}
}
(: , : = ): <[]> {
results = .({
query,
: ,
: limit,
: {
: .?.,
},
});
conversationIds = results.( r.);
conversations = .()
.(, conversationIds)
.();
conversations.( ({
...conv,
: .(conv.),
: .(conv.),
}));
}
}
Process: Implementing Real-time AI Features
Step 1: WebSocket Handler
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);
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',
: data.?. || ,
: data.?. || ,
: data.?.,
: ,
});
fullResponse = ;
tokenCount = ;
( chunk stream) {
content = chunk.[]?.?. || ;
(content) {
fullResponse += content;
tokenCount++;
socket.(, {
: content,
: tokenCount,
});
}
}
suggestions = aiService.({
: fullResponse,
: ,
: ,
});
socket.(, {
: fullResponse,
: tokenCount,
suggestions,
});
} (error) {
socket.(, {
: error. || ,
});
}
});
socket.(, (data) => {
{
imageUrl = aiService.({
: data.,
: data. || ,
: data. || ,
});
socket.(, { : imageUrl });
} (error) {
socket.(, { : error. });
}
});
socket.(, (data) => {
{
transcription = aiService.({
: data.,
: data.,
});
socket.(, { : transcription });
} (error) {
socket.(, { : error. });
}
});
socket.(, {
.(, socket.);
});
});
}
AI-Powered Flows
Custom AI Operations for Flows
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 = [];
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({
: ,
: item[options.],
: options. || ,
});
;
:
processedContent = aiService.({
: ,
: item[options.],
: options.,
});
;
:
moderation = aiService.(
item[options.]
);
processedContent = moderation. ? : ;
;
:
processedContent = aiService.({
: ,
: ,
});
;
:
processedContent = aiService.({
: ,
: [
{
: ,
: {
: ,
: options.,
},
},
],
});
;
}
itemsService.(item., {
[options.]: processedContent,
: (),
});
results.({
: item.,
: ,
: processedContent,
});
} (error) {
results.({
: item.,
: ,
: error.,
});
}
}
{
: results.,
results,
};
},
});
Natural Language Query Interface
NLQ Implementation
export class NaturalLanguageQueryService {
constructor(
private aiService: AIService,
private database: any
) {}
async processQuery(naturalQuery: string, collection: string): Promise<any> {
const structuredQuery = await this.convertToStructuredQuery(
naturalQuery,
collection
);
const results = await this.executeQuery(structuredQuery, collection);
const nlResponse = await this.formatResponse(
naturalQuery,
results,
collection
);
return {
query: structuredQuery,
results,
response: nlResponse,
};
}
private async convertToStructuredQuery(nlQuery: string, collection: string) {
const schema = await .(collection);
response = ..({
: nlQuery,
: [
{
: ,
: ,
: {
: ,
: {
: {
: ,
: ,
},
: {
: ,
: { : },
},
: {
: ,
},
: {
: ,
: { : },
},
: {
: ,
},
},
},
},
],
: { schema },
});
response.;
}
() {
knexQuery = .(collection);
(query.) {
knexQuery = .(knexQuery, query.);
}
(query.) {
query..( {
direction = sortField.() ? : ;
field = sortField.(, );
knexQuery = knexQuery.(field, direction);
});
}
(query.) {
knexQuery = knexQuery.(query.);
}
(query. && query.. > ) {
knexQuery = knexQuery.(query.);
}
knexQuery;
}
(: , : ): {
.(filters).( {
( condition === ) {
.(condition).( {
(op) {
:
query = query.(field, , value);
;
:
query = query.(field, , value);
;
:
query = query.(field, , value);
;
:
query = query.(field, , value);
;
:
query = query.(field, , value);
;
:
query = query.(field, , value);
;
:
query = query.(field, , );
;
:
query = query.(field, value);
;
:
query = query.(field, value);
;
}
});
} {
query = query.(field, , condition);
}
});
query;
}
(
: ,
: [],
:
): <> {
response = ..({
: [
{
: ,
: ,
},
],
: ,
});
response.;
}
() {
fields = .()
.(, collection)
.(, , );
fields;
}
}
AI Content Moderation Hook
import { defineHook } from '@directus/extensions-sdk';
export default defineHook(({ filter, action }, context) => {
const { services, logger } = context;
filter('items.create', async (payload, meta) => {
const moderatedCollections = ['comments', 'posts', 'reviews'];
if (moderatedCollections.includes(meta.collection)) {
const aiService = new services.AIService({ knex: context.database });
const contentToModerate = Object.values(payload)
.filter(value => typeof value === 'string')
.join(' ');
const moderation = await aiService.moderateContent(contentToModerate);
if (!moderation.safe) {
payload.status = 'pending_review';
payload.moderation_flags = moderation.;
payload. = moderation.;
logger.(, {
: meta.,
: moderation.,
});
} {
payload. = ;
}
}
payload;
});
(, ({ payload, key, collection }) => {
(collection === && payload. === ) {
aiService = services.({ : context. });
(!payload. && payload.) {
suggestions = aiService.({
: ,
: payload..(, ),
});
context.().({
: key,
collection,
: ,
: .(suggestions),
});
}
tags = aiService.({
: ,
: [
{
: ,
: {
: ,
: {
: {
: ,
: { : },
: ,
},
},
},
},
],
});
(tags.?.) {
context.().(
tags...( ({
: key,
tag,
}))
);
}
}
});
});
Testing AI Features
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).();
});
(, () => {
stream = aiService.({
: [
{ : , : },
],
: ,
});
fullContent = ;
( chunk stream) {
fullContent += chunk.[]?.?. || ;
}
(fullContent.).();
});
});
(, {
(, () => {
content = aiService.({
: ,
: ,
: ,
: ,
});
(content).();
(content.).();
});
(, () => {
translation = aiService.({
: ,
: ,
: ,
});
(translation).();
});
});
(, {
(, () => {
moderation = aiService.(
);
(moderation).();
(moderation).();
(moderation).();
});
(, () => {
moderation = aiService.(
);
(moderation.).();
(moderation.).();
});
});
(, {
(, () => {
embedding = aiService.({
: ,
});
(.(embedding)).();
(embedding.).();
});
(, () => {
results = aiService.({
: ,
: ,
: ,
});
(.(results)).();
(results.).();
});
});
(, {
(, () => {
response = aiService.({
: ,
: ,
});
(response).();
(response).();
(response).();
});
});
});
Performance Optimization
Caching AI Responses
import { LRUCache } from 'lru-cache';
import crypto from 'crypto';
export class AICacheService {
private cache: LRUCache<string, any>;
private embedCache: LRUCache<string, number[]>;
constructor() {
this.cache = new LRUCache({
max: 1000,
ttl: 1000 * 60 * 60,
updateAgeOnGet: true,
});
this.embedCache = new LRUCache({
max: 5000,
ttl: 1000 * 60 * 60 * 24 * 7,
});
}
getCacheKey(input: any): {
hash = crypto.();
hash.(.(input));
hash.();
}
(: ): < | > {
..(key);
}
(: , : ): <> {
..(key, response);
}
(: ): <[] | > {
key = .(text);
..(key);
}
(: , : []): <> {
key = .(text);
..(key, embedding);
}
batchProcess<T, R>(
: T[],
: <R[]>,
: =
): <R[]> {
: R[] = [];
( i = ; i < items.; i += batchSize) {
batch = items.(i, i + batchSize);
batchResults = (batch);
results.(...batchResults);
(i + batchSize < items.) {
( (resolve, ));
}
}
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
Version History
- 1.0.0 - Initial release with comprehensive AI integration patterns