@directus/api 33.0.0 → 33.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.
- package/dist/ai/chat/controllers/chat.post.js +19 -4
- package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
- package/dist/ai/chat/lib/create-ui-stream.js +28 -25
- package/dist/ai/chat/middleware/load-settings.js +31 -7
- package/dist/ai/chat/models/chat-request.d.ts +135 -2
- package/dist/ai/chat/models/chat-request.js +56 -2
- package/dist/ai/chat/models/providers.d.ts +16 -2
- package/dist/ai/chat/models/providers.js +16 -2
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
- package/dist/ai/chat/utils/format-context.d.ts +5 -0
- package/dist/ai/chat/utils/format-context.js +122 -0
- package/dist/ai/mcp/server.d.ts +27 -1
- package/dist/ai/providers/index.d.ts +3 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/options.d.ts +14 -0
- package/dist/ai/providers/options.js +26 -0
- package/dist/ai/providers/registry.d.ts +6 -0
- package/dist/ai/providers/registry.js +65 -0
- package/dist/ai/providers/types.d.ts +34 -0
- package/dist/ai/providers/types.js +1 -0
- package/dist/ai/tools/items/index.js +4 -1
- package/dist/ai/tools/items/prompt.md +7 -9
- package/dist/ai/tools/schema.js +1 -1
- package/dist/app.js +4 -0
- package/dist/auth/drivers/ldap.d.ts +1 -1
- package/dist/auth/drivers/ldap.js +142 -137
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +25 -1
- package/dist/cli/utils/create-env/env-stub.liquid +3 -0
- package/dist/controllers/deployment.d.ts +2 -0
- package/dist/controllers/deployment.js +481 -0
- package/dist/controllers/fields.js +6 -4
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
- package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
- package/dist/database/migrations/20260204A-add-deployment.js +32 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
- package/dist/deployment/deployment.d.ts +94 -0
- package/dist/deployment/deployment.js +29 -0
- package/dist/deployment/drivers/index.d.ts +1 -0
- package/dist/deployment/drivers/index.js +1 -0
- package/dist/deployment/drivers/vercel.d.ts +32 -0
- package/dist/deployment/drivers/vercel.js +208 -0
- package/dist/deployment/index.d.ts +2 -0
- package/dist/deployment/index.js +2 -0
- package/dist/deployment.d.ts +24 -0
- package/dist/deployment.js +39 -0
- package/dist/middleware/respond.js +27 -14
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
- package/dist/server.js +2 -1
- package/dist/services/deployment-projects.d.ts +20 -0
- package/dist/services/deployment-projects.js +34 -0
- package/dist/services/deployment-runs.d.ts +13 -0
- package/dist/services/deployment-runs.js +6 -0
- package/dist/services/deployment.d.ts +40 -0
- package/dist/services/deployment.js +202 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -3
- package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/server.js +1 -0
- package/dist/services/specifications.js +2 -2
- package/dist/services/versions.js +1 -1
- package/dist/telemetry/lib/get-report.js +2 -0
- package/dist/telemetry/types/report.d.ts +8 -0
- package/dist/telemetry/utils/get-settings.d.ts +2 -0
- package/dist/telemetry/utils/get-settings.js +5 -0
- package/dist/utils/deep-map-response.d.ts +1 -1
- package/dist/utils/deep-map-response.js +1 -1
- package/dist/utils/get-column-path.js +1 -1
- package/dist/utils/get-service.js +7 -1
- package/dist/utils/is-field-allowed.d.ts +4 -0
- package/dist/utils/is-field-allowed.js +9 -0
- package/dist/utils/versioning/handle-version.js +1 -1
- package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
- package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
- package/dist/websocket/collab/collab.d.ts +63 -0
- package/dist/websocket/collab/collab.js +481 -0
- package/dist/websocket/collab/constants.d.ts +1 -0
- package/dist/websocket/collab/constants.js +13 -0
- package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
- package/dist/websocket/collab/filter-to-fields.js +11 -0
- package/dist/websocket/collab/messenger.d.ts +43 -0
- package/dist/websocket/collab/messenger.js +225 -0
- package/dist/websocket/collab/payload-permissions.d.ts +18 -0
- package/dist/websocket/collab/payload-permissions.js +158 -0
- package/dist/websocket/collab/permissions-cache.d.ts +52 -0
- package/dist/websocket/collab/permissions-cache.js +204 -0
- package/dist/websocket/collab/room.d.ts +125 -0
- package/dist/websocket/collab/room.js +593 -0
- package/dist/websocket/collab/store.d.ts +7 -0
- package/dist/websocket/collab/store.js +33 -0
- package/dist/websocket/collab/types.d.ts +21 -0
- package/dist/websocket/collab/types.js +1 -0
- package/dist/websocket/collab/verify-permissions.d.ts +11 -0
- package/dist/websocket/collab/verify-permissions.js +100 -0
- package/dist/websocket/handlers/index.d.ts +2 -0
- package/dist/websocket/handlers/index.js +9 -0
- package/dist/websocket/utils/items.d.ts +2 -2
- package/dist/websocket/utils/message.d.ts +1 -1
- package/dist/websocket/utils/message.js +2 -2
- package/package.json +32 -30
- package/dist/utils/get-relation-info.d.ts +0 -6
- package/dist/utils/get-relation-info.js +0 -43
- package/dist/utils/get-relation-type.d.ts +0 -6
- package/dist/utils/get-relation-type.js +0 -18
- package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
- package/dist/utils/versioning/deep-map-with-schema.js +0 -81
|
@@ -13,7 +13,21 @@ export const aiChatPostHandler = async (req, res, _next) => {
|
|
|
13
13
|
if (!parseResult.success) {
|
|
14
14
|
throw new InvalidPayloadError({ reason: fromZodError(parseResult.error).message });
|
|
15
15
|
}
|
|
16
|
-
const { provider, model, messages: rawMessages, tools: requestedTools, toolApprovals } = parseResult.data;
|
|
16
|
+
const { provider, model, messages: rawMessages, tools: requestedTools, toolApprovals, context } = parseResult.data;
|
|
17
|
+
const aiSettings = res.locals['ai'].settings;
|
|
18
|
+
const allowedModelsMap = {
|
|
19
|
+
openai: aiSettings.openaiAllowedModels,
|
|
20
|
+
anthropic: aiSettings.anthropicAllowedModels,
|
|
21
|
+
google: aiSettings.googleAllowedModels,
|
|
22
|
+
};
|
|
23
|
+
// For standard providers: null/empty = no models allowed, must be in list
|
|
24
|
+
// openai-compatible skips validation entirely
|
|
25
|
+
if (provider !== 'openai-compatible') {
|
|
26
|
+
const allowedModels = allowedModelsMap[provider];
|
|
27
|
+
if (!allowedModels || allowedModels.length === 0 || !allowedModels.includes(model)) {
|
|
28
|
+
throw new ForbiddenError({ reason: 'Model not allowed for this provider' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
17
31
|
if (rawMessages.length === 0) {
|
|
18
32
|
throw new InvalidPayloadError({ reason: `"messages" must not be empty` });
|
|
19
33
|
}
|
|
@@ -33,12 +47,13 @@ export const aiChatPostHandler = async (req, res, _next) => {
|
|
|
33
47
|
if (validationResult.success === false) {
|
|
34
48
|
throw new InvalidPayloadError({ reason: validationResult.error.message });
|
|
35
49
|
}
|
|
36
|
-
const stream = createUiStream(validationResult.data, {
|
|
50
|
+
const stream = await createUiStream(validationResult.data, {
|
|
37
51
|
provider,
|
|
38
52
|
model,
|
|
39
|
-
tools
|
|
40
|
-
|
|
53
|
+
tools,
|
|
54
|
+
aiSettings,
|
|
41
55
|
systemPrompt: res.locals['ai'].systemPrompt,
|
|
56
|
+
...(context && { context }),
|
|
42
57
|
onUsage: (usage) => {
|
|
43
58
|
res.write(`data: ${JSON.stringify({ type: 'data-usage', data: usage })}\n\n`);
|
|
44
59
|
},
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import type { ProviderType } from '@directus/ai';
|
|
1
2
|
import { type LanguageModelUsage, type StreamTextResult, type Tool, type UIMessage } from 'ai';
|
|
3
|
+
import { type AISettings } from '../../providers/index.js';
|
|
4
|
+
import type { ChatContext } from '../models/chat-request.js';
|
|
2
5
|
export interface CreateUiStreamOptions {
|
|
3
|
-
provider:
|
|
6
|
+
provider: ProviderType;
|
|
4
7
|
model: string;
|
|
5
8
|
tools: {
|
|
6
9
|
[x: string]: Tool;
|
|
7
10
|
};
|
|
8
|
-
|
|
9
|
-
openai: string | null;
|
|
10
|
-
anthropic: string | null;
|
|
11
|
-
};
|
|
11
|
+
aiSettings: AISettings;
|
|
12
12
|
systemPrompt?: string;
|
|
13
|
+
context?: ChatContext;
|
|
13
14
|
onUsage?: (usage: Pick<LanguageModelUsage, 'inputTokens' | 'outputTokens' | 'totalTokens'>) => void | Promise<void>;
|
|
14
15
|
}
|
|
15
|
-
export declare const createUiStream: (messages: UIMessage[], { provider, model, tools,
|
|
16
|
+
export declare const createUiStream: (messages: UIMessage[], { provider, model, tools, aiSettings, systemPrompt, context, onUsage }: CreateUiStreamOptions) => Promise<StreamTextResult<Record<string, Tool<any, any>>, any>>;
|
|
@@ -1,36 +1,39 @@
|
|
|
1
|
-
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
|
-
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
1
|
import { ServiceUnavailableError } from '@directus/errors';
|
|
4
2
|
import { convertToModelMessages, stepCountIs, streamText, } from 'ai';
|
|
3
|
+
import { buildProviderConfigs, createAIProviderRegistry, getProviderOptions, } from '../../providers/index.js';
|
|
5
4
|
import { SYSTEM_PROMPT } from '../constants/system-prompt.js';
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { formatContextForSystemPrompt } from '../utils/format-context.js';
|
|
6
|
+
export const createUiStream = async (messages, { provider, model, tools, aiSettings, systemPrompt, context, onUsage }) => {
|
|
7
|
+
const configs = buildProviderConfigs(aiSettings);
|
|
8
|
+
const providerConfig = configs.find((c) => c.type === provider);
|
|
9
|
+
if (!providerConfig) {
|
|
8
10
|
throw new ServiceUnavailableError({ service: provider, reason: 'No API key configured for LLM provider' });
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
throw new Error(`Unexpected provider given: "${provider}"`);
|
|
19
|
-
}
|
|
20
|
-
systemPrompt ||= SYSTEM_PROMPT;
|
|
12
|
+
const registry = createAIProviderRegistry(configs, aiSettings);
|
|
13
|
+
const baseSystemPrompt = systemPrompt || SYSTEM_PROMPT;
|
|
14
|
+
const contextBlock = context ? formatContextForSystemPrompt(context) : null;
|
|
15
|
+
const providerOptions = getProviderOptions(provider, model, aiSettings);
|
|
16
|
+
// Compute the full system prompt once to avoid re-computing on each step
|
|
17
|
+
const fullSystemPrompt = contextBlock ? baseSystemPrompt + contextBlock : baseSystemPrompt;
|
|
21
18
|
const stream = streamText({
|
|
22
|
-
system:
|
|
23
|
-
model:
|
|
24
|
-
messages: convertToModelMessages(messages),
|
|
19
|
+
system: baseSystemPrompt,
|
|
20
|
+
model: registry.languageModel(`${provider}:${model}`),
|
|
21
|
+
messages: await convertToModelMessages(messages),
|
|
25
22
|
stopWhen: [stepCountIs(10)],
|
|
26
|
-
providerOptions
|
|
27
|
-
openai: {
|
|
28
|
-
reasoningSummary: 'auto',
|
|
29
|
-
store: false,
|
|
30
|
-
include: ['reasoning.encrypted_content'],
|
|
31
|
-
},
|
|
32
|
-
},
|
|
23
|
+
providerOptions,
|
|
33
24
|
tools,
|
|
25
|
+
/**
|
|
26
|
+
* prepareStep is called before each AI step to prepare the system prompt.
|
|
27
|
+
* When context exists, we override the system prompt to include context attachments.
|
|
28
|
+
* This allows the initial system prompt to be simple while ensuring all steps
|
|
29
|
+
* (including tool continuation steps) receive the full context.
|
|
30
|
+
*/
|
|
31
|
+
prepareStep: () => {
|
|
32
|
+
if (contextBlock) {
|
|
33
|
+
return { system: fullSystemPrompt };
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
},
|
|
34
37
|
onFinish({ usage }) {
|
|
35
38
|
if (onUsage) {
|
|
36
39
|
const { inputTokens, outputTokens, totalTokens } = usage;
|
|
@@ -4,15 +4,39 @@ export const loadSettings = async (_req, res, next) => {
|
|
|
4
4
|
const service = new SettingsService({
|
|
5
5
|
schema: await getSchema(),
|
|
6
6
|
});
|
|
7
|
-
const
|
|
8
|
-
fields: [
|
|
7
|
+
const settings = await service.readSingleton({
|
|
8
|
+
fields: [
|
|
9
|
+
'ai_openai_api_key',
|
|
10
|
+
'ai_anthropic_api_key',
|
|
11
|
+
'ai_google_api_key',
|
|
12
|
+
'ai_openai_compatible_api_key',
|
|
13
|
+
'ai_openai_compatible_base_url',
|
|
14
|
+
'ai_openai_compatible_name',
|
|
15
|
+
'ai_openai_compatible_models',
|
|
16
|
+
'ai_openai_compatible_headers',
|
|
17
|
+
'ai_openai_allowed_models',
|
|
18
|
+
'ai_anthropic_allowed_models',
|
|
19
|
+
'ai_google_allowed_models',
|
|
20
|
+
'ai_system_prompt',
|
|
21
|
+
],
|
|
9
22
|
});
|
|
23
|
+
const aiSettings = {
|
|
24
|
+
openaiApiKey: settings['ai_openai_api_key'] ?? null,
|
|
25
|
+
anthropicApiKey: settings['ai_anthropic_api_key'] ?? null,
|
|
26
|
+
googleApiKey: settings['ai_google_api_key'] ?? null,
|
|
27
|
+
openaiCompatibleApiKey: settings['ai_openai_compatible_api_key'] ?? null,
|
|
28
|
+
openaiCompatibleBaseUrl: settings['ai_openai_compatible_base_url'] ?? null,
|
|
29
|
+
openaiCompatibleName: settings['ai_openai_compatible_name'] ?? null,
|
|
30
|
+
openaiCompatibleModels: settings['ai_openai_compatible_models'] ?? null,
|
|
31
|
+
openaiCompatibleHeaders: settings['ai_openai_compatible_headers'] ?? null,
|
|
32
|
+
openaiAllowedModels: settings['ai_openai_allowed_models'] ?? null,
|
|
33
|
+
anthropicAllowedModels: settings['ai_anthropic_allowed_models'] ?? null,
|
|
34
|
+
googleAllowedModels: settings['ai_google_allowed_models'] ?? null,
|
|
35
|
+
systemPrompt: settings['ai_system_prompt'] ?? null,
|
|
36
|
+
};
|
|
10
37
|
res.locals['ai'] = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
anthropic: ai_anthropic_api_key,
|
|
14
|
-
},
|
|
15
|
-
systemPrompt: ai_system_prompt,
|
|
38
|
+
settings: aiSettings,
|
|
39
|
+
systemPrompt: settings['ai_system_prompt'],
|
|
16
40
|
};
|
|
17
41
|
return next();
|
|
18
42
|
};
|
|
@@ -12,12 +12,103 @@ export declare const ToolApprovalMode: z.ZodEnum<{
|
|
|
12
12
|
disabled: "disabled";
|
|
13
13
|
}>;
|
|
14
14
|
export type ToolApprovalMode = z.infer<typeof ToolApprovalMode>;
|
|
15
|
+
export declare const ContextAttachment: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
16
|
+
type: z.ZodLiteral<"item">;
|
|
17
|
+
display: z.ZodString;
|
|
18
|
+
data: z.ZodObject<{
|
|
19
|
+
collection: z.ZodString;
|
|
20
|
+
key: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
23
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
24
|
+
type: z.ZodLiteral<"visual-element">;
|
|
25
|
+
display: z.ZodString;
|
|
26
|
+
data: z.ZodObject<{
|
|
27
|
+
key: z.ZodString;
|
|
28
|
+
collection: z.ZodString;
|
|
29
|
+
item: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
30
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
rect: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
top: z.ZodNumber;
|
|
33
|
+
left: z.ZodNumber;
|
|
34
|
+
width: z.ZodNumber;
|
|
35
|
+
height: z.ZodNumber;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
39
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
40
|
+
type: z.ZodLiteral<"prompt">;
|
|
41
|
+
display: z.ZodString;
|
|
42
|
+
data: z.ZodObject<{
|
|
43
|
+
text: z.ZodString;
|
|
44
|
+
prompt: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
45
|
+
values: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
48
|
+
}, z.core.$strip>], "type">;
|
|
49
|
+
export type ContextAttachment = z.infer<typeof ContextAttachment>;
|
|
50
|
+
export declare const PageContext: z.ZodObject<{
|
|
51
|
+
path: z.ZodString;
|
|
52
|
+
collection: z.ZodOptional<z.ZodString>;
|
|
53
|
+
item: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
54
|
+
module: z.ZodOptional<z.ZodString>;
|
|
55
|
+
}, z.core.$strip>;
|
|
56
|
+
export type PageContext = z.infer<typeof PageContext>;
|
|
57
|
+
export declare const ChatContext: z.ZodObject<{
|
|
58
|
+
attachments: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
59
|
+
type: z.ZodLiteral<"item">;
|
|
60
|
+
display: z.ZodString;
|
|
61
|
+
data: z.ZodObject<{
|
|
62
|
+
collection: z.ZodString;
|
|
63
|
+
key: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
66
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
67
|
+
type: z.ZodLiteral<"visual-element">;
|
|
68
|
+
display: z.ZodString;
|
|
69
|
+
data: z.ZodObject<{
|
|
70
|
+
key: z.ZodString;
|
|
71
|
+
collection: z.ZodString;
|
|
72
|
+
item: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
73
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
74
|
+
rect: z.ZodOptional<z.ZodObject<{
|
|
75
|
+
top: z.ZodNumber;
|
|
76
|
+
left: z.ZodNumber;
|
|
77
|
+
width: z.ZodNumber;
|
|
78
|
+
height: z.ZodNumber;
|
|
79
|
+
}, z.core.$strip>>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
82
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
83
|
+
type: z.ZodLiteral<"prompt">;
|
|
84
|
+
display: z.ZodString;
|
|
85
|
+
data: z.ZodObject<{
|
|
86
|
+
text: z.ZodString;
|
|
87
|
+
prompt: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
88
|
+
values: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
91
|
+
}, z.core.$strip>], "type">>>;
|
|
92
|
+
page: z.ZodOptional<z.ZodObject<{
|
|
93
|
+
path: z.ZodString;
|
|
94
|
+
collection: z.ZodOptional<z.ZodString>;
|
|
95
|
+
item: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
96
|
+
module: z.ZodOptional<z.ZodString>;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
export type ChatContext = z.infer<typeof ChatContext>;
|
|
15
100
|
export declare const ChatRequest: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
16
101
|
provider: z.ZodLiteral<"openai">;
|
|
17
|
-
model: z.
|
|
102
|
+
model: z.ZodString;
|
|
18
103
|
}, z.core.$strip>, z.ZodObject<{
|
|
19
104
|
provider: z.ZodLiteral<"anthropic">;
|
|
20
|
-
model: z.
|
|
105
|
+
model: z.ZodString;
|
|
106
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
107
|
+
provider: z.ZodLiteral<"google">;
|
|
108
|
+
model: z.ZodString;
|
|
109
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
110
|
+
provider: z.ZodLiteral<"openai-compatible">;
|
|
111
|
+
model: z.ZodString;
|
|
21
112
|
}, z.core.$strip>], "provider">, z.ZodObject<{
|
|
22
113
|
tools: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
23
114
|
name: z.ZodString;
|
|
@@ -30,5 +121,47 @@ export declare const ChatRequest: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.Z
|
|
|
30
121
|
ask: "ask";
|
|
31
122
|
disabled: "disabled";
|
|
32
123
|
}>>>;
|
|
124
|
+
context: z.ZodOptional<z.ZodObject<{
|
|
125
|
+
attachments: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
126
|
+
type: z.ZodLiteral<"item">;
|
|
127
|
+
display: z.ZodString;
|
|
128
|
+
data: z.ZodObject<{
|
|
129
|
+
collection: z.ZodString;
|
|
130
|
+
key: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
131
|
+
}, z.core.$strip>;
|
|
132
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
133
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
134
|
+
type: z.ZodLiteral<"visual-element">;
|
|
135
|
+
display: z.ZodString;
|
|
136
|
+
data: z.ZodObject<{
|
|
137
|
+
key: z.ZodString;
|
|
138
|
+
collection: z.ZodString;
|
|
139
|
+
item: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
140
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
141
|
+
rect: z.ZodOptional<z.ZodObject<{
|
|
142
|
+
top: z.ZodNumber;
|
|
143
|
+
left: z.ZodNumber;
|
|
144
|
+
width: z.ZodNumber;
|
|
145
|
+
height: z.ZodNumber;
|
|
146
|
+
}, z.core.$strip>>;
|
|
147
|
+
}, z.core.$strip>;
|
|
148
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
149
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
150
|
+
type: z.ZodLiteral<"prompt">;
|
|
151
|
+
display: z.ZodString;
|
|
152
|
+
data: z.ZodObject<{
|
|
153
|
+
text: z.ZodString;
|
|
154
|
+
prompt: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
155
|
+
values: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
158
|
+
}, z.core.$strip>], "type">>>;
|
|
159
|
+
page: z.ZodOptional<z.ZodObject<{
|
|
160
|
+
path: z.ZodString;
|
|
161
|
+
collection: z.ZodOptional<z.ZodString>;
|
|
162
|
+
item: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
163
|
+
module: z.ZodOptional<z.ZodString>;
|
|
164
|
+
}, z.core.$strip>>;
|
|
165
|
+
}, z.core.$strip>>;
|
|
33
166
|
}, z.core.$strip>>;
|
|
34
167
|
export type ChatRequest = z.infer<typeof ChatRequest>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { parseJsonSchema7 } from '../utils/parse-json-schema-7.js';
|
|
4
|
-
import { ProviderAnthropic, ProviderOpenAi } from './providers.js';
|
|
4
|
+
import { ProviderAnthropic, ProviderGoogle, ProviderOpenAi, ProviderOpenAiCompatible } from './providers.js';
|
|
5
5
|
export const ChatRequestTool = z.union([
|
|
6
6
|
z.string(),
|
|
7
7
|
z.object({
|
|
@@ -19,8 +19,62 @@ export const ChatRequestTool = z.union([
|
|
|
19
19
|
}),
|
|
20
20
|
]);
|
|
21
21
|
export const ToolApprovalMode = z.enum(['always', 'ask', 'disabled']);
|
|
22
|
-
|
|
22
|
+
const ItemContextData = z.object({
|
|
23
|
+
collection: z.string(),
|
|
24
|
+
key: z.union([z.string(), z.number()]),
|
|
25
|
+
});
|
|
26
|
+
const VisualElementContextData = z.object({
|
|
27
|
+
key: z.string(),
|
|
28
|
+
collection: z.string(),
|
|
29
|
+
item: z.union([z.string(), z.number()]),
|
|
30
|
+
fields: z.array(z.string()).optional(),
|
|
31
|
+
rect: z
|
|
32
|
+
.object({
|
|
33
|
+
top: z.number(),
|
|
34
|
+
left: z.number(),
|
|
35
|
+
width: z.number(),
|
|
36
|
+
height: z.number(),
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
39
|
+
});
|
|
40
|
+
const PromptContextData = z.object({
|
|
41
|
+
text: z.string(),
|
|
42
|
+
prompt: z.record(z.string(), z.unknown()),
|
|
43
|
+
values: z.record(z.string(), z.string()),
|
|
44
|
+
});
|
|
45
|
+
export const ContextAttachment = z.discriminatedUnion('type', [
|
|
46
|
+
z.object({
|
|
47
|
+
type: z.literal('item'),
|
|
48
|
+
display: z.string(),
|
|
49
|
+
data: ItemContextData,
|
|
50
|
+
snapshot: z.record(z.string(), z.unknown()),
|
|
51
|
+
}),
|
|
52
|
+
z.object({
|
|
53
|
+
type: z.literal('visual-element'),
|
|
54
|
+
display: z.string(),
|
|
55
|
+
data: VisualElementContextData,
|
|
56
|
+
snapshot: z.record(z.string(), z.unknown()),
|
|
57
|
+
}),
|
|
58
|
+
z.object({
|
|
59
|
+
type: z.literal('prompt'),
|
|
60
|
+
display: z.string(),
|
|
61
|
+
data: PromptContextData,
|
|
62
|
+
snapshot: z.record(z.string(), z.unknown()),
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
export const PageContext = z.object({
|
|
66
|
+
path: z.string(),
|
|
67
|
+
collection: z.string().optional(),
|
|
68
|
+
item: z.union([z.string(), z.number()]).optional(),
|
|
69
|
+
module: z.string().optional(),
|
|
70
|
+
});
|
|
71
|
+
export const ChatContext = z.object({
|
|
72
|
+
attachments: z.array(ContextAttachment).max(10).optional(),
|
|
73
|
+
page: PageContext.optional(),
|
|
74
|
+
});
|
|
75
|
+
export const ChatRequest = z.intersection(z.discriminatedUnion('provider', [ProviderOpenAi, ProviderAnthropic, ProviderGoogle, ProviderOpenAiCompatible]), z.object({
|
|
23
76
|
tools: z.array(ChatRequestTool),
|
|
24
77
|
messages: z.array(z.looseObject({})),
|
|
25
78
|
toolApprovals: z.record(z.string(), ToolApprovalMode).optional(),
|
|
79
|
+
context: ChatContext.optional(),
|
|
26
80
|
}));
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const ProviderTypeSchema: z.ZodEnum<{
|
|
3
|
+
openai: "openai";
|
|
4
|
+
anthropic: "anthropic";
|
|
5
|
+
google: "google";
|
|
6
|
+
"openai-compatible": "openai-compatible";
|
|
7
|
+
}>;
|
|
2
8
|
export declare const ProviderOpenAi: z.ZodObject<{
|
|
3
9
|
provider: z.ZodLiteral<"openai">;
|
|
4
|
-
model: z.
|
|
10
|
+
model: z.ZodString;
|
|
5
11
|
}, z.core.$strip>;
|
|
6
12
|
export declare const ProviderAnthropic: z.ZodObject<{
|
|
7
13
|
provider: z.ZodLiteral<"anthropic">;
|
|
8
|
-
model: z.
|
|
14
|
+
model: z.ZodString;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export declare const ProviderGoogle: z.ZodObject<{
|
|
17
|
+
provider: z.ZodLiteral<"google">;
|
|
18
|
+
model: z.ZodString;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export declare const ProviderOpenAiCompatible: z.ZodObject<{
|
|
21
|
+
provider: z.ZodLiteral<"openai-compatible">;
|
|
22
|
+
model: z.ZodString;
|
|
9
23
|
}, z.core.$strip>;
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export const ProviderTypeSchema = z.enum([
|
|
3
|
+
'openai',
|
|
4
|
+
'anthropic',
|
|
5
|
+
'google',
|
|
6
|
+
'openai-compatible',
|
|
7
|
+
]);
|
|
2
8
|
export const ProviderOpenAi = z.object({
|
|
3
9
|
provider: z.literal('openai'),
|
|
4
|
-
model: z.
|
|
10
|
+
model: z.string(),
|
|
5
11
|
});
|
|
6
12
|
export const ProviderAnthropic = z.object({
|
|
7
13
|
provider: z.literal('anthropic'),
|
|
8
|
-
model: z.
|
|
14
|
+
model: z.string(),
|
|
15
|
+
});
|
|
16
|
+
export const ProviderGoogle = z.object({
|
|
17
|
+
provider: z.literal('google'),
|
|
18
|
+
model: z.string(),
|
|
19
|
+
});
|
|
20
|
+
export const ProviderOpenAiCompatible = z.object({
|
|
21
|
+
provider: z.literal('openai-compatible'),
|
|
22
|
+
model: z.string(),
|
|
9
23
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvalidPayloadError } from '@directus/errors';
|
|
2
2
|
import {} from '@directus/types';
|
|
3
|
-
import { jsonSchema, tool } from 'ai';
|
|
3
|
+
import { jsonSchema, tool, zodSchema } from 'ai';
|
|
4
4
|
import { fromZodError } from 'zod-validation-error';
|
|
5
5
|
import { ALL_TOOLS } from '../../tools/index.js';
|
|
6
6
|
export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, schema, toolApprovals, }) => {
|
|
@@ -13,10 +13,10 @@ export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, sc
|
|
|
13
13
|
// Default to 'ask' (needs approval) if not specified
|
|
14
14
|
const approvalMode = toolApprovals?.[chatRequestTool] ?? 'ask';
|
|
15
15
|
const needsApproval = approvalMode !== 'always';
|
|
16
|
+
const inputSchema = zodSchema(directusTool.inputSchema);
|
|
16
17
|
return tool({
|
|
17
|
-
name: directusTool.name,
|
|
18
18
|
description: directusTool.description,
|
|
19
|
-
inputSchema
|
|
19
|
+
inputSchema,
|
|
20
20
|
needsApproval,
|
|
21
21
|
execute: async (rawArgs) => {
|
|
22
22
|
const { error, data: args } = directusTool.validateSchema?.safeParse(rawArgs) ?? {
|
|
@@ -31,7 +31,6 @@ export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, sc
|
|
|
31
31
|
}
|
|
32
32
|
// Local/client-side tool (schema only, executed on client)
|
|
33
33
|
return tool({
|
|
34
|
-
name: chatRequestTool.name,
|
|
35
34
|
description: chatRequestTool.description,
|
|
36
35
|
inputSchema: jsonSchema(chatRequestTool.inputSchema),
|
|
37
36
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function escapeAngleBrackets(text) {
|
|
2
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
3
|
+
}
|
|
4
|
+
function groupAttachments(attachments) {
|
|
5
|
+
const groups = { visualElements: [], prompts: [], items: [] };
|
|
6
|
+
for (const att of attachments) {
|
|
7
|
+
switch (att.type) {
|
|
8
|
+
case 'visual-element':
|
|
9
|
+
groups.visualElements.push(att);
|
|
10
|
+
break;
|
|
11
|
+
case 'prompt':
|
|
12
|
+
groups.prompts.push(att);
|
|
13
|
+
break;
|
|
14
|
+
case 'item':
|
|
15
|
+
groups.items.push(att);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return groups;
|
|
20
|
+
}
|
|
21
|
+
function formatVisualElement(att) {
|
|
22
|
+
const fields = att.data.fields?.length ? att.data.fields.map((f) => escapeAngleBrackets(f)).join(', ') : 'all';
|
|
23
|
+
const collection = escapeAngleBrackets(String(att.data.collection));
|
|
24
|
+
const item = escapeAngleBrackets(String(att.data.item));
|
|
25
|
+
const display = escapeAngleBrackets(att.display);
|
|
26
|
+
return `### ${collection}/${item} — "${display}"
|
|
27
|
+
Editable fields: ${fields}
|
|
28
|
+
\`\`\`json
|
|
29
|
+
${escapeAngleBrackets(JSON.stringify(att.snapshot, null, 2))}
|
|
30
|
+
\`\`\``;
|
|
31
|
+
}
|
|
32
|
+
function formatPrompt(att) {
|
|
33
|
+
const snapshot = att.snapshot;
|
|
34
|
+
const lines = [];
|
|
35
|
+
const display = escapeAngleBrackets(att.display);
|
|
36
|
+
if (snapshot.text) {
|
|
37
|
+
lines.push(escapeAngleBrackets(snapshot.text));
|
|
38
|
+
}
|
|
39
|
+
if (snapshot.messages?.length) {
|
|
40
|
+
lines.push('\n### Example Exchange');
|
|
41
|
+
for (const msg of snapshot.messages) {
|
|
42
|
+
const role = escapeAngleBrackets(msg.role);
|
|
43
|
+
const text = escapeAngleBrackets(msg.text);
|
|
44
|
+
lines.push(`**${role}**: ${text}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `### ${display}\n${lines.join('\n')}`;
|
|
48
|
+
}
|
|
49
|
+
function formatItem(att) {
|
|
50
|
+
const display = escapeAngleBrackets(att.display);
|
|
51
|
+
const collectionLabel = att.data.collection ? ` (${escapeAngleBrackets(att.data.collection)})` : '';
|
|
52
|
+
const keyLabel = ` — key: ${escapeAngleBrackets(String(att.data.key))}`;
|
|
53
|
+
const collection = att.data.collection ? escapeAngleBrackets(att.data.collection) : '';
|
|
54
|
+
const updateHint = att.data.collection
|
|
55
|
+
? `\nTo update this item, use the items tool with: collection="${collection}", keys=["${escapeAngleBrackets(String(att.data.key))}"], action="update"`
|
|
56
|
+
: '\nUse the items tool to update this item.';
|
|
57
|
+
return `[Item: ${display}${collectionLabel}${keyLabel}]${updateHint}\n${escapeAngleBrackets(JSON.stringify(att.snapshot, null, 2))}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Format context for appending to system prompt
|
|
61
|
+
*/
|
|
62
|
+
export function formatContextForSystemPrompt(context) {
|
|
63
|
+
const attachments = context.attachments ?? [];
|
|
64
|
+
const groups = groupAttachments(attachments);
|
|
65
|
+
const parts = [];
|
|
66
|
+
// 1. Custom instructions (prompts) - highest priority, placed first
|
|
67
|
+
if (groups.prompts.length > 0) {
|
|
68
|
+
const promptBlocks = groups.prompts.map(formatPrompt).join('\n\n');
|
|
69
|
+
parts.push(`<custom_instructions>
|
|
70
|
+
The user has applied the following prompt(s) to guide your behavior:
|
|
71
|
+
|
|
72
|
+
${promptBlocks}
|
|
73
|
+
</custom_instructions>`);
|
|
74
|
+
}
|
|
75
|
+
// 2. User context (current page + items)
|
|
76
|
+
const sections = [];
|
|
77
|
+
const now = new Date();
|
|
78
|
+
sections.push(`## Current Date\n${now.toISOString().split('T')[0]}`);
|
|
79
|
+
if (context.page) {
|
|
80
|
+
const page = context.page;
|
|
81
|
+
const pageLines = [`Path: ${escapeAngleBrackets(String(page.path))}`];
|
|
82
|
+
if (page.collection)
|
|
83
|
+
pageLines.push(`Collection: ${escapeAngleBrackets(String(page.collection))}`);
|
|
84
|
+
if (page.item !== undefined)
|
|
85
|
+
pageLines.push(`Item: ${escapeAngleBrackets(String(page.item))}`);
|
|
86
|
+
if (page.module)
|
|
87
|
+
pageLines.push(`Module: ${escapeAngleBrackets(String(page.module))}`);
|
|
88
|
+
sections.push(`## Current Page\n${pageLines.join('\n')}`);
|
|
89
|
+
}
|
|
90
|
+
if (groups.items.length > 0) {
|
|
91
|
+
const itemLines = groups.items.map(formatItem).join('\n\n');
|
|
92
|
+
sections.push(`## User-Added Context
|
|
93
|
+
The user has attached these items as reference for their request.
|
|
94
|
+
All root-level fields the user has access to are shown below — use these exact field names when updating.
|
|
95
|
+
Use the items tool to fetch additional fields or update items when asked.
|
|
96
|
+
|
|
97
|
+
${itemLines}`);
|
|
98
|
+
}
|
|
99
|
+
if (sections.length > 0) {
|
|
100
|
+
parts.push(`<user_context>\n${sections.join('\n\n')}\n</user_context>`);
|
|
101
|
+
}
|
|
102
|
+
// 3. Visual editing context
|
|
103
|
+
if (groups.visualElements.length > 0) {
|
|
104
|
+
const elementLines = groups.visualElements.map(formatVisualElement).join('\n\n');
|
|
105
|
+
parts.push(`<visual_editing>
|
|
106
|
+
## Selected Elements
|
|
107
|
+
The user selected these elements for editing in the visual editor.
|
|
108
|
+
|
|
109
|
+
${elementLines}
|
|
110
|
+
</visual_editing>`);
|
|
111
|
+
}
|
|
112
|
+
// 4. Attachment rules (only if any attachments exist)
|
|
113
|
+
if (attachments.length > 0) {
|
|
114
|
+
parts.push(`## Attachment Rules
|
|
115
|
+
- User-added attachments have HIGHER PRIORITY than page context.
|
|
116
|
+
- To modify attached items, ALWAYS use the items tool with action: 'update'. NEVER use form-values tools for attached items.
|
|
117
|
+
- form-values tools ONLY affect the currently open page form, which may be a DIFFERENT item than what the user attached.`);
|
|
118
|
+
}
|
|
119
|
+
if (parts.length === 0)
|
|
120
|
+
return '';
|
|
121
|
+
return '\n\n' + parts.join('\n\n');
|
|
122
|
+
}
|