@databricks/appkit 0.28.0 → 0.29.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/agents/databricks.d.ts +180 -0
- package/dist/agents/databricks.d.ts.map +1 -0
- package/dist/agents/databricks.js +473 -0
- package/dist/agents/databricks.js.map +1 -0
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -1
- package/dist/beta.js +3 -1
- package/dist/connectors/serving/client.js +27 -2
- package/dist/connectors/serving/client.js.map +1 -1
- package/dist/shared/src/agent.d.ts +72 -0
- package/dist/shared/src/agent.d.ts.map +1 -0
- package/dist/shared/src/index.d.ts +1 -0
- package/package.json +2 -2
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { AgentAdapter, AgentEvent, AgentInput, AgentRunContext } from "../shared/src/agent.js";
|
|
2
|
+
import "../shared/src/index.js";
|
|
3
|
+
|
|
4
|
+
//#region src/agents/databricks.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Transport shim: given an OpenAI-compatible request body, returns the raw
|
|
7
|
+
* SSE byte stream from the serving endpoint. Injected at construction time so
|
|
8
|
+
* callers can swap in the workspace SDK (factory paths), a bare `fetch`
|
|
9
|
+
* (the raw constructor), or a test fake.
|
|
10
|
+
*/
|
|
11
|
+
type StreamBody = (body: Record<string, unknown>, signal?: AbortSignal) => Promise<ReadableStream<Uint8Array>>;
|
|
12
|
+
/**
|
|
13
|
+
* Escape-hatch options: provide an `endpointUrl` + `authenticate()` and the
|
|
14
|
+
* adapter uses a bare `fetch()` to call it. Useful for tests and for pointing
|
|
15
|
+
* the adapter at non-workspace endpoints (reverse proxies, mocks).
|
|
16
|
+
*/
|
|
17
|
+
interface RawFetchAdapterOptions {
|
|
18
|
+
endpointUrl: string;
|
|
19
|
+
authenticate: () => Promise<Record<string, string>>;
|
|
20
|
+
maxSteps?: number;
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
/** Max length of one SSE line (including an incomplete tail in the buffer). */
|
|
23
|
+
maxSseLineChars?: number;
|
|
24
|
+
/** Max total length of assistant `delta.content` across the stream. */
|
|
25
|
+
maxStreamTextChars?: number;
|
|
26
|
+
/** Max length of streamed `function.arguments` per tool call index. */
|
|
27
|
+
maxToolArgumentsChars?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Preferred options: caller provides the transport function directly.
|
|
31
|
+
* The `fromServingEndpoint` / `fromModelServing` factories use this to route
|
|
32
|
+
* through `connectors/serving/stream`, which centralises URL encoding, auth
|
|
33
|
+
* via the SDK's `apiClient.request`, and any future retries/telemetry.
|
|
34
|
+
*/
|
|
35
|
+
interface StreamBodyAdapterOptions {
|
|
36
|
+
streamBody: StreamBody;
|
|
37
|
+
maxSteps?: number;
|
|
38
|
+
maxTokens?: number;
|
|
39
|
+
maxSseLineChars?: number;
|
|
40
|
+
maxStreamTextChars?: number;
|
|
41
|
+
maxToolArgumentsChars?: number;
|
|
42
|
+
}
|
|
43
|
+
type DatabricksAdapterOptions = RawFetchAdapterOptions | StreamBodyAdapterOptions;
|
|
44
|
+
/**
|
|
45
|
+
* Duck-typed subset of the Databricks SDK `WorkspaceClient`. Callers of
|
|
46
|
+
* `fromServingEndpoint` and `fromModelServing` pass a real `WorkspaceClient`,
|
|
47
|
+
* but we only need the `apiClient.request` surface — so we declare the minimal
|
|
48
|
+
* interface rather than importing the SDK type directly. This keeps the adapter
|
|
49
|
+
* free of a hard compile-time dependency on `@databricks/sdk-experimental`.
|
|
50
|
+
*/
|
|
51
|
+
interface WorkspaceClientLike {
|
|
52
|
+
apiClient: {
|
|
53
|
+
request(options: Record<string, unknown>): Promise<unknown>;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
interface ServingEndpointOptions {
|
|
57
|
+
workspaceClient: WorkspaceClientLike;
|
|
58
|
+
endpointName: string;
|
|
59
|
+
maxSteps?: number;
|
|
60
|
+
maxTokens?: number;
|
|
61
|
+
maxSseLineChars?: number;
|
|
62
|
+
maxStreamTextChars?: number;
|
|
63
|
+
maxToolArgumentsChars?: number;
|
|
64
|
+
}
|
|
65
|
+
interface ModelServingOptions {
|
|
66
|
+
maxSteps?: number;
|
|
67
|
+
maxTokens?: number;
|
|
68
|
+
workspaceClient?: WorkspaceClientLike;
|
|
69
|
+
maxSseLineChars?: number;
|
|
70
|
+
maxStreamTextChars?: number;
|
|
71
|
+
maxToolArgumentsChars?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adapter that talks directly to Databricks Model Serving `/invocations` endpoint.
|
|
75
|
+
*
|
|
76
|
+
* No dependency on the Vercel AI SDK or LangChain. Uses raw `fetch()` to POST
|
|
77
|
+
* OpenAI-compatible payloads and parses the SSE stream itself. Calls
|
|
78
|
+
* `authenticate()` per-request so tokens are always fresh.
|
|
79
|
+
*
|
|
80
|
+
* Handles both structured `tool_calls` responses and text-based tool call
|
|
81
|
+
* fallback parsing for models that output tool calls as text.
|
|
82
|
+
*
|
|
83
|
+
* @example Using the factory (recommended)
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { createApp, createAgent, agents } from "@databricks/appkit";
|
|
86
|
+
* import { DatabricksAdapter } from "@databricks/appkit/beta";
|
|
87
|
+
* import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
88
|
+
*
|
|
89
|
+
* const adapter = DatabricksAdapter.fromServingEndpoint({
|
|
90
|
+
* workspaceClient: new WorkspaceClient({}),
|
|
91
|
+
* endpointName: "my-endpoint",
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* await createApp({
|
|
95
|
+
* plugins: [
|
|
96
|
+
* agents({
|
|
97
|
+
* agents: {
|
|
98
|
+
* assistant: createAgent({
|
|
99
|
+
* instructions: "You are a helpful assistant.",
|
|
100
|
+
* model: adapter,
|
|
101
|
+
* }),
|
|
102
|
+
* },
|
|
103
|
+
* }),
|
|
104
|
+
* ],
|
|
105
|
+
* });
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @example Using the raw constructor
|
|
109
|
+
* ```ts
|
|
110
|
+
* const adapter = new DatabricksAdapter({
|
|
111
|
+
* endpointUrl: "https://host/serving-endpoints/my-endpoint/invocations",
|
|
112
|
+
* authenticate: async () => ({ Authorization: `Bearer ${token}` }),
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare class DatabricksAdapter implements AgentAdapter {
|
|
117
|
+
private streamBody;
|
|
118
|
+
private maxSteps;
|
|
119
|
+
private maxTokens;
|
|
120
|
+
private maxSseLineChars;
|
|
121
|
+
private maxStreamTextChars;
|
|
122
|
+
private maxToolArgumentsChars;
|
|
123
|
+
constructor(options: DatabricksAdapterOptions);
|
|
124
|
+
/**
|
|
125
|
+
* Creates a DatabricksAdapter for a Databricks Model Serving endpoint.
|
|
126
|
+
*
|
|
127
|
+
* Routes through the shared `connectors/serving/stream` helper, which
|
|
128
|
+
* delegates to the SDK's `apiClient.request({ raw: true })`. That gives the
|
|
129
|
+
* adapter centralised URL encoding + authentication with the rest of the
|
|
130
|
+
* serving surface — no bespoke `fetch()` + `authenticate()` plumbing.
|
|
131
|
+
*/
|
|
132
|
+
static fromServingEndpoint(options: ServingEndpointOptions): Promise<DatabricksAdapter>;
|
|
133
|
+
/**
|
|
134
|
+
* Creates a DatabricksAdapter from a Model Serving endpoint name.
|
|
135
|
+
* Auto-creates a WorkspaceClient internally. Reads the endpoint name
|
|
136
|
+
* from the argument or the `DATABRICKS_SERVING_ENDPOINT_NAME` env var.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* // Reads endpoint from DATABRICKS_SERVING_ENDPOINT_NAME env var
|
|
141
|
+
* const adapter = await DatabricksAdapter.fromModelServing();
|
|
142
|
+
*
|
|
143
|
+
* // Explicit endpoint
|
|
144
|
+
* const adapter = await DatabricksAdapter.fromModelServing("my-endpoint");
|
|
145
|
+
*
|
|
146
|
+
* // With options
|
|
147
|
+
* const adapter = await DatabricksAdapter.fromModelServing("my-endpoint", {
|
|
148
|
+
* maxSteps: 5,
|
|
149
|
+
* maxTokens: 2048,
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
static fromModelServing(endpointName?: string, options?: ModelServingOptions): Promise<DatabricksAdapter>;
|
|
154
|
+
run(input: AgentInput, context: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown>;
|
|
155
|
+
/** Parse wire arguments, emit tool_call / tool_result, append tool messages. */
|
|
156
|
+
private executeSingleTool;
|
|
157
|
+
private streamCompletion;
|
|
158
|
+
private executeToolCalls;
|
|
159
|
+
/**
|
|
160
|
+
* Maps AppKit {@link AgentInput} messages into OpenAI-compatible wire messages.
|
|
161
|
+
* Preserves multi-turn tool state (`toolCalls` → `tool_calls`, `toolCallId` →
|
|
162
|
+
* `tool_call_id`) so resumed threads and hydrated history reach the model.
|
|
163
|
+
*/
|
|
164
|
+
private buildMessages;
|
|
165
|
+
private buildTools;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parses text-based tool calls from model output.
|
|
169
|
+
*
|
|
170
|
+
* Handles two formats:
|
|
171
|
+
* 1. Llama native: `[{"name": "tool_name", "parameters": {"arg": "val"}}]`
|
|
172
|
+
* 2. Python-style: `[tool_name(arg1='val1', arg2='val2')]`
|
|
173
|
+
*/
|
|
174
|
+
declare function parseTextToolCalls(text: string): Array<{
|
|
175
|
+
name: string;
|
|
176
|
+
args: unknown;
|
|
177
|
+
}>;
|
|
178
|
+
//#endregion
|
|
179
|
+
export { DatabricksAdapter, parseTextToolCalls };
|
|
180
|
+
//# sourceMappingURL=databricks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"databricks.d.ts","names":[],"sources":["../../src/agents/databricks.ts"],"mappings":";;;;;;;AAMgB;;;KAgEX,UAAA,IACH,IAAA,EAAM,MAAA,mBACN,MAAA,GAAS,WAAA,KACN,OAAA,CAAQ,cAAA,CAAe,UAAA;;;;;;UAOlB,sBAAA;EACR,WAAA;EACA,YAAA,QAAoB,OAAA,CAAQ,MAAA;EAC5B,QAAA;EACA,SAAA;EAXG;EAaH,eAAA;EAb0B;EAe1B,kBAAA;EAfoC;EAiBpC,qBAAA;AAAA;;;;;;;UASQ,wBAAA;EACR,UAAA,EAAY,UAAA;EACZ,QAAA;EACA,SAAA;EACA,eAAA;EACA,kBAAA;EACA,qBAAA;AAAA;AAAA,KAGG,wBAAA,GACD,sBAAA,GACA,wBAAA;;;;;;;;UAeM,mBAAA;EACR,SAAA;IACE,OAAA,CAAQ,OAAA,EAAS,MAAA,oBAA0B,OAAA;EAAA;AAAA;AAAA,UAIrC,sBAAA;EACR,eAAA,EAAiB,mBAAA;EACjB,YAAA;EACA,QAAA;EACA,SAAA;EACA,eAAA;EACA,kBAAA;EACA,qBAAA;AAAA;AAAA,UAGQ,mBAAA;EACR,QAAA;EACA,SAAA;EACA,eAAA,GAAkB,mBAAA;EAClB,eAAA;EACA,kBAAA;EACA,qBAAA;AAAA;;;;;;;;;;;;;AATqB;;;;;;;;;;;;;AAoFvB;;;;;;;;;;;;;;;;;;cAAa,iBAAA,YAA6B,YAAA;EAAA,QAChC,UAAA;EAAA,QACA,QAAA;EAAA,QACA,SAAA;EAAA,QACA,eAAA;EAAA,QACA,kBAAA;EAAA,QACA,qBAAA;cAEI,OAAA,EAAS,wBAAA;EAAT;;;;;;;;EAAA,OA+CC,mBAAA,CACX,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,iBAAA;EAmDT;;;;;;;;;;;;;;;;;AA+YJ;;;EA/YI,OAFW,gBAAA,CACX,YAAA,WACA,OAAA,GAAU,mBAAA,GACT,OAAA,CAAQ,iBAAA;EA+BJ,GAAA,CACL,KAAA,EAAO,UAAA,EACP,OAAA,EAAS,eAAA,GACR,cAAA,CAAe,UAAA;EA8WjB;EAAA,QAvTc,iBAAA;EAAA,QA8CA,gBAAA;EAAA,QA4JA,gBAAA;EA6GY;;;;;EAAA,QAzEnB,aAAA;EAAA,QA6CA,UAAA;AAAA;;;;;;;;iBA0BM,kBAAA,CACd,IAAA,WACC,KAAA;EAAQ,IAAA;EAAc,IAAA;AAAA"}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { stream } from "../connectors/serving/client.js";
|
|
2
|
+
|
|
3
|
+
//#region src/agents/databricks.ts
|
|
4
|
+
/** Default cap for a single incomplete SSE line tail (DoS guard). */
|
|
5
|
+
const DEFAULT_MAX_SSE_LINE_CHARS = 1024 * 1024;
|
|
6
|
+
/** Default cap for accumulated assistant text from `delta.content`. */
|
|
7
|
+
const DEFAULT_MAX_STREAM_TEXT_CHARS = 4 * 1024 * 1024;
|
|
8
|
+
/** Default cap for accumulated JSON arguments per streamed tool call index. */
|
|
9
|
+
const DEFAULT_MAX_TOOL_ARGUMENT_CHARS = 2 * 1024 * 1024;
|
|
10
|
+
/** Cap text length before running Python-style tool-call regex (ReDoS guard). */
|
|
11
|
+
const PYTHON_STYLE_TOOL_PARSE_MAX_INPUT = 64 * 1024;
|
|
12
|
+
/** Fallback HTTP timeout when the raw fetch adapter path receives no AbortSignal from the runner. */
|
|
13
|
+
const RAW_FETCH_DEFAULT_TIMEOUT_MS = 12e4;
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null;
|
|
16
|
+
}
|
|
17
|
+
function extractLlamaToolJsonSlice(text) {
|
|
18
|
+
const start = text.indexOf("[{");
|
|
19
|
+
if (start < 0) return void 0;
|
|
20
|
+
const endBracket = text.lastIndexOf("}]");
|
|
21
|
+
if (endBracket < start) return void 0;
|
|
22
|
+
return text.slice(start, endBracket + 2);
|
|
23
|
+
}
|
|
24
|
+
/** OpenAI SSE payload: `{ choices: [{ delta }] }`. */
|
|
25
|
+
function openAiChoicesDelta(parsed) {
|
|
26
|
+
if (!isRecord(parsed)) return void 0;
|
|
27
|
+
const choices = parsed.choices;
|
|
28
|
+
if (!Array.isArray(choices) || choices.length < 1) return void 0;
|
|
29
|
+
const first = choices[0];
|
|
30
|
+
if (!isRecord(first)) return void 0;
|
|
31
|
+
return first.delta;
|
|
32
|
+
}
|
|
33
|
+
function isStreamingDeltaToolCall(value) {
|
|
34
|
+
if (!isRecord(value)) return false;
|
|
35
|
+
return typeof value.index === "number";
|
|
36
|
+
}
|
|
37
|
+
function throwIfExceedsStreamLimit(label, currentLength, chunk, max) {
|
|
38
|
+
if (currentLength + chunk.length > max) throw new Error(`DatabricksAdapter: ${label} exceeds configured limit (${max} UTF-16 code units)`);
|
|
39
|
+
}
|
|
40
|
+
function isStreamBodyOptions(o) {
|
|
41
|
+
return "streamBody" in o;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Adapter that talks directly to Databricks Model Serving `/invocations` endpoint.
|
|
45
|
+
*
|
|
46
|
+
* No dependency on the Vercel AI SDK or LangChain. Uses raw `fetch()` to POST
|
|
47
|
+
* OpenAI-compatible payloads and parses the SSE stream itself. Calls
|
|
48
|
+
* `authenticate()` per-request so tokens are always fresh.
|
|
49
|
+
*
|
|
50
|
+
* Handles both structured `tool_calls` responses and text-based tool call
|
|
51
|
+
* fallback parsing for models that output tool calls as text.
|
|
52
|
+
*
|
|
53
|
+
* @example Using the factory (recommended)
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { createApp, createAgent, agents } from "@databricks/appkit";
|
|
56
|
+
* import { DatabricksAdapter } from "@databricks/appkit/beta";
|
|
57
|
+
* import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
58
|
+
*
|
|
59
|
+
* const adapter = DatabricksAdapter.fromServingEndpoint({
|
|
60
|
+
* workspaceClient: new WorkspaceClient({}),
|
|
61
|
+
* endpointName: "my-endpoint",
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* await createApp({
|
|
65
|
+
* plugins: [
|
|
66
|
+
* agents({
|
|
67
|
+
* agents: {
|
|
68
|
+
* assistant: createAgent({
|
|
69
|
+
* instructions: "You are a helpful assistant.",
|
|
70
|
+
* model: adapter,
|
|
71
|
+
* }),
|
|
72
|
+
* },
|
|
73
|
+
* }),
|
|
74
|
+
* ],
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example Using the raw constructor
|
|
79
|
+
* ```ts
|
|
80
|
+
* const adapter = new DatabricksAdapter({
|
|
81
|
+
* endpointUrl: "https://host/serving-endpoints/my-endpoint/invocations",
|
|
82
|
+
* authenticate: async () => ({ Authorization: `Bearer ${token}` }),
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
var DatabricksAdapter = class DatabricksAdapter {
|
|
87
|
+
streamBody;
|
|
88
|
+
maxSteps;
|
|
89
|
+
maxTokens;
|
|
90
|
+
maxSseLineChars;
|
|
91
|
+
maxStreamTextChars;
|
|
92
|
+
maxToolArgumentsChars;
|
|
93
|
+
constructor(options) {
|
|
94
|
+
this.maxSteps = options.maxSteps ?? 10;
|
|
95
|
+
this.maxTokens = options.maxTokens ?? 4096;
|
|
96
|
+
this.maxSseLineChars = options.maxSseLineChars ?? DEFAULT_MAX_SSE_LINE_CHARS;
|
|
97
|
+
this.maxStreamTextChars = options.maxStreamTextChars ?? DEFAULT_MAX_STREAM_TEXT_CHARS;
|
|
98
|
+
this.maxToolArgumentsChars = options.maxToolArgumentsChars ?? DEFAULT_MAX_TOOL_ARGUMENT_CHARS;
|
|
99
|
+
if (isStreamBodyOptions(options)) this.streamBody = options.streamBody;
|
|
100
|
+
else {
|
|
101
|
+
const { endpointUrl, authenticate } = options;
|
|
102
|
+
this.streamBody = async (body, signal) => {
|
|
103
|
+
const fetchSignal = signal ?? AbortSignal.timeout(RAW_FETCH_DEFAULT_TIMEOUT_MS);
|
|
104
|
+
const authHeaders = await authenticate();
|
|
105
|
+
const response = await fetch(endpointUrl, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
...authHeaders
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
signal: fetchSignal
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
116
|
+
throw new Error(`Databricks API error (${response.status}): ${errorText}`);
|
|
117
|
+
}
|
|
118
|
+
if (!response.body) throw new Error("No response body");
|
|
119
|
+
return response.body;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Creates a DatabricksAdapter for a Databricks Model Serving endpoint.
|
|
125
|
+
*
|
|
126
|
+
* Routes through the shared `connectors/serving/stream` helper, which
|
|
127
|
+
* delegates to the SDK's `apiClient.request({ raw: true })`. That gives the
|
|
128
|
+
* adapter centralised URL encoding + authentication with the rest of the
|
|
129
|
+
* serving surface — no bespoke `fetch()` + `authenticate()` plumbing.
|
|
130
|
+
*/
|
|
131
|
+
static async fromServingEndpoint(options) {
|
|
132
|
+
const { workspaceClient, endpointName, maxSteps, maxTokens, maxSseLineChars, maxStreamTextChars, maxToolArgumentsChars } = options;
|
|
133
|
+
return new DatabricksAdapter({
|
|
134
|
+
streamBody: (body, signal) => stream(workspaceClient, endpointName, body, signal),
|
|
135
|
+
maxSteps,
|
|
136
|
+
maxTokens,
|
|
137
|
+
maxSseLineChars,
|
|
138
|
+
maxStreamTextChars,
|
|
139
|
+
maxToolArgumentsChars
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Creates a DatabricksAdapter from a Model Serving endpoint name.
|
|
144
|
+
* Auto-creates a WorkspaceClient internally. Reads the endpoint name
|
|
145
|
+
* from the argument or the `DATABRICKS_SERVING_ENDPOINT_NAME` env var.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* // Reads endpoint from DATABRICKS_SERVING_ENDPOINT_NAME env var
|
|
150
|
+
* const adapter = await DatabricksAdapter.fromModelServing();
|
|
151
|
+
*
|
|
152
|
+
* // Explicit endpoint
|
|
153
|
+
* const adapter = await DatabricksAdapter.fromModelServing("my-endpoint");
|
|
154
|
+
*
|
|
155
|
+
* // With options
|
|
156
|
+
* const adapter = await DatabricksAdapter.fromModelServing("my-endpoint", {
|
|
157
|
+
* maxSteps: 5,
|
|
158
|
+
* maxTokens: 2048,
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
static async fromModelServing(endpointName, options) {
|
|
163
|
+
const resolvedEndpoint = endpointName ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
|
|
164
|
+
if (!resolvedEndpoint) throw new Error("No endpoint name provided and DATABRICKS_SERVING_ENDPOINT_NAME env var is not set. Pass an endpoint name or set DATABRICKS_SERVING_ENDPOINT_NAME.");
|
|
165
|
+
let workspaceClient = options?.workspaceClient;
|
|
166
|
+
if (!workspaceClient) workspaceClient = new (await (import("@databricks/sdk-experimental"))).WorkspaceClient({});
|
|
167
|
+
return DatabricksAdapter.fromServingEndpoint({
|
|
168
|
+
workspaceClient,
|
|
169
|
+
endpointName: resolvedEndpoint,
|
|
170
|
+
maxSteps: options?.maxSteps,
|
|
171
|
+
maxTokens: options?.maxTokens,
|
|
172
|
+
maxSseLineChars: options?.maxSseLineChars,
|
|
173
|
+
maxStreamTextChars: options?.maxStreamTextChars,
|
|
174
|
+
maxToolArgumentsChars: options?.maxToolArgumentsChars
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async *run(input, context) {
|
|
178
|
+
const nameToWire = /* @__PURE__ */ new Map();
|
|
179
|
+
const wireToName = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const tool of input.tools) {
|
|
181
|
+
const wire = tool.name.replace(/\./g, "__");
|
|
182
|
+
if (wireToName.has(wire) && wireToName.get(wire) !== tool.name) throw new Error(`Tool name collision: '${tool.name}' and '${wireToName.get(wire)}' both map to wire name '${wire}'`);
|
|
183
|
+
nameToWire.set(tool.name, wire);
|
|
184
|
+
wireToName.set(wire, tool.name);
|
|
185
|
+
}
|
|
186
|
+
const tools = this.buildTools(input.tools, nameToWire);
|
|
187
|
+
const messages = this.buildMessages(input.messages, nameToWire);
|
|
188
|
+
yield {
|
|
189
|
+
type: "status",
|
|
190
|
+
status: "running"
|
|
191
|
+
};
|
|
192
|
+
for (let step = 0; step < this.maxSteps; step++) {
|
|
193
|
+
if (context.signal?.aborted) break;
|
|
194
|
+
const { text, toolCalls } = yield* this.streamCompletion(messages, tools, context);
|
|
195
|
+
if (toolCalls.length === 0) {
|
|
196
|
+
const parsed = parseTextToolCalls(text);
|
|
197
|
+
if (parsed.length > 0) {
|
|
198
|
+
yield* this.executeToolCalls(parsed, messages, context, nameToWire);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
messages.push({
|
|
204
|
+
role: "assistant",
|
|
205
|
+
content: text || null,
|
|
206
|
+
tool_calls: toolCalls
|
|
207
|
+
});
|
|
208
|
+
for (const tc of toolCalls) {
|
|
209
|
+
const wireName = tc.function.name;
|
|
210
|
+
const originalName = wireToName.get(wireName) ?? wireName;
|
|
211
|
+
yield* this.executeSingleTool(tc, originalName, messages, context);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Parse wire arguments, emit tool_call / tool_result, append tool messages. */
|
|
216
|
+
async *executeSingleTool(tc, originalName, messages, context) {
|
|
217
|
+
let args;
|
|
218
|
+
try {
|
|
219
|
+
args = JSON.parse(tc.function.arguments);
|
|
220
|
+
} catch {
|
|
221
|
+
args = {};
|
|
222
|
+
}
|
|
223
|
+
yield {
|
|
224
|
+
type: "tool_call",
|
|
225
|
+
callId: tc.id,
|
|
226
|
+
name: originalName,
|
|
227
|
+
args
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
const result = await context.executeTool(originalName, args);
|
|
231
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
232
|
+
yield {
|
|
233
|
+
type: "tool_result",
|
|
234
|
+
callId: tc.id,
|
|
235
|
+
result
|
|
236
|
+
};
|
|
237
|
+
messages.push({
|
|
238
|
+
role: "tool",
|
|
239
|
+
content: resultStr,
|
|
240
|
+
tool_call_id: tc.id
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const errMsg = error instanceof Error ? error.message : "Tool execution failed";
|
|
244
|
+
yield {
|
|
245
|
+
type: "tool_result",
|
|
246
|
+
callId: tc.id,
|
|
247
|
+
result: null,
|
|
248
|
+
error: errMsg
|
|
249
|
+
};
|
|
250
|
+
messages.push({
|
|
251
|
+
role: "tool",
|
|
252
|
+
content: JSON.stringify({ error: errMsg }),
|
|
253
|
+
tool_call_id: tc.id
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async *streamCompletion(messages, tools, context) {
|
|
258
|
+
const body = {
|
|
259
|
+
messages,
|
|
260
|
+
stream: true,
|
|
261
|
+
max_tokens: this.maxTokens
|
|
262
|
+
};
|
|
263
|
+
if (tools.length > 0) body.tools = tools;
|
|
264
|
+
let responseBody;
|
|
265
|
+
try {
|
|
266
|
+
responseBody = await this.streamBody(body, context.signal);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
yield {
|
|
269
|
+
type: "status",
|
|
270
|
+
status: "error",
|
|
271
|
+
error: err instanceof Error ? err.message : "Stream request failed"
|
|
272
|
+
};
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
const reader = responseBody.getReader();
|
|
276
|
+
const decoder = new TextDecoder();
|
|
277
|
+
let buffer = "";
|
|
278
|
+
let fullText = "";
|
|
279
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
280
|
+
try {
|
|
281
|
+
while (true) {
|
|
282
|
+
if (context.signal?.aborted) break;
|
|
283
|
+
const { done, value } = await reader.read();
|
|
284
|
+
if (done) break;
|
|
285
|
+
buffer += decoder.decode(value, { stream: true });
|
|
286
|
+
const lines = buffer.split("\n");
|
|
287
|
+
buffer = lines.pop() ?? "";
|
|
288
|
+
if (buffer.length > this.maxSseLineChars) throw new Error(`DatabricksAdapter: SSE line buffer exceeds configured limit (${this.maxSseLineChars} UTF-16 code units)`);
|
|
289
|
+
for (const line of lines) {
|
|
290
|
+
if (line.length > this.maxSseLineChars) throw new Error(`DatabricksAdapter: SSE line exceeds configured limit (${this.maxSseLineChars} UTF-16 code units)`);
|
|
291
|
+
const trimmed = line.trim();
|
|
292
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
293
|
+
const data = trimmed.slice(6);
|
|
294
|
+
if (data === "[DONE]") continue;
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(data);
|
|
298
|
+
} catch (parseErr) {
|
|
299
|
+
console.debug("[DatabricksAdapter] malformed SSE data line JSON", { line: `${data.slice(0, 256)}${data.length > 256 ? "…" : ""}` }, parseErr);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const deltaUnknown = openAiChoicesDelta(parsed);
|
|
303
|
+
if (!isRecord(deltaUnknown)) continue;
|
|
304
|
+
if (typeof deltaUnknown.content === "string") {
|
|
305
|
+
const content = deltaUnknown.content;
|
|
306
|
+
throwIfExceedsStreamLimit("streamed assistant text", fullText.length, content, this.maxStreamTextChars);
|
|
307
|
+
fullText += content;
|
|
308
|
+
yield {
|
|
309
|
+
type: "message_delta",
|
|
310
|
+
content
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const toolCallsRaw = deltaUnknown.tool_calls;
|
|
314
|
+
if (!Array.isArray(toolCallsRaw)) continue;
|
|
315
|
+
for (const tc of toolCallsRaw) {
|
|
316
|
+
if (!isStreamingDeltaToolCall(tc)) continue;
|
|
317
|
+
const existing = toolCallAccumulator.get(tc.index);
|
|
318
|
+
if (existing) {
|
|
319
|
+
if (tc.function?.arguments) {
|
|
320
|
+
throwIfExceedsStreamLimit("tool call arguments", existing.arguments.length, tc.function.arguments, this.maxToolArgumentsChars);
|
|
321
|
+
existing.arguments += tc.function.arguments;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const initial = tc.function?.arguments ?? "";
|
|
325
|
+
if (initial.length > this.maxToolArgumentsChars) throw new Error(`DatabricksAdapter: tool call arguments exceed configured limit (${this.maxToolArgumentsChars} UTF-16 code units)`);
|
|
326
|
+
toolCallAccumulator.set(tc.index, {
|
|
327
|
+
id: tc.id ?? `call_${tc.index}`,
|
|
328
|
+
name: tc.function?.name ?? "",
|
|
329
|
+
arguments: initial
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} finally {
|
|
336
|
+
try {
|
|
337
|
+
await reader.cancel();
|
|
338
|
+
} catch (cancelErr) {
|
|
339
|
+
console.debug("[DatabricksAdapter] reader.cancel() failed during teardown", cancelErr);
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
reader.releaseLock();
|
|
343
|
+
} catch (unlockErr) {
|
|
344
|
+
console.debug("[DatabricksAdapter] reader.releaseLock() failed during teardown", unlockErr);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const toolCalls = Array.from(toolCallAccumulator.values()).map((tc) => ({
|
|
348
|
+
id: tc.id,
|
|
349
|
+
type: "function",
|
|
350
|
+
function: {
|
|
351
|
+
name: tc.name,
|
|
352
|
+
arguments: tc.arguments || "{}"
|
|
353
|
+
}
|
|
354
|
+
}));
|
|
355
|
+
return {
|
|
356
|
+
text: fullText,
|
|
357
|
+
toolCalls
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async *executeToolCalls(calls, messages, context, nameToWire) {
|
|
361
|
+
const wireToolName = (name) => nameToWire.get(name) ?? name.replace(/\./g, "__");
|
|
362
|
+
const toolCallObjs = calls.map((c, i) => ({
|
|
363
|
+
id: `text_call_${i}`,
|
|
364
|
+
type: "function",
|
|
365
|
+
function: {
|
|
366
|
+
name: wireToolName(c.name),
|
|
367
|
+
arguments: JSON.stringify(c.args)
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
370
|
+
messages.push({
|
|
371
|
+
role: "assistant",
|
|
372
|
+
content: null,
|
|
373
|
+
tool_calls: toolCallObjs
|
|
374
|
+
});
|
|
375
|
+
for (let i = 0; i < toolCallObjs.length; i++) {
|
|
376
|
+
const tc = toolCallObjs[i];
|
|
377
|
+
const originalName = calls[i]?.name ?? tc.function.name;
|
|
378
|
+
yield* this.executeSingleTool(tc, originalName, messages, context);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Maps AppKit {@link AgentInput} messages into OpenAI-compatible wire messages.
|
|
383
|
+
* Preserves multi-turn tool state (`toolCalls` → `tool_calls`, `toolCallId` →
|
|
384
|
+
* `tool_call_id`) so resumed threads and hydrated history reach the model.
|
|
385
|
+
*/
|
|
386
|
+
buildMessages(messages, nameToWire) {
|
|
387
|
+
const wireToolName = (name) => nameToWire.get(name) ?? name.replace(/\./g, "__");
|
|
388
|
+
return messages.map((m) => {
|
|
389
|
+
let content = m.content;
|
|
390
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0 && (!m.content || m.content.trim() === "")) content = null;
|
|
391
|
+
const out = {
|
|
392
|
+
role: m.role,
|
|
393
|
+
content
|
|
394
|
+
};
|
|
395
|
+
if (m.toolCallId) out.tool_call_id = m.toolCallId;
|
|
396
|
+
if (m.toolCalls && m.toolCalls.length > 0) out.tool_calls = m.toolCalls.map((tc) => ({
|
|
397
|
+
id: tc.id,
|
|
398
|
+
type: "function",
|
|
399
|
+
function: {
|
|
400
|
+
name: wireToolName(tc.name),
|
|
401
|
+
arguments: typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args ?? {})
|
|
402
|
+
}
|
|
403
|
+
}));
|
|
404
|
+
return out;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
buildTools(definitions, nameToWire) {
|
|
408
|
+
return definitions.map((def) => ({
|
|
409
|
+
type: "function",
|
|
410
|
+
function: {
|
|
411
|
+
name: nameToWire.get(def.name) ?? def.name,
|
|
412
|
+
description: def.description,
|
|
413
|
+
parameters: def.parameters
|
|
414
|
+
}
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* Parses text-based tool calls from model output.
|
|
420
|
+
*
|
|
421
|
+
* Handles two formats:
|
|
422
|
+
* 1. Llama native: `[{"name": "tool_name", "parameters": {"arg": "val"}}]`
|
|
423
|
+
* 2. Python-style: `[tool_name(arg1='val1', arg2='val2')]`
|
|
424
|
+
*/
|
|
425
|
+
function parseTextToolCalls(text) {
|
|
426
|
+
const trimmed = text.trim();
|
|
427
|
+
const jsonResult = tryParseLlamaJsonToolCalls(trimmed);
|
|
428
|
+
if (jsonResult.length > 0) return jsonResult;
|
|
429
|
+
const pyResult = tryParsePythonStyleToolCalls(trimmed);
|
|
430
|
+
if (pyResult.length > 0) return pyResult;
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
function isLlamaToolJsonItem(value) {
|
|
434
|
+
if (!isRecord(value)) return false;
|
|
435
|
+
return typeof value.name === "string";
|
|
436
|
+
}
|
|
437
|
+
function tryParseLlamaJsonToolCalls(text) {
|
|
438
|
+
const slice = extractLlamaToolJsonSlice(text);
|
|
439
|
+
if (!slice) return [];
|
|
440
|
+
try {
|
|
441
|
+
const parsed = JSON.parse(slice);
|
|
442
|
+
if (!Array.isArray(parsed)) return [];
|
|
443
|
+
return parsed.filter(isLlamaToolJsonItem).map((item) => ({
|
|
444
|
+
name: item.name,
|
|
445
|
+
args: item.parameters ?? item.arguments ?? item.args ?? {}
|
|
446
|
+
}));
|
|
447
|
+
} catch {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function tryParsePythonStyleToolCalls(text) {
|
|
452
|
+
if (text.length > PYTHON_STYLE_TOOL_PARSE_MAX_INPUT) return [];
|
|
453
|
+
const pattern = /\[?([a-zA-Z_][\w.]*)\(([^)]*)\)\]?/g;
|
|
454
|
+
const results = [];
|
|
455
|
+
for (const match of text.matchAll(pattern)) {
|
|
456
|
+
const name = match[1];
|
|
457
|
+
const argsStr = match[2];
|
|
458
|
+
const args = {};
|
|
459
|
+
for (const argMatch of argsStr.matchAll(/(\w+)\s*=\s*(?:'([^']*)'|"([^"]*)"|(\S+))/g)) {
|
|
460
|
+
const key = argMatch[1];
|
|
461
|
+
args[key] = argMatch[2] ?? argMatch[3] ?? argMatch[4];
|
|
462
|
+
}
|
|
463
|
+
results.push({
|
|
464
|
+
name,
|
|
465
|
+
args
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return results;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
//#endregion
|
|
472
|
+
export { DatabricksAdapter, parseTextToolCalls };
|
|
473
|
+
//# sourceMappingURL=databricks.js.map
|