@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.20
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/README.md +67 -6
- package/docs/guides/add-channel.md +118 -7
- package/docs/guides/add-knowledge.md +144 -0
- package/docs/guides/add-managed-composio.md +163 -0
- package/docs/guides/add-tool.md +1 -1
- package/docs/guides/channel-security.md +97 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +78 -1
- package/docs/guides/connect-whatsapp-zapster.md +112 -8
- package/docs/guides/create-agent.md +45 -4
- package/docs/guides/debug-channel.md +147 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +47 -17
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +147 -20
- package/docs/guides/security-rules.md +7 -6
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-provider.md +27 -3
- package/docs/llms-full.txt +303 -55
- package/docs/llms.txt +57 -7
- package/package.json +2 -5
- package/src/cli/args.ts +57 -0
- package/src/cli/cloud-client.ts +377 -0
- package/src/cli/commands/channels.ts +1315 -0
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/knowledge.ts +136 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/constants.ts +4 -0
- package/src/cli/deploy-chat-ui.ts +535 -0
- package/src/cli/deploy-readiness.ts +481 -0
- package/src/cli/flags.ts +162 -0
- package/src/cli/help.ts +236 -0
- package/src/cli/index.ts +1167 -1005
- package/src/cli/process.ts +31 -0
- package/src/cloud/artifact.ts +139 -0
- package/src/cloud/client.ts +80 -0
- package/src/cloud/contracts.ts +63 -0
- package/src/cloud/index.ts +3 -0
- package/src/create-project.ts +21 -6
- package/src/index.ts +479 -7
- package/src/providers/pi.ts +70 -16
- package/src/providers/test.ts +88 -1
- package/src/providers/types.ts +7 -0
- package/src/runtime/channel-buffer.ts +30 -0
- package/src/runtime/channel-test-harness.ts +8 -1
- package/src/runtime/channels/discord.ts +896 -0
- package/src/runtime/channels/slack.ts +646 -0
- package/src/runtime/channels/telegram.ts +466 -23
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-zapster.ts +677 -40
- package/src/runtime/channels.ts +86 -3
- package/src/runtime/chat.ts +130 -38
- package/src/runtime/config.ts +483 -18
- package/src/runtime/core/manifest.ts +103 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/database.ts +93 -2
- package/src/runtime/db-commands.ts +9 -0
- package/src/runtime/deploy-readiness.ts +46 -4
- package/src/runtime/deploy.ts +1 -1
- package/src/runtime/dev-server.ts +759 -41
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +589 -43
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +194 -4
- package/src/runtime/integrations/composio.ts +423 -0
- package/src/runtime/knowledge/chunk.ts +333 -0
- package/src/runtime/knowledge/config.ts +135 -0
- package/src/runtime/knowledge/embeddings.ts +133 -0
- package/src/runtime/knowledge/ingest.ts +521 -0
- package/src/runtime/knowledge/prompt-policy.ts +30 -0
- package/src/runtime/knowledge/retrieve.ts +303 -0
- package/src/runtime/knowledge/schema.ts +100 -0
- package/src/runtime/knowledge/tool.ts +64 -0
- package/src/runtime/knowledge/vector.ts +258 -0
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +86 -8
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/spec.ts +152 -0
- package/src/runtime/sync.ts +144 -0
- package/src/runtime/targets/cloudflare/build.ts +1430 -185
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +128 -2
- package/src/runtime/traces.ts +41 -0
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +149 -3
- package/src/templates/blank.ts +76 -17
- package/src/templates/dentista.ts +1011 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
- package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
- package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
- package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
- package/src/templates/skills/agentkit-channels/SKILL.md +104 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
- package/src/templates/skills/agentkit-database/SKILL.md +45 -0
- package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
- package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
- package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
- package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
- package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
- package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
- package/src/templates/skills/agentkit-security/SKILL.md +56 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
- package/src/templates/support.ts +77 -18
- package/docs/guides/channels-production-handoff.md +0 -99
- package/docs/portable-deploy-release-checklist.md +0 -41
- package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import type { AgentTranscriptionConfig, TranscriptionProviderName } from "../index";
|
|
2
|
+
|
|
3
|
+
export type ResolvedTranscriptionConfig = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
provider: TranscriptionProviderName;
|
|
6
|
+
model: string;
|
|
7
|
+
secret: string | null;
|
|
8
|
+
language: string | null;
|
|
9
|
+
prompt: string | null;
|
|
10
|
+
limits: {
|
|
11
|
+
maxDurationSeconds?: number;
|
|
12
|
+
maxBytes?: number;
|
|
13
|
+
};
|
|
14
|
+
rawAudioTtlSeconds: number | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type TranscriptionInput = {
|
|
18
|
+
config: ResolvedTranscriptionConfig;
|
|
19
|
+
secrets: Record<string, string>;
|
|
20
|
+
audio: Uint8Array;
|
|
21
|
+
filename: string;
|
|
22
|
+
mimeType?: string;
|
|
23
|
+
durationSeconds?: number;
|
|
24
|
+
providerMetadata?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type TranscriptionFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
28
|
+
|
|
29
|
+
export type TranscriptionResult =
|
|
30
|
+
| {
|
|
31
|
+
ok: true;
|
|
32
|
+
text: string;
|
|
33
|
+
provider: TranscriptionProviderName;
|
|
34
|
+
model: string;
|
|
35
|
+
language?: string;
|
|
36
|
+
durationSeconds?: number;
|
|
37
|
+
providerMetadata?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
ok: false;
|
|
41
|
+
retryable: boolean;
|
|
42
|
+
code:
|
|
43
|
+
| "transcription_disabled"
|
|
44
|
+
| "transcription_secret_missing"
|
|
45
|
+
| "transcription_model_unsupported"
|
|
46
|
+
| "transcription_audio_too_large"
|
|
47
|
+
| "transcription_audio_too_long"
|
|
48
|
+
| "transcription_audio_format_unsupported"
|
|
49
|
+
| "transcription_provider_unavailable"
|
|
50
|
+
| "transcription_failed";
|
|
51
|
+
message: string;
|
|
52
|
+
provider?: TranscriptionProviderName;
|
|
53
|
+
model?: string;
|
|
54
|
+
providerMetadata?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type TranscriptionAdapter = {
|
|
58
|
+
provider: TranscriptionProviderName;
|
|
59
|
+
models: string[];
|
|
60
|
+
supportedMimeTypes: string[];
|
|
61
|
+
supportedExtensions: string[];
|
|
62
|
+
maxBytes: number;
|
|
63
|
+
requiredSecret(config: ResolvedTranscriptionConfig): string | null;
|
|
64
|
+
transcribe(input: TranscriptionInput, fetcher?: TranscriptionFetch): Promise<TranscriptionResult>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const DEFAULT_MAX_BYTES = 25_000_000;
|
|
68
|
+
const OPENAI_MODELS = ["gpt-4o-mini-transcribe", "gpt-4o-transcribe", "whisper-1"];
|
|
69
|
+
const GROQ_MODELS = ["whisper-large-v3-turbo", "whisper-large-v3", "distil-whisper-large-v3-en"];
|
|
70
|
+
const OPENAI_EXTENSIONS = ["mp3", "mp4", "mpeg", "mpga", "m4a", "wav", "webm"];
|
|
71
|
+
const GROQ_EXTENSIONS = ["flac", "mp3", "mp4", "mpeg", "mpga", "m4a", "ogg", "wav", "webm"];
|
|
72
|
+
const OPENAI_MIME_TYPES = [
|
|
73
|
+
"audio/mpeg",
|
|
74
|
+
"audio/mp3",
|
|
75
|
+
"audio/mp4",
|
|
76
|
+
"audio/mpga",
|
|
77
|
+
"audio/m4a",
|
|
78
|
+
"audio/wav",
|
|
79
|
+
"audio/webm",
|
|
80
|
+
"video/mp4",
|
|
81
|
+
];
|
|
82
|
+
const GROQ_MIME_TYPES = [
|
|
83
|
+
...OPENAI_MIME_TYPES,
|
|
84
|
+
"audio/flac",
|
|
85
|
+
"audio/ogg",
|
|
86
|
+
"audio/opus",
|
|
87
|
+
"application/ogg",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export function resolveTranscriptionConfig(config: AgentTranscriptionConfig | undefined): ResolvedTranscriptionConfig {
|
|
91
|
+
if (!config) {
|
|
92
|
+
return {
|
|
93
|
+
enabled: false,
|
|
94
|
+
provider: "test",
|
|
95
|
+
model: "fake",
|
|
96
|
+
secret: null,
|
|
97
|
+
language: null,
|
|
98
|
+
prompt: null,
|
|
99
|
+
limits: {},
|
|
100
|
+
rawAudioTtlSeconds: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
enabled: true,
|
|
106
|
+
provider: config.provider,
|
|
107
|
+
model: config.model,
|
|
108
|
+
secret: config.secret ?? defaultTranscriptionSecret(config.provider),
|
|
109
|
+
language: config.language ?? null,
|
|
110
|
+
prompt: config.prompt ?? null,
|
|
111
|
+
limits: {
|
|
112
|
+
...(config.limits?.maxDurationSeconds !== undefined
|
|
113
|
+
? { maxDurationSeconds: config.limits.maxDurationSeconds }
|
|
114
|
+
: {}),
|
|
115
|
+
...(config.limits?.maxBytes !== undefined ? { maxBytes: config.limits.maxBytes } : {}),
|
|
116
|
+
},
|
|
117
|
+
rawAudioTtlSeconds: config.rawAudioTtlSeconds ?? 3600,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function transcribeAudio(
|
|
122
|
+
input: TranscriptionInput,
|
|
123
|
+
fetcher: TranscriptionFetch = fetch,
|
|
124
|
+
): Promise<TranscriptionResult> {
|
|
125
|
+
if (!input.config.enabled) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
retryable: false,
|
|
129
|
+
code: "transcription_disabled",
|
|
130
|
+
message: "Audio transcription is not configured for this Agent Capsule.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const adapter = transcriptionAdapterFor(input.config.provider);
|
|
135
|
+
|
|
136
|
+
if (!adapter) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
retryable: false,
|
|
140
|
+
code: "transcription_model_unsupported",
|
|
141
|
+
message: `No transcription adapter is registered for ${input.config.provider}.`,
|
|
142
|
+
provider: input.config.provider,
|
|
143
|
+
model: input.config.model,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const validation = validateTranscriptionInput(adapter, input);
|
|
148
|
+
|
|
149
|
+
if (!validation.ok) {
|
|
150
|
+
return validation;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return adapter.transcribe(input, fetcher);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function transcriptionAdapterFor(provider: TranscriptionProviderName): TranscriptionAdapter | null {
|
|
157
|
+
if (provider === "test") {
|
|
158
|
+
return testTranscriptionAdapter;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (provider === "openai") {
|
|
162
|
+
return openaiTranscriptionAdapter;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (provider === "groq") {
|
|
166
|
+
return groqTranscriptionAdapter;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function defaultTranscriptionSecret(provider: TranscriptionProviderName): string | null {
|
|
173
|
+
if (provider === "openai") {
|
|
174
|
+
return "OPENAI_API_KEY";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (provider === "groq") {
|
|
178
|
+
return "GROQ_API_KEY";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateTranscriptionInput(adapter: TranscriptionAdapter, input: TranscriptionInput): TranscriptionResult {
|
|
185
|
+
if (!adapter.models.includes(input.config.model)) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
retryable: false,
|
|
189
|
+
code: "transcription_model_unsupported",
|
|
190
|
+
message: `${input.config.provider} transcription model ${input.config.model} is not supported by AgentKit.`,
|
|
191
|
+
provider: input.config.provider,
|
|
192
|
+
model: input.config.model,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const maxBytes = Math.min(input.config.limits.maxBytes ?? adapter.maxBytes, adapter.maxBytes);
|
|
197
|
+
if (input.audio.byteLength > maxBytes) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
retryable: false,
|
|
201
|
+
code: "transcription_audio_too_large",
|
|
202
|
+
message: `Audio file is ${input.audio.byteLength} bytes, which exceeds the configured ${maxBytes} byte limit.`,
|
|
203
|
+
provider: input.config.provider,
|
|
204
|
+
model: input.config.model,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
input.durationSeconds !== undefined &&
|
|
210
|
+
input.config.limits.maxDurationSeconds !== undefined &&
|
|
211
|
+
input.durationSeconds > input.config.limits.maxDurationSeconds
|
|
212
|
+
) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
retryable: false,
|
|
216
|
+
code: "transcription_audio_too_long",
|
|
217
|
+
message: `Audio is ${input.durationSeconds}s, which exceeds the configured ${input.config.limits.maxDurationSeconds}s limit.`,
|
|
218
|
+
provider: input.config.provider,
|
|
219
|
+
model: input.config.model,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!isSupportedAudioFormat(adapter, input.filename, input.mimeType)) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
retryable: false,
|
|
227
|
+
code: "transcription_audio_format_unsupported",
|
|
228
|
+
message: `${input.config.provider} does not support audio format ${input.mimeType ?? extensionFor(input.filename) ?? "unknown"}.`,
|
|
229
|
+
provider: input.config.provider,
|
|
230
|
+
model: input.config.model,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const secret = adapter.requiredSecret(input.config);
|
|
235
|
+
if (secret && !input.secrets[secret]) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
retryable: false,
|
|
239
|
+
code: "transcription_secret_missing",
|
|
240
|
+
message: `Secret ${secret} is not set for audio transcription.`,
|
|
241
|
+
provider: input.config.provider,
|
|
242
|
+
model: input.config.model,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
text: "",
|
|
249
|
+
provider: input.config.provider,
|
|
250
|
+
model: input.config.model,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const testTranscriptionAdapter: TranscriptionAdapter = {
|
|
255
|
+
provider: "test",
|
|
256
|
+
models: ["fake"],
|
|
257
|
+
supportedMimeTypes: ["audio/wav", "audio/ogg", "audio/mpeg", "audio/webm", "application/octet-stream"],
|
|
258
|
+
supportedExtensions: ["wav", "ogg", "mp3", "webm", "bin"],
|
|
259
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
260
|
+
requiredSecret() {
|
|
261
|
+
return null;
|
|
262
|
+
},
|
|
263
|
+
async transcribe(input) {
|
|
264
|
+
const text = decodeFixtureTranscript(input.audio) ?? "fake audio transcript";
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
text,
|
|
269
|
+
provider: "test",
|
|
270
|
+
model: input.config.model,
|
|
271
|
+
...(input.config.language ? { language: input.config.language } : {}),
|
|
272
|
+
...(input.durationSeconds !== undefined ? { durationSeconds: input.durationSeconds } : {}),
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const openaiTranscriptionAdapter: TranscriptionAdapter = {
|
|
278
|
+
provider: "openai",
|
|
279
|
+
models: OPENAI_MODELS,
|
|
280
|
+
supportedMimeTypes: OPENAI_MIME_TYPES,
|
|
281
|
+
supportedExtensions: OPENAI_EXTENSIONS,
|
|
282
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
283
|
+
requiredSecret(config) {
|
|
284
|
+
return config.secret;
|
|
285
|
+
},
|
|
286
|
+
async transcribe(input, fetcher = fetch) {
|
|
287
|
+
return transcribeViaOpenAiCompatibleEndpoint({
|
|
288
|
+
input,
|
|
289
|
+
fetcher,
|
|
290
|
+
url: "https://api.openai.com/v1/audio/transcriptions",
|
|
291
|
+
apiKey: input.secrets[input.config.secret ?? ""],
|
|
292
|
+
provider: "openai",
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const groqTranscriptionAdapter: TranscriptionAdapter = {
|
|
298
|
+
provider: "groq",
|
|
299
|
+
models: GROQ_MODELS,
|
|
300
|
+
supportedMimeTypes: GROQ_MIME_TYPES,
|
|
301
|
+
supportedExtensions: GROQ_EXTENSIONS,
|
|
302
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
303
|
+
requiredSecret(config) {
|
|
304
|
+
return config.secret;
|
|
305
|
+
},
|
|
306
|
+
async transcribe(input, fetcher = fetch) {
|
|
307
|
+
return transcribeViaOpenAiCompatibleEndpoint({
|
|
308
|
+
input,
|
|
309
|
+
fetcher,
|
|
310
|
+
url: "https://api.groq.com/openai/v1/audio/transcriptions",
|
|
311
|
+
apiKey: input.secrets[input.config.secret ?? ""],
|
|
312
|
+
provider: "groq",
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
async function transcribeViaOpenAiCompatibleEndpoint(input: {
|
|
318
|
+
input: TranscriptionInput;
|
|
319
|
+
fetcher: TranscriptionFetch;
|
|
320
|
+
url: string;
|
|
321
|
+
apiKey: string | undefined;
|
|
322
|
+
provider: TranscriptionProviderName;
|
|
323
|
+
}): Promise<TranscriptionResult> {
|
|
324
|
+
if (!input.apiKey) {
|
|
325
|
+
const secret = input.input.config.secret ?? defaultTranscriptionSecret(input.provider) ?? "TRANSCRIPTION_API_KEY";
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
retryable: false,
|
|
330
|
+
code: "transcription_secret_missing",
|
|
331
|
+
message: `Secret ${secret} is not set for audio transcription.`,
|
|
332
|
+
provider: input.provider,
|
|
333
|
+
model: input.input.config.model,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const form = new FormData();
|
|
338
|
+
form.set("model", input.input.config.model);
|
|
339
|
+
form.set(
|
|
340
|
+
"file",
|
|
341
|
+
new Blob([arrayBufferForBlob(input.input.audio)], { type: input.input.mimeType ?? "application/octet-stream" }),
|
|
342
|
+
input.input.filename,
|
|
343
|
+
);
|
|
344
|
+
form.set("response_format", "json");
|
|
345
|
+
|
|
346
|
+
if (input.input.config.language) {
|
|
347
|
+
form.set("language", input.input.config.language);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (input.input.config.prompt) {
|
|
351
|
+
form.set("prompt", input.input.config.prompt);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let response: Response;
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
response = await input.fetcher(input.url, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: {
|
|
360
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
361
|
+
},
|
|
362
|
+
body: form,
|
|
363
|
+
});
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
retryable: true,
|
|
368
|
+
code: "transcription_provider_unavailable",
|
|
369
|
+
message: `Transcription provider request failed before a response: ${redactSecret(
|
|
370
|
+
error instanceof Error ? error.message : String(error),
|
|
371
|
+
input.apiKey,
|
|
372
|
+
)}`,
|
|
373
|
+
provider: input.provider,
|
|
374
|
+
model: input.input.config.model,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const payload = await response.json().catch(() => null);
|
|
379
|
+
|
|
380
|
+
if (!response.ok) {
|
|
381
|
+
const message = readProviderError(payload) ?? `Transcription provider returned HTTP ${response.status}.`;
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
386
|
+
code: response.status === 429 || response.status >= 500 ? "transcription_provider_unavailable" : "transcription_failed",
|
|
387
|
+
message: redactSecret(message, input.apiKey),
|
|
388
|
+
provider: input.provider,
|
|
389
|
+
model: input.input.config.model,
|
|
390
|
+
providerMetadata: {
|
|
391
|
+
status: response.status,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const text = isRecord(payload) && typeof payload.text === "string" ? payload.text.trim() : "";
|
|
397
|
+
|
|
398
|
+
if (!text) {
|
|
399
|
+
return {
|
|
400
|
+
ok: false,
|
|
401
|
+
retryable: false,
|
|
402
|
+
code: "transcription_failed",
|
|
403
|
+
message: "Transcription provider returned an empty transcript.",
|
|
404
|
+
provider: input.provider,
|
|
405
|
+
model: input.input.config.model,
|
|
406
|
+
providerMetadata: {
|
|
407
|
+
status: response.status,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
ok: true,
|
|
414
|
+
text,
|
|
415
|
+
provider: input.provider,
|
|
416
|
+
model: input.input.config.model,
|
|
417
|
+
...(input.input.config.language ? { language: input.input.config.language } : {}),
|
|
418
|
+
...(input.input.durationSeconds !== undefined ? { durationSeconds: input.input.durationSeconds } : {}),
|
|
419
|
+
providerMetadata: {
|
|
420
|
+
status: response.status,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isSupportedAudioFormat(adapter: TranscriptionAdapter, filename: string, mimeType?: string): boolean {
|
|
426
|
+
const normalizedMimeType = mimeType?.toLowerCase();
|
|
427
|
+
|
|
428
|
+
if (normalizedMimeType && adapter.supportedMimeTypes.includes(normalizedMimeType)) {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const extension = extensionFor(filename);
|
|
433
|
+
|
|
434
|
+
return Boolean(extension && adapter.supportedExtensions.includes(extension));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function extensionFor(filename: string): string | null {
|
|
438
|
+
const match = /\.([a-z0-9]+)$/i.exec(filename);
|
|
439
|
+
return match ? match[1].toLowerCase() : null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function decodeFixtureTranscript(audio: Uint8Array): string | null {
|
|
443
|
+
try {
|
|
444
|
+
const text = new TextDecoder().decode(audio).trim();
|
|
445
|
+
return text.length > 0 && /^[\t\n\r -~\u00a0-\uffff]+$/.test(text) ? text : null;
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function arrayBufferForBlob(audio: Uint8Array): ArrayBuffer {
|
|
452
|
+
const copy = new Uint8Array(audio.byteLength);
|
|
453
|
+
copy.set(audio);
|
|
454
|
+
return copy.buffer;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function readProviderError(payload: unknown): string | null {
|
|
458
|
+
if (!isRecord(payload)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (typeof payload.error === "string") {
|
|
463
|
+
return payload.error;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (isRecord(payload.error) && typeof payload.error.message === "string") {
|
|
467
|
+
return payload.error.message;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (typeof payload.message === "string") {
|
|
471
|
+
return payload.message;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function redactSecret(value: string, secret: string): string {
|
|
478
|
+
return secret ? value.replaceAll(secret, "<redacted>") : value;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
482
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
483
|
+
}
|