Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/ai-writer-operation/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AI Writer Operation

Generate text based on a written prompt within Directus Flows with this custom operation, powered by [OpenAI's Text Generation API]([https://.com](https://openai.com/product)), [Anthropic](https://www.anthropic.com/) [MistralAi (via Replicate)](https://replicate.com/mistralai/mistral-7b-v0.1) and [Meta's LLama (via Replicate)](https://replicate.com/meta/meta-llama-3.1-405b-instruct).
Generate text based on a written prompt within Directus Flows with this custom operation, powered by [OpenAI's Text Generation API](https://openai.com/product), [Anthropic](https://www.anthropic.com/), [MistralAi (via Replicate)](https://replicate.com/mistralai/mistral-7b-v0.1), [Meta's LLama (via Replicate)](https://replicate.com/meta/meta-llama-3.1-405b-instruct), and any OpenAI-compatible API.

![The AI Writer operation, showing a masked OpenAI API Key field, model and prompt selection fields, and a multiline text input.](https://raw.githubusercontent.com/directus-labs/extensions/main/packages/ai-writer-operation/docs/options.png)

Expand All @@ -10,11 +10,23 @@ This operation contains some configuration options - an Api-Key, a selection of
![The output showing a string that has been grammatically fixed.](https://raw.githubusercontent.com/directus-labs/extensions/main/packages/ai-writer-operation/docs/output.png)

### API-Keys
You can generate your API-Keys on the follosing sites:
You can generate your API-Keys on the following sites:
- [OpenAI](https://platform.openai.com/api-keys)
- [Anthropic](https://console.anthropic.com/settings/workspaces/default/keys)
- [Replicate](https://replicate.com/account/api-tokens)

### OpenAI-Compatible APIs
The extension now supports any OpenAI-compatible API by selecting "OpenAI Compatible (Custom)" as the AI Provider. This allows you to use services like:
- Local models (Ollama, LM Studio, etc.)
- Cloud providers (Azure OpenAI, Google Vertex AI, etc.)
- Other OpenAI-compatible services (Groq, Together AI, etc.)

When using a custom provider, you'll need to:
1. Select "OpenAI Compatible (Custom)" as the AI Provider
2. Enter your custom API endpoint (e.g., `https://api.example.com/v1`)
3. Provide your API key
4. Choose a model from the dropdown or select "Custom Model" to enter a specific model name

## Custom Prompts

For a completely custom prompt using the "Create custom prompt" type, you will need to create a **system** message at the start of the message thread so that the Text Generation API knows how it should respond. Examples of initial system prompts can be found in the config objects of each built-in prompt in the [source code of this extension](https://github.com/directus-labs/extension-ai-writer-operation/tree/production/src/prompts). OpenAI also provides a solid overview of [how to write good prompts](https://platform.openai.com/docs/guides/prompt-engineering).
Expand Down
4 changes: 3 additions & 1 deletion packages/ai-writer-operation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
"urls": [
"https://api.openai.com/v1/**",
"https://api.anthropic.com/v1/**",
"https://api.replicate.com/v1/**"
"https://api.replicate.com/v1/**",
"https://**/v1/**",
"http://**/v1/**"
]
},
"sleep": {}
Expand Down
35 changes: 30 additions & 5 deletions packages/ai-writer-operation/src/Provider/OpenAi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,43 @@ import { Provider } from './Provider';

export class OpenAi extends Provider {
constructor(options: AiWriterOperationOptions) {
if (!options.apiKeyOpenAi) {
throw new InvalidPayloadError({ reason: 'OpenAI API Key is missing' });
}
// Determine if this is a custom OpenAI-compatible provider or standard OpenAI
const isCustomProvider = options.aiProvider === 'openai-compatible';

if (isCustomProvider) {
if (!options.apiKeyCustom) {
throw new InvalidPayloadError({ reason: 'Custom API Key is missing' });
}

if (!options.customEndpoint) {
throw new InvalidPayloadError({ reason: 'Custom Endpoint is missing' });
}

// Ensure the endpoint ends with /chat/completions for OpenAI-compatible APIs
const endpoint = options.customEndpoint.endsWith('/chat/completions')
? options.customEndpoint
: `${options.customEndpoint.replace(/\/$/, '')}/chat/completions`;

super(options, 'https://api.openai.com/v1/chat/completions', options.apiKeyOpenAi);
super(options, endpoint, options.apiKeyCustom);
} else {
if (!options.apiKeyOpenAi) {
throw new InvalidPayloadError({ reason: 'OpenAI API Key is missing' });
}

super(options, 'https://api.openai.com/v1/chat/completions', options.apiKeyOpenAi);
}
}

public async messageRequest(): Promise<string> {
const messages = this.getMessages();

// Use custom model name if provided, otherwise use the selected model
const modelName = this.options.model === 'custom' && this.options.customModelName
? this.options.customModelName
: this.options.model!;

const requestBody: RequestBody = {
model: this.options.model!,
model: modelName,
messages,
max_completion_tokens: this.options.maxToken || 0,
};
Expand Down
4 changes: 2 additions & 2 deletions packages/ai-writer-operation/src/Provider/ProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export function getProvider(options: AiWriterOperationOptions) {
return new Anthropic(options);
}

if (options.aiProvider.toLowerCase() === 'openai') {
if (options.aiProvider.toLowerCase() === 'openai' || options.aiProvider.toLowerCase() === 'openai-compatible') {
return new OpenAi(options);
}

if (options.aiProvider.toLowerCase() === 'replicate') {
return new Replicate(options);
}

throw new Error(`Unsoported AI Provider ${options.aiProvider}`);
throw new Error(`Unsupported AI Provider ${options.aiProvider}`);
}
3 changes: 3 additions & 0 deletions packages/ai-writer-operation/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export interface AiWriterOperationOptions {
apiKeyAnthropic?: string | null;
apiKeyOpenAi?: string | null;
apiKeyReplicate?: string | null;
apiKeyCustom?: string | null;
customEndpoint?: string | null;
model?: string | null;
customModelName?: string | null;
promptKey?: string | null;
system?: string | null;
json_mode?: boolean;
Expand Down
50 changes: 50 additions & 0 deletions packages/ai-writer-operation/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export default defineOperationApp({
return replicateModels;
}

if (provider === 'openai-compatible') {
// For custom providers, allow any model name to be entered
return [
{ text: 'Custom Model (enter model name below)', value: 'custom' },
...openAiModels, // Include OpenAI models as examples
];
}

return [];
};

Expand All @@ -98,6 +106,10 @@ export default defineOperationApp({
text: 'Open AI',
value: 'openai',
},
{
text: 'OpenAI Compatible (Custom)',
value: 'openai-compatible',
},
{
text: 'Replicate (Meta & Mistral)',
value: 'replicate',
Expand Down Expand Up @@ -148,6 +160,32 @@ export default defineOperationApp({
hidden: context.aiProvider !== 'replicate',
},
},
{
field: 'apiKeyCustom',
name: 'Custom API Key',
type: 'string',
meta: {
required: context.aiProvider === 'openai-compatible',
options: {
masked: true,
},
width: 'full',
interface: 'input',
hidden: context.aiProvider !== 'openai-compatible',
},
},
{
field: 'customEndpoint',
name: 'Custom Endpoint',
type: 'string',
meta: {
required: context.aiProvider === 'openai-compatible',
width: 'full',
interface: 'input',
hidden: context.aiProvider !== 'openai-compatible',
note: 'Enter the base URL of your OpenAI-compatible API (e.g., https://api.example.com/v1). The /chat/completions endpoint will be automatically appended.',
},
},
{
field: 'model',
name: 'AI Model',
Expand All @@ -161,6 +199,18 @@ export default defineOperationApp({
width: 'half',
},
},
{
field: 'customModelName',
name: 'Custom Model Name',
type: 'string',
meta: {
required: context.aiProvider === 'openai-compatible' && context.model === 'custom',
width: 'half',
interface: 'input',
hidden: !(context.aiProvider === 'openai-compatible' && context.model === 'custom'),
note: 'Enter the exact model name as expected by your API provider.',
},
},
{
field: 'maxToken',
name: 'Max Token',
Expand Down