@smithers-orchestrator/agents 0.24.2 → 0.25.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/package.json +15 -5
- package/src/AgentLike.ts +5 -0
- package/src/AmpAgent.js +15 -5
- package/src/AmpAgentOptions.ts +6 -0
- package/src/BaseCliAgent/BaseCliAgent.js +198 -10
- package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +21 -3
- package/src/BaseCliAgent/index.d.ts +467 -0
- package/src/ClaudeCodeAgent.js +6 -2
- package/src/CodexAgent.js +4 -0
- package/src/GeminiAgent.js +34 -224
- package/src/GeminiAgentOptions.ts +4 -9
- package/src/OpenCodeAgent.js +2 -12
- package/src/OpenCodeAgentOptions.ts +19 -0
- package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +0 -1
- package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +3 -2
- package/src/cli-capabilities/getCliAgentCapabilityReport.js +0 -6
- package/src/cli-surface/cliAgentSurfaceManifest.js +1 -40
- package/src/createElevenLabsTextToSpeechTool.js +128 -0
- package/src/createElevenLabsTextToSpeechTool.ts +33 -0
- package/src/diagnostics/getDiagnosticStrategy.js +13 -12
- package/src/document-parsing/DocumentParsingProvider.ts +13 -0
- package/src/document-parsing/DocumentParsingResult.ts +13 -0
- package/src/document-parsing/DocumentParsingToolset.ts +4 -0
- package/src/document-parsing/DocumentParsingToolsetOptions.ts +9 -0
- package/src/document-parsing/createDocumentParsingToolset.d.ts +9 -0
- package/src/document-parsing/createDocumentParsingToolset.js +416 -0
- package/src/http/CreateHttpToolOptions.ts +4 -0
- package/src/http/HttpToolAuth.ts +15 -0
- package/src/http/HttpToolInput.ts +11 -0
- package/src/http/HttpToolOutput.ts +7 -0
- package/src/http/createHttpTool.js +136 -0
- package/src/image-generation/ImageGenerationProvider.ts +7 -0
- package/src/image-generation/ImageGenerationRequest.ts +8 -0
- package/src/image-generation/ImageGenerationResult.ts +10 -0
- package/src/image-generation/ImageGenerationToolOptions.ts +10 -0
- package/src/image-generation/createImageGenerationTool.d.ts +18 -0
- package/src/image-generation/createImageGenerationTool.js +92 -0
- package/src/index.d.ts +490 -147
- package/src/index.js +23 -5
- package/src/streamResultToGenerateResult.js +55 -26
- package/src/transcription/createTranscriptionTool.js +182 -0
- package/src/transcription/createTranscriptionTool.ts +29 -0
- package/src/transcription/index.js +1 -0
- package/src/transcription/index.ts +6 -0
- package/src/web-search/GroundedWebSearchProvider.ts +21 -0
- package/src/web-search/GroundedWebSearchToolset.ts +6 -0
- package/src/web-search/createBraveSearchProvider.js +53 -0
- package/src/web-search/createExaSearchProvider.js +72 -0
- package/src/web-search/createGroundedWebSearchToolset.js +110 -0
- package/src/web-search/createSerperSearchProvider.js +63 -0
- package/src/web-search/createTavilySearchProvider.js +59 -0
- package/src/web-search/index.js +5 -0
- package/src/zodToOpenAISchema.js +4 -0
- package/src/OpenCodeAgent.ts +0 -43
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
/** @typedef {import("./PiAgentOptions.ts").PiAgentOptions} PiAgentOptions */
|
|
22
22
|
/** @typedef {import("./BaseCliAgent/PiExtensionUiRequest.ts").PiExtensionUiRequest} PiExtensionUiRequest */
|
|
23
23
|
/** @typedef {import("./BaseCliAgent/PiExtensionUiResponse.ts").PiExtensionUiResponse} PiExtensionUiResponse */
|
|
24
|
-
/** @typedef {import("./
|
|
24
|
+
/** @typedef {import("./OpenCodeAgentOptions.ts").OpenCodeAgentOptions} OpenCodeAgentOptions */
|
|
25
25
|
/** @typedef {import("./VibeAgentOptions.ts").VibeAgentOptions} VibeAgentOptions */
|
|
26
26
|
/** @typedef {import("./agent-contract/SmithersAgentContract.ts").SmithersAgentContract} SmithersAgentContract */
|
|
27
27
|
/** @typedef {import("./agent-contract/SmithersAgentContractTool.ts").SmithersAgentContractTool} SmithersAgentContractTool */
|
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
/** @typedef {import("./cli-surface/CliAgentSurfaceTypes.ts").CliAgentSurfaceOptionMapping} CliAgentSurfaceOptionMapping */
|
|
38
38
|
/** @typedef {import("./cli-surface/CliAgentSurfaceTypes.ts").CliAgentSurfaceResumeContract} CliAgentSurfaceResumeContract */
|
|
39
39
|
/** @typedef {import("./cli-surface/CliAgentSurfaceTypes.ts").CliAgentUnsupportedFlag} CliAgentUnsupportedFlag */
|
|
40
|
+
/** @typedef {import("./image-generation/ImageGenerationProvider.ts").ImageGenerationProvider} ImageGenerationProvider */
|
|
41
|
+
/** @typedef {import("./image-generation/ImageGenerationRequest.ts").ImageGenerationRequest} ImageGenerationRequest */
|
|
42
|
+
/** @typedef {import("./image-generation/ImageGenerationResult.ts").ImageGenerationResult} ImageGenerationResult */
|
|
43
|
+
/** @typedef {import("./image-generation/ImageGenerationToolOptions.ts").ImageGenerationToolOptions} ImageGenerationToolOptions */
|
|
44
|
+
/** @typedef {import("./http/CreateHttpToolOptions.ts").CreateHttpToolOptions} CreateHttpToolOptions */
|
|
45
|
+
/** @typedef {import("./http/HttpToolAuth.ts").HttpToolAuth} HttpToolAuth */
|
|
46
|
+
/** @typedef {import("./http/HttpToolInput.ts").HttpToolInput} HttpToolInput */
|
|
47
|
+
/** @typedef {import("./http/HttpToolOutput.ts").HttpToolOutput} HttpToolOutput */
|
|
40
48
|
// @smithers-type-exports-end
|
|
41
49
|
|
|
42
50
|
export { BaseCliAgent } from "./BaseCliAgent/index.js";
|
|
@@ -45,17 +53,15 @@ export { AnthropicAgent } from "./AnthropicAgent.js";
|
|
|
45
53
|
export { OpenAIAgent } from "./OpenAIAgent.js";
|
|
46
54
|
export { HermesAgent } from "./HermesAgent.js";
|
|
47
55
|
export { AmpAgent } from "./AmpAgent.js";
|
|
48
|
-
export {
|
|
49
|
-
export { AntigravityAgent, createAntigravityCapabilityRegistry } from "./AntigravityAgent.js";
|
|
56
|
+
export { AntigravityAgent } from "./AntigravityAgent.js";
|
|
50
57
|
export { ClaudeCodeAgent } from "./ClaudeCodeAgent.js";
|
|
51
58
|
export { CodexAgent } from "./CodexAgent.js";
|
|
52
59
|
export { GeminiAgent } from "./GeminiAgent.js";
|
|
53
60
|
export { PiAgent } from "./PiAgent.js";
|
|
54
61
|
export { KimiAgent } from "./KimiAgent.js";
|
|
55
62
|
export { ForgeAgent } from "./ForgeAgent.js";
|
|
56
|
-
export { createForgeCapabilityRegistry } from "./ForgeAgent.js";
|
|
57
63
|
export { OpenCodeAgent } from "./OpenCodeAgent.js";
|
|
58
|
-
export { VibeAgent
|
|
64
|
+
export { VibeAgent } from "./VibeAgent.js";
|
|
59
65
|
export {
|
|
60
66
|
getCliAgentCapabilityReport,
|
|
61
67
|
getCliAgentCapabilityDoctorReport,
|
|
@@ -66,5 +72,17 @@ export {
|
|
|
66
72
|
} from "./cli-capabilities/index.js";
|
|
67
73
|
export { createSmithersAgentContract } from "./agent-contract/createSmithersAgentContract.js";
|
|
68
74
|
export { renderSmithersAgentPromptGuidance } from "./agent-contract/renderSmithersAgentPromptGuidance.js";
|
|
75
|
+
export { createImageGenerationTool } from "./image-generation/createImageGenerationTool.js";
|
|
76
|
+
export { createHttpTool } from "./http/createHttpTool.js";
|
|
69
77
|
export { zodToOpenAISchema } from "./zodToOpenAISchema.js";
|
|
70
78
|
export { sanitizeForOpenAI } from "./sanitizeForOpenAI.js";
|
|
79
|
+
export { createTranscriptionTool } from "./transcription/createTranscriptionTool.js";
|
|
80
|
+
export {
|
|
81
|
+
createGroundedWebSearchToolset,
|
|
82
|
+
createExaSearchProvider,
|
|
83
|
+
createTavilySearchProvider,
|
|
84
|
+
createBraveSearchProvider,
|
|
85
|
+
createSerperSearchProvider,
|
|
86
|
+
} from "./web-search/index.js";
|
|
87
|
+
|
|
88
|
+
export { createElevenLabsTextToSpeechTool } from "./createElevenLabsTextToSpeechTool.js";
|
|
@@ -8,40 +8,69 @@
|
|
|
8
8
|
* @returns {Promise<GenerateTextResult<TOOLS, any>>}
|
|
9
9
|
*/
|
|
10
10
|
export async function streamResultToGenerateResult(stream, onStdout) {
|
|
11
|
+
// When the provider rejects the request (e.g. a 404 "model not found"), the
|
|
12
|
+
// AI SDK delivers an `error` part on the stream and then rejects the derived
|
|
13
|
+
// promises (text/output/steps) with a generic NoOutputGeneratedError that
|
|
14
|
+
// masks the real cause — which smithers' engine misclassifies as "the agent
|
|
15
|
+
// did not return valid JSON for the declared output schema". Capture the
|
|
16
|
+
// first error here so we can re-throw the genuine provider error instead.
|
|
17
|
+
/** @type {unknown} */
|
|
18
|
+
let streamError;
|
|
11
19
|
if (onStdout) {
|
|
12
20
|
for await (const part of stream.fullStream) {
|
|
13
|
-
if (part.type === "
|
|
21
|
+
if (part.type === "error") {
|
|
22
|
+
if (streamError === undefined)
|
|
23
|
+
streamError = part.error;
|
|
24
|
+
}
|
|
25
|
+
else if (part.type === "text-delta" && part.text) {
|
|
14
26
|
onStdout(part.text);
|
|
15
27
|
}
|
|
16
28
|
}
|
|
17
29
|
}
|
|
18
30
|
else {
|
|
19
|
-
await stream.consumeStream(
|
|
31
|
+
await stream.consumeStream({
|
|
32
|
+
onError: (error) => {
|
|
33
|
+
if (streamError === undefined)
|
|
34
|
+
streamError = error;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** @type {any[]} */
|
|
39
|
+
let resolved;
|
|
40
|
+
try {
|
|
41
|
+
resolved = await Promise.all([
|
|
42
|
+
stream.content,
|
|
43
|
+
stream.text,
|
|
44
|
+
stream.reasoning,
|
|
45
|
+
stream.reasoningText,
|
|
46
|
+
stream.files,
|
|
47
|
+
stream.sources,
|
|
48
|
+
stream.toolCalls,
|
|
49
|
+
stream.staticToolCalls,
|
|
50
|
+
stream.dynamicToolCalls,
|
|
51
|
+
stream.toolResults,
|
|
52
|
+
stream.staticToolResults,
|
|
53
|
+
stream.dynamicToolResults,
|
|
54
|
+
stream.finishReason,
|
|
55
|
+
stream.rawFinishReason,
|
|
56
|
+
stream.usage,
|
|
57
|
+
stream.totalUsage,
|
|
58
|
+
stream.warnings,
|
|
59
|
+
stream.steps,
|
|
60
|
+
stream.request,
|
|
61
|
+
stream.response,
|
|
62
|
+
stream.providerMetadata,
|
|
63
|
+
stream.output,
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// Prefer the captured provider error (the true 404/APICallError) over the
|
|
68
|
+
// SDK's masking NoOutputGeneratedError.
|
|
69
|
+
if (streamError !== undefined)
|
|
70
|
+
throw streamError;
|
|
71
|
+
throw err;
|
|
20
72
|
}
|
|
21
|
-
const [content, text, reasoning, reasoningText, files, sources, toolCalls, staticToolCalls, dynamicToolCalls, toolResults, staticToolResults, dynamicToolResults, finishReason, rawFinishReason, usage, totalUsage, warnings, steps, request, response, providerMetadata, output,] =
|
|
22
|
-
stream.content,
|
|
23
|
-
stream.text,
|
|
24
|
-
stream.reasoning,
|
|
25
|
-
stream.reasoningText,
|
|
26
|
-
stream.files,
|
|
27
|
-
stream.sources,
|
|
28
|
-
stream.toolCalls,
|
|
29
|
-
stream.staticToolCalls,
|
|
30
|
-
stream.dynamicToolCalls,
|
|
31
|
-
stream.toolResults,
|
|
32
|
-
stream.staticToolResults,
|
|
33
|
-
stream.dynamicToolResults,
|
|
34
|
-
stream.finishReason,
|
|
35
|
-
stream.rawFinishReason,
|
|
36
|
-
stream.usage,
|
|
37
|
-
stream.totalUsage,
|
|
38
|
-
stream.warnings,
|
|
39
|
-
stream.steps,
|
|
40
|
-
stream.request,
|
|
41
|
-
stream.response,
|
|
42
|
-
stream.providerMetadata,
|
|
43
|
-
stream.output,
|
|
44
|
-
]);
|
|
73
|
+
const [content, text, reasoning, reasoningText, files, sources, toolCalls, staticToolCalls, dynamicToolCalls, toolResults, staticToolResults, dynamicToolResults, finishReason, rawFinishReason, usage, totalUsage, warnings, steps, request, response, providerMetadata, output,] = resolved;
|
|
45
74
|
return {
|
|
46
75
|
content,
|
|
47
76
|
text,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { dynamicTool, jsonSchema } from "ai";
|
|
2
|
+
|
|
3
|
+
const transcriptionInputSchema = {
|
|
4
|
+
type: "object",
|
|
5
|
+
properties: {
|
|
6
|
+
audioUrl: {
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "HTTP(S) URL for the audio file to transcribe.",
|
|
9
|
+
},
|
|
10
|
+
audioBase64: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Base64-encoded audio bytes. Use this when the audio is already available in memory.",
|
|
13
|
+
},
|
|
14
|
+
mimeType: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "MIME type for audioBase64, for example audio/mpeg or audio/wav.",
|
|
17
|
+
},
|
|
18
|
+
language: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Optional BCP-47 or provider-supported language hint.",
|
|
21
|
+
},
|
|
22
|
+
prompt: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Optional provider prompt or keywords to improve recognition.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create an AI SDK-compatible audio transcription tool backed by Whisper or Deepgram.
|
|
32
|
+
*
|
|
33
|
+
* @param {import("./createTranscriptionTool.ts").CreateTranscriptionToolOptions} options
|
|
34
|
+
* @returns {import("ai").Tool}
|
|
35
|
+
*/
|
|
36
|
+
export function createTranscriptionTool(options) {
|
|
37
|
+
const provider = options.provider;
|
|
38
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
39
|
+
if (typeof fetchImpl !== "function") {
|
|
40
|
+
throw new Error("createTranscriptionTool requires fetch to be available");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return dynamicTool({
|
|
44
|
+
description:
|
|
45
|
+
options.description ??
|
|
46
|
+
"Transcribe speech from an audio URL or base64-encoded audio using a configured transcription provider.",
|
|
47
|
+
inputSchema: jsonSchema(transcriptionInputSchema),
|
|
48
|
+
execute: async (input) => {
|
|
49
|
+
const request = normalizeInput(input);
|
|
50
|
+
if (provider === "whisper") {
|
|
51
|
+
return transcribeWithWhisper(options, request, fetchImpl);
|
|
52
|
+
}
|
|
53
|
+
if (provider === "deepgram") {
|
|
54
|
+
return transcribeWithDeepgram(options, request, fetchImpl);
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Unsupported transcription provider: ${provider}`);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {unknown} input
|
|
63
|
+
* @returns {import("./createTranscriptionTool.ts").TranscriptionToolInput}
|
|
64
|
+
*/
|
|
65
|
+
function normalizeInput(input) {
|
|
66
|
+
const value = input && typeof input === "object" ? /** @type {Record<string, unknown>} */ (input) : {};
|
|
67
|
+
const audioUrl = typeof value.audioUrl === "string" && value.audioUrl.trim() ? value.audioUrl.trim() : undefined;
|
|
68
|
+
const audioBase64 =
|
|
69
|
+
typeof value.audioBase64 === "string" && value.audioBase64.trim() ? value.audioBase64.trim() : undefined;
|
|
70
|
+
if (!audioUrl && !audioBase64) {
|
|
71
|
+
throw new Error("Transcription requires either audioUrl or audioBase64");
|
|
72
|
+
}
|
|
73
|
+
if (audioUrl && audioBase64) {
|
|
74
|
+
throw new Error("Transcription accepts only one of audioUrl or audioBase64");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
78
|
+
...(audioBase64 ? { audioBase64 } : {}),
|
|
79
|
+
...(typeof value.mimeType === "string" && value.mimeType.trim() ? { mimeType: value.mimeType.trim() } : {}),
|
|
80
|
+
...(typeof value.language === "string" && value.language.trim() ? { language: value.language.trim() } : {}),
|
|
81
|
+
...(typeof value.prompt === "string" && value.prompt.trim() ? { prompt: value.prompt.trim() } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {import("./createTranscriptionTool.ts").CreateTranscriptionToolOptions} options
|
|
87
|
+
* @param {import("./createTranscriptionTool.ts").TranscriptionToolInput} input
|
|
88
|
+
* @param {typeof fetch} fetchImpl
|
|
89
|
+
* @returns {Promise<import("./createTranscriptionTool.ts").TranscriptionToolResult>}
|
|
90
|
+
*/
|
|
91
|
+
async function transcribeWithWhisper(options, input, fetchImpl) {
|
|
92
|
+
const form = new FormData();
|
|
93
|
+
form.set("model", options.model ?? "whisper-1");
|
|
94
|
+
form.set("response_format", "verbose_json");
|
|
95
|
+
if (input.language) form.set("language", input.language);
|
|
96
|
+
if (input.prompt) form.set("prompt", input.prompt);
|
|
97
|
+
|
|
98
|
+
if (input.audioBase64) {
|
|
99
|
+
form.set("file", base64ToFile(input.audioBase64, input.mimeType ?? "application/octet-stream"));
|
|
100
|
+
} else if (input.audioUrl) {
|
|
101
|
+
const audioResponse = await fetchImpl(input.audioUrl);
|
|
102
|
+
await assertOk(audioResponse, "download audio for Whisper transcription");
|
|
103
|
+
const blob = await audioResponse.blob();
|
|
104
|
+
form.set("file", new File([blob], filenameForMime(input.mimeType ?? blob.type), { type: input.mimeType ?? blob.type }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const response = await fetchImpl(options.baseUrl ?? "https://api.openai.com/v1/audio/transcriptions", {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { Authorization: `Bearer ${options.apiKey}` },
|
|
110
|
+
body: form,
|
|
111
|
+
});
|
|
112
|
+
await assertOk(response, "transcribe audio with Whisper");
|
|
113
|
+
const payload = /** @type {any} */ (await response.json());
|
|
114
|
+
return {
|
|
115
|
+
text: String(payload.text ?? ""),
|
|
116
|
+
...(typeof payload.language === "string" ? { language: payload.language } : {}),
|
|
117
|
+
...(typeof payload.duration === "number" ? { durationSeconds: payload.duration } : {}),
|
|
118
|
+
provider: "whisper",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {import("./createTranscriptionTool.ts").CreateTranscriptionToolOptions} options
|
|
124
|
+
* @param {import("./createTranscriptionTool.ts").TranscriptionToolInput} input
|
|
125
|
+
* @param {typeof fetch} fetchImpl
|
|
126
|
+
* @returns {Promise<import("./createTranscriptionTool.ts").TranscriptionToolResult>}
|
|
127
|
+
*/
|
|
128
|
+
async function transcribeWithDeepgram(options, input, fetchImpl) {
|
|
129
|
+
const body = input.audioUrl
|
|
130
|
+
? JSON.stringify({ url: input.audioUrl })
|
|
131
|
+
: Buffer.from(input.audioBase64 ?? "", "base64");
|
|
132
|
+
const url = new URL(options.baseUrl ?? "https://api.deepgram.com/v1/listen");
|
|
133
|
+
url.searchParams.set("model", options.model ?? "nova-3");
|
|
134
|
+
url.searchParams.set("smart_format", "true");
|
|
135
|
+
if (input.language) url.searchParams.set("language", input.language);
|
|
136
|
+
|
|
137
|
+
const response = await fetchImpl(url.toString(), {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Token ${options.apiKey}`,
|
|
141
|
+
"Content-Type": input.audioUrl ? "application/json" : (input.mimeType ?? "application/octet-stream"),
|
|
142
|
+
},
|
|
143
|
+
body,
|
|
144
|
+
});
|
|
145
|
+
await assertOk(response, "transcribe audio with Deepgram");
|
|
146
|
+
const payload = /** @type {any} */ (await response.json());
|
|
147
|
+
const alternative = payload.results?.channels?.[0]?.alternatives?.[0] ?? {};
|
|
148
|
+
return {
|
|
149
|
+
text: String(alternative.transcript ?? ""),
|
|
150
|
+
...(typeof payload.metadata?.duration === "number" ? { durationSeconds: payload.metadata.duration } : {}),
|
|
151
|
+
provider: "deepgram",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {Response} response
|
|
157
|
+
* @param {string} action
|
|
158
|
+
*/
|
|
159
|
+
async function assertOk(response, action) {
|
|
160
|
+
if (response.ok) return;
|
|
161
|
+
const message = await response.text().catch(() => "");
|
|
162
|
+
throw new Error(`Failed to ${action}: ${response.status} ${response.statusText}${message ? ` - ${message}` : ""}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} audioBase64
|
|
167
|
+
* @param {string} mimeType
|
|
168
|
+
* @returns {File}
|
|
169
|
+
*/
|
|
170
|
+
function base64ToFile(audioBase64, mimeType) {
|
|
171
|
+
const bytes = Buffer.from(audioBase64, "base64");
|
|
172
|
+
return new File([bytes], filenameForMime(mimeType), { type: mimeType });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} mimeType
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function filenameForMime(mimeType) {
|
|
180
|
+
const extension = mimeType.split("/")[1]?.split(";")[0] || "bin";
|
|
181
|
+
return `audio.${extension}`;
|
|
182
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Tool } from "ai";
|
|
2
|
+
|
|
3
|
+
export type TranscriptionProvider = "whisper" | "deepgram";
|
|
4
|
+
|
|
5
|
+
export type TranscriptionToolInput = {
|
|
6
|
+
audioUrl?: string;
|
|
7
|
+
audioBase64?: string;
|
|
8
|
+
mimeType?: string;
|
|
9
|
+
language?: string;
|
|
10
|
+
prompt?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TranscriptionToolResult = {
|
|
14
|
+
text: string;
|
|
15
|
+
language?: string;
|
|
16
|
+
durationSeconds?: number;
|
|
17
|
+
provider: TranscriptionProvider;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CreateTranscriptionToolOptions = {
|
|
21
|
+
provider: TranscriptionProvider;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export declare function createTranscriptionTool(options: CreateTranscriptionToolOptions): Tool;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTranscriptionTool } from "./createTranscriptionTool.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type GroundedWebSearchProviderKind = "semantic" | "fresh";
|
|
2
|
+
|
|
3
|
+
export type GroundedWebSearchProviderName = "exa" | "tavily" | "brave" | "serper";
|
|
4
|
+
|
|
5
|
+
export type GroundedWebSearchResult = {
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
snippet?: string;
|
|
9
|
+
publishedDate?: string;
|
|
10
|
+
score?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type GroundedWebSearchProvider = {
|
|
14
|
+
name: GroundedWebSearchProviderName;
|
|
15
|
+
kind: GroundedWebSearchProviderKind;
|
|
16
|
+
search(input: {
|
|
17
|
+
query: string;
|
|
18
|
+
maxResults: number;
|
|
19
|
+
freshness?: "day" | "week" | "month" | "year";
|
|
20
|
+
}): Promise<GroundedWebSearchResult[]>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** @typedef {import("./GroundedWebSearchProvider.ts").GroundedWebSearchProvider} GroundedWebSearchProvider */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ apiKey: string; baseUrl?: string; fetch?: typeof fetch }} options
|
|
5
|
+
* @returns {GroundedWebSearchProvider}
|
|
6
|
+
*/
|
|
7
|
+
export function createBraveSearchProvider(options) {
|
|
8
|
+
return {
|
|
9
|
+
name: "brave",
|
|
10
|
+
kind: "fresh",
|
|
11
|
+
async search(input) {
|
|
12
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
13
|
+
const params = new URLSearchParams({ q: input.query, count: String(input.maxResults) });
|
|
14
|
+
const freshness = freshnessParam(input.freshness);
|
|
15
|
+
if (freshness) params.set("freshness", freshness);
|
|
16
|
+
const response = await fetchImpl(`${options.baseUrl ?? "https://api.search.brave.com/res/v1/web/search"}?${params}`, {
|
|
17
|
+
headers: {
|
|
18
|
+
accept: "application/json",
|
|
19
|
+
"x-subscription-token": options.apiKey,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const body = await readJson(response, "Brave");
|
|
23
|
+
const results = Array.isArray(body.web?.results) ? body.web.results : [];
|
|
24
|
+
return results.map((result) => ({
|
|
25
|
+
title: String(result.title ?? result.url ?? "Untitled"),
|
|
26
|
+
url: String(result.url ?? ""),
|
|
27
|
+
snippet: result.description,
|
|
28
|
+
})).filter((result) => result.url);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {string | undefined} freshness */
|
|
34
|
+
function freshnessParam(freshness) {
|
|
35
|
+
if (freshness === "day") return "pd";
|
|
36
|
+
if (freshness === "week") return "pw";
|
|
37
|
+
if (freshness === "month") return "pm";
|
|
38
|
+
if (freshness === "year") return "py";
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {Response} response
|
|
44
|
+
* @param {string} provider
|
|
45
|
+
* @returns {Promise<any>}
|
|
46
|
+
*/
|
|
47
|
+
async function readJson(response, provider) {
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`${provider} search failed (${response.status}): ${text}`);
|
|
51
|
+
}
|
|
52
|
+
return text ? JSON.parse(text) : {};
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** @typedef {import("./GroundedWebSearchProvider.ts").GroundedWebSearchProvider} GroundedWebSearchProvider */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ apiKey: string; baseUrl?: string; fetch?: typeof fetch }} options
|
|
5
|
+
* @returns {GroundedWebSearchProvider}
|
|
6
|
+
*/
|
|
7
|
+
export function createExaSearchProvider(options) {
|
|
8
|
+
return {
|
|
9
|
+
name: "exa",
|
|
10
|
+
kind: "semantic",
|
|
11
|
+
async search(input) {
|
|
12
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
13
|
+
const response = await fetchImpl(`${options.baseUrl ?? "https://api.exa.ai"}/search`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
"content-type": "application/json",
|
|
17
|
+
"x-api-key": options.apiKey,
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
query: input.query,
|
|
21
|
+
numResults: input.maxResults,
|
|
22
|
+
useAutoprompt: true,
|
|
23
|
+
...freshnessParams(input.freshness),
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
const body = await readJson(response, "Exa");
|
|
27
|
+
const results = Array.isArray(body.results) ? body.results : [];
|
|
28
|
+
return results.map((result) => ({
|
|
29
|
+
title: String(result.title ?? result.url ?? "Untitled"),
|
|
30
|
+
url: String(result.url ?? ""),
|
|
31
|
+
snippet: typeof result.text === "string" ? result.text : result.summary,
|
|
32
|
+
publishedDate: result.publishedDate,
|
|
33
|
+
score: typeof result.score === "number" ? result.score : undefined,
|
|
34
|
+
})).filter((result) => result.url);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string | undefined} freshness */
|
|
40
|
+
function freshnessParams(freshness) {
|
|
41
|
+
const days = freshnessDays(freshness);
|
|
42
|
+
return days ? { startPublishedDate: isoDateDaysAgo(days) } : {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string | undefined} freshness */
|
|
46
|
+
function freshnessDays(freshness) {
|
|
47
|
+
if (freshness === "day") return 1;
|
|
48
|
+
if (freshness === "week") return 7;
|
|
49
|
+
if (freshness === "month") return 30;
|
|
50
|
+
if (freshness === "year") return 365;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {number} days */
|
|
55
|
+
function isoDateDaysAgo(days) {
|
|
56
|
+
const date = new Date();
|
|
57
|
+
date.setUTCDate(date.getUTCDate() - days);
|
|
58
|
+
return date.toISOString().slice(0, 10);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Response} response
|
|
63
|
+
* @param {string} provider
|
|
64
|
+
* @returns {Promise<any>}
|
|
65
|
+
*/
|
|
66
|
+
async function readJson(response, provider) {
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`${provider} search failed (${response.status}): ${text}`);
|
|
70
|
+
}
|
|
71
|
+
return text ? JSON.parse(text) : {};
|
|
72
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { dynamicTool, jsonSchema } from "ai";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./GroundedWebSearchProvider.ts").GroundedWebSearchProvider} GroundedWebSearchProvider */
|
|
4
|
+
/** @typedef {import("./GroundedWebSearchProvider.ts").GroundedWebSearchResult} GroundedWebSearchResult */
|
|
5
|
+
/** @typedef {import("./GroundedWebSearchToolset.ts").GroundedWebSearchToolset} GroundedWebSearchToolset */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {{ providers: GroundedWebSearchProvider[]; maxResultsPerProvider?: number }} options
|
|
9
|
+
* @returns {GroundedWebSearchToolset}
|
|
10
|
+
*/
|
|
11
|
+
export function createGroundedWebSearchToolset(options) {
|
|
12
|
+
const providers = options.providers ?? [];
|
|
13
|
+
if (!providers.some((provider) => provider.name === "exa" && provider.kind === "semantic")) {
|
|
14
|
+
throw new Error("grounded_web_search requires Exa as the semantic provider");
|
|
15
|
+
}
|
|
16
|
+
if (!providers.some((provider) => provider.kind === "fresh")) {
|
|
17
|
+
throw new Error("grounded_web_search requires at least one fresh/SERP provider: Tavily, Brave, or Serper");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
tools: {
|
|
22
|
+
grounded_web_search: dynamicTool({
|
|
23
|
+
description: "Search the web with both Exa semantic retrieval and a fresh/SERP provider, returning grounded citations.",
|
|
24
|
+
inputSchema: jsonSchema({
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
query: { type: "string", minLength: 1 },
|
|
29
|
+
maxResults: { type: "number", minimum: 1, maximum: 20 },
|
|
30
|
+
freshness: { type: "string", enum: ["day", "week", "month", "year"] },
|
|
31
|
+
},
|
|
32
|
+
required: ["query"],
|
|
33
|
+
}),
|
|
34
|
+
execute: async (input) => searchAll(providers, input, options.maxResultsPerProvider ?? 5),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
toolNames: ["grounded_web_search"],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {GroundedWebSearchProvider[]} providers
|
|
43
|
+
* @param {unknown} input
|
|
44
|
+
* @param {number} maxResultsPerProvider
|
|
45
|
+
* @returns {Promise<{ query: string; providers: string[]; results: Array<GroundedWebSearchResult & { provider: string; citation: number }> }>}
|
|
46
|
+
*/
|
|
47
|
+
async function searchAll(providers, input, maxResultsPerProvider) {
|
|
48
|
+
const args = normalizeInput(input, maxResultsPerProvider);
|
|
49
|
+
const settled = await Promise.allSettled(providers.map(async (provider) => ({
|
|
50
|
+
provider,
|
|
51
|
+
results: await provider.search(args),
|
|
52
|
+
})));
|
|
53
|
+
const deduped = new Map();
|
|
54
|
+
const succeededProviders = [];
|
|
55
|
+
for (const outcome of settled) {
|
|
56
|
+
if (outcome.status !== "fulfilled") continue;
|
|
57
|
+
const entry = outcome.value;
|
|
58
|
+
succeededProviders.push(entry.provider.name);
|
|
59
|
+
for (const result of entry.results) {
|
|
60
|
+
const key = normalizeUrl(result.url);
|
|
61
|
+
if (!key || deduped.has(key)) continue;
|
|
62
|
+
deduped.set(key, {
|
|
63
|
+
...result,
|
|
64
|
+
provider: entry.provider.name,
|
|
65
|
+
citation: deduped.size + 1,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
query: args.query,
|
|
71
|
+
providers: succeededProviders,
|
|
72
|
+
results: [...deduped.values()],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {unknown} input
|
|
78
|
+
* @param {number} defaultMaxResults
|
|
79
|
+
*/
|
|
80
|
+
function normalizeInput(input, defaultMaxResults) {
|
|
81
|
+
const value = input && typeof input === "object" ? /** @type {Record<string, unknown>} */ (input) : {};
|
|
82
|
+
const query = typeof value.query === "string" ? value.query.trim() : "";
|
|
83
|
+
if (!query) {
|
|
84
|
+
throw new Error("grounded_web_search requires a non-empty query");
|
|
85
|
+
}
|
|
86
|
+
const requestedMax = typeof value.maxResults === "number" ? value.maxResults : defaultMaxResults;
|
|
87
|
+
const cappedMax = Math.min(requestedMax, defaultMaxResults);
|
|
88
|
+
return {
|
|
89
|
+
query,
|
|
90
|
+
maxResults: Math.min(Math.max(Math.trunc(cappedMax), 1), 20),
|
|
91
|
+
freshness: isFreshness(value.freshness) ? value.freshness : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {unknown} value */
|
|
96
|
+
function isFreshness(value) {
|
|
97
|
+
return value === "day" || value === "week" || value === "month" || value === "year";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {string | undefined} url */
|
|
101
|
+
function normalizeUrl(url) {
|
|
102
|
+
if (!url) return "";
|
|
103
|
+
try {
|
|
104
|
+
const parsed = new URL(url);
|
|
105
|
+
parsed.hash = "";
|
|
106
|
+
return parsed.toString();
|
|
107
|
+
} catch {
|
|
108
|
+
return url;
|
|
109
|
+
}
|
|
110
|
+
}
|