@ekodb/ekodb-client 0.15.2 → 0.16.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/client.d.ts +34 -30
- package/dist/client.js +117 -12
- package/dist/client.test.js +204 -3
- package/dist/functions.d.ts +10 -8
- package/dist/functions.js +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +1 -1
- package/src/client.test.ts +243 -3
- package/src/client.ts +155 -30
- package/src/functions.ts +11 -9
- package/src/index.ts +1 -2
package/dist/client.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { QueryBuilder } from "./query-builder";
|
|
5
5
|
import { SearchQuery, SearchResponse } from "./search";
|
|
6
6
|
import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
|
|
7
|
-
import {
|
|
7
|
+
import { UserFunction, FunctionResult } from "./functions";
|
|
8
8
|
export interface Record {
|
|
9
9
|
[key: string]: any;
|
|
10
10
|
}
|
|
@@ -156,6 +156,7 @@ export interface CreateChatSessionRequest {
|
|
|
156
156
|
llm_provider: string;
|
|
157
157
|
llm_model?: string;
|
|
158
158
|
system_prompt?: string;
|
|
159
|
+
agent_id?: string;
|
|
159
160
|
bypass_ripple?: boolean;
|
|
160
161
|
parent_id?: string;
|
|
161
162
|
branch_point_idx?: number;
|
|
@@ -171,6 +172,9 @@ export interface ChatMessageRequest {
|
|
|
171
172
|
max_iterations?: number;
|
|
172
173
|
tool_config?: ToolConfig;
|
|
173
174
|
llm_model?: string;
|
|
175
|
+
client_tools?: ClientToolDefinition[];
|
|
176
|
+
confirm_tools?: string[];
|
|
177
|
+
exclude_tools?: string[];
|
|
174
178
|
}
|
|
175
179
|
export interface TokenUsage {
|
|
176
180
|
prompt_tokens: number;
|
|
@@ -193,6 +197,7 @@ export interface ChatSession {
|
|
|
193
197
|
llm_model: string;
|
|
194
198
|
collections: CollectionConfig[];
|
|
195
199
|
system_prompt?: string;
|
|
200
|
+
agent_id?: string;
|
|
196
201
|
title?: string;
|
|
197
202
|
message_count: number;
|
|
198
203
|
}
|
|
@@ -285,23 +290,6 @@ export interface RawCompletionRequest {
|
|
|
285
290
|
export interface RawCompletionResponse {
|
|
286
291
|
content: string;
|
|
287
292
|
}
|
|
288
|
-
/**
|
|
289
|
-
* User function definition - reusable sequence of Functions that can be called by Scripts
|
|
290
|
-
*/
|
|
291
|
-
export interface UserFunction {
|
|
292
|
-
label: string;
|
|
293
|
-
name: string;
|
|
294
|
-
description?: string;
|
|
295
|
-
version?: string;
|
|
296
|
-
parameters: {
|
|
297
|
-
[key: string]: ParameterDefinition;
|
|
298
|
-
};
|
|
299
|
-
functions: FunctionStageConfig[];
|
|
300
|
-
tags?: string[];
|
|
301
|
-
id?: string;
|
|
302
|
-
created_at?: string;
|
|
303
|
-
updated_at?: string;
|
|
304
|
-
}
|
|
305
293
|
/**
|
|
306
294
|
* Parameter definition for functions
|
|
307
295
|
*/
|
|
@@ -821,6 +809,11 @@ export declare class EkoDBClient {
|
|
|
821
809
|
* Send a message in an existing chat session
|
|
822
810
|
*/
|
|
823
811
|
chatMessage(sessionId: string, request: ChatMessageRequest): Promise<ChatResponse>;
|
|
812
|
+
/**
|
|
813
|
+
* Submit a client tool result for an in-flight SSE chat stream.
|
|
814
|
+
* Unblocks ekoDB's tool loop so it can feed the result to the LLM.
|
|
815
|
+
*/
|
|
816
|
+
submitChatToolResult(chatId: string, callId: string, success: boolean, result?: any, error?: string): Promise<void>;
|
|
824
817
|
/**
|
|
825
818
|
* Send a message in an existing chat session via SSE streaming.
|
|
826
819
|
*
|
|
@@ -902,29 +895,29 @@ export declare class EkoDBClient {
|
|
|
902
895
|
*/
|
|
903
896
|
getChatMessage(sessionId: string, messageId: string): Promise<Record>;
|
|
904
897
|
/**
|
|
905
|
-
* Save a new
|
|
898
|
+
* Save a new function definition
|
|
906
899
|
*/
|
|
907
|
-
|
|
900
|
+
saveFunction(script: UserFunction): Promise<string>;
|
|
908
901
|
/**
|
|
909
|
-
* Get a
|
|
902
|
+
* Get a function by ID
|
|
910
903
|
*/
|
|
911
|
-
|
|
904
|
+
getFunction(id: string): Promise<UserFunction>;
|
|
912
905
|
/**
|
|
913
|
-
* List all
|
|
906
|
+
* List all functions, optionally filtered by tags
|
|
914
907
|
*/
|
|
915
|
-
|
|
908
|
+
listFunctions(tags?: string[]): Promise<UserFunction[]>;
|
|
916
909
|
/**
|
|
917
|
-
* Update an existing
|
|
910
|
+
* Update an existing function by ID
|
|
918
911
|
*/
|
|
919
|
-
|
|
912
|
+
updateFunction(id: string, script: UserFunction): Promise<void>;
|
|
920
913
|
/**
|
|
921
|
-
* Delete a
|
|
914
|
+
* Delete a function by ID
|
|
922
915
|
*/
|
|
923
|
-
|
|
916
|
+
deleteFunction(id: string): Promise<void>;
|
|
924
917
|
/**
|
|
925
|
-
* Call a saved
|
|
918
|
+
* Call a saved function by ID or label
|
|
926
919
|
*/
|
|
927
|
-
|
|
920
|
+
callFunction(idOrLabel: string, params?: {
|
|
928
921
|
[key: string]: any;
|
|
929
922
|
}): Promise<FunctionResult>;
|
|
930
923
|
/**
|
|
@@ -1058,6 +1051,17 @@ export declare class EkoDBClient {
|
|
|
1058
1051
|
* @returns Number of documents in the collection
|
|
1059
1052
|
*/
|
|
1060
1053
|
countDocuments(collection: string): Promise<number>;
|
|
1054
|
+
/**
|
|
1055
|
+
* Subscribe to collection mutations via SSE (Server-Sent Events).
|
|
1056
|
+
*
|
|
1057
|
+
* Returns an EventStream that emits MutationNotification events.
|
|
1058
|
+
* Use this when WebSocket connections aren't available (e.g. behind
|
|
1059
|
+
* reverse proxies that block WS upgrades).
|
|
1060
|
+
*/
|
|
1061
|
+
subscribeSSE(collection: string, options?: {
|
|
1062
|
+
filterField?: string;
|
|
1063
|
+
filterValue?: string;
|
|
1064
|
+
}): EventStream<MutationNotification>;
|
|
1061
1065
|
/**
|
|
1062
1066
|
* Create a WebSocket client
|
|
1063
1067
|
*/
|
package/dist/client.js
CHANGED
|
@@ -1098,6 +1098,18 @@ class EkoDBClient {
|
|
|
1098
1098
|
async chatMessage(sessionId, request) {
|
|
1099
1099
|
return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
|
|
1100
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Submit a client tool result for an in-flight SSE chat stream.
|
|
1103
|
+
* Unblocks ekoDB's tool loop so it can feed the result to the LLM.
|
|
1104
|
+
*/
|
|
1105
|
+
async submitChatToolResult(chatId, callId, success, result, error) {
|
|
1106
|
+
await this.makeRequest("POST", `/api/chat/${chatId}/tool-result`, {
|
|
1107
|
+
call_id: callId,
|
|
1108
|
+
success,
|
|
1109
|
+
...(result !== undefined && { result }),
|
|
1110
|
+
...(error !== undefined && { error }),
|
|
1111
|
+
}, 0, true);
|
|
1112
|
+
}
|
|
1101
1113
|
/**
|
|
1102
1114
|
* Send a message in an existing chat session via SSE streaming.
|
|
1103
1115
|
*
|
|
@@ -1307,41 +1319,41 @@ class EkoDBClient {
|
|
|
1307
1319
|
// SCRIPTS API
|
|
1308
1320
|
// ========================================================================
|
|
1309
1321
|
/**
|
|
1310
|
-
* Save a new
|
|
1322
|
+
* Save a new function definition
|
|
1311
1323
|
*/
|
|
1312
|
-
async
|
|
1324
|
+
async saveFunction(script) {
|
|
1313
1325
|
const result = await this.makeRequest("POST", "/api/functions", script);
|
|
1314
1326
|
return result.id;
|
|
1315
1327
|
}
|
|
1316
1328
|
/**
|
|
1317
|
-
* Get a
|
|
1329
|
+
* Get a function by ID
|
|
1318
1330
|
*/
|
|
1319
|
-
async
|
|
1331
|
+
async getFunction(id) {
|
|
1320
1332
|
return this.makeRequest("GET", `/api/functions/${id}`);
|
|
1321
1333
|
}
|
|
1322
1334
|
/**
|
|
1323
|
-
* List all
|
|
1335
|
+
* List all functions, optionally filtered by tags
|
|
1324
1336
|
*/
|
|
1325
|
-
async
|
|
1337
|
+
async listFunctions(tags) {
|
|
1326
1338
|
const params = tags ? `?tags=${tags.join(",")}` : "";
|
|
1327
1339
|
return this.makeRequest("GET", `/api/functions${params}`);
|
|
1328
1340
|
}
|
|
1329
1341
|
/**
|
|
1330
|
-
* Update an existing
|
|
1342
|
+
* Update an existing function by ID
|
|
1331
1343
|
*/
|
|
1332
|
-
async
|
|
1344
|
+
async updateFunction(id, script) {
|
|
1333
1345
|
await this.makeRequest("PUT", `/api/functions/${id}`, script);
|
|
1334
1346
|
}
|
|
1335
1347
|
/**
|
|
1336
|
-
* Delete a
|
|
1348
|
+
* Delete a function by ID
|
|
1337
1349
|
*/
|
|
1338
|
-
async
|
|
1350
|
+
async deleteFunction(id) {
|
|
1339
1351
|
await this.makeRequest("DELETE", `/api/functions/${id}`);
|
|
1340
1352
|
}
|
|
1341
1353
|
/**
|
|
1342
|
-
* Call a saved
|
|
1354
|
+
* Call a saved function by ID or label
|
|
1343
1355
|
*/
|
|
1344
|
-
async
|
|
1356
|
+
async callFunction(idOrLabel, params) {
|
|
1345
1357
|
return this.makeRequest("POST", `/api/functions/${idOrLabel}`, params || {});
|
|
1346
1358
|
}
|
|
1347
1359
|
// ========================================================================
|
|
@@ -1613,6 +1625,99 @@ class EkoDBClient {
|
|
|
1613
1625
|
const records = await this.find(collection, query);
|
|
1614
1626
|
return records.length;
|
|
1615
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Subscribe to collection mutations via SSE (Server-Sent Events).
|
|
1630
|
+
*
|
|
1631
|
+
* Returns an EventStream that emits MutationNotification events.
|
|
1632
|
+
* Use this when WebSocket connections aren't available (e.g. behind
|
|
1633
|
+
* reverse proxies that block WS upgrades).
|
|
1634
|
+
*/
|
|
1635
|
+
subscribeSSE(collection, options) {
|
|
1636
|
+
const stream = new EventStream();
|
|
1637
|
+
(async () => {
|
|
1638
|
+
try {
|
|
1639
|
+
let token = await this.getToken();
|
|
1640
|
+
if (!token) {
|
|
1641
|
+
await this.refreshToken();
|
|
1642
|
+
token = await this.getToken();
|
|
1643
|
+
}
|
|
1644
|
+
let url = `${this.baseURL}/api/subscribe/${encodeURIComponent(collection)}`;
|
|
1645
|
+
const params = [];
|
|
1646
|
+
if (options?.filterField)
|
|
1647
|
+
params.push(`filter_field=${encodeURIComponent(options.filterField)}`);
|
|
1648
|
+
if (options?.filterValue)
|
|
1649
|
+
params.push(`filter_value=${encodeURIComponent(options.filterValue)}`);
|
|
1650
|
+
if (params.length > 0)
|
|
1651
|
+
url += `?${params.join("&")}`;
|
|
1652
|
+
const response = await fetch(url, {
|
|
1653
|
+
method: "GET",
|
|
1654
|
+
headers: {
|
|
1655
|
+
Accept: "text/event-stream",
|
|
1656
|
+
Authorization: `Bearer ${token}`,
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
if (!response.ok) {
|
|
1660
|
+
const body = await response.text();
|
|
1661
|
+
stream.emit("error", `SSE subscribe failed (${response.status}): ${body}`);
|
|
1662
|
+
stream.close();
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const reader = response.body?.getReader();
|
|
1666
|
+
if (!reader) {
|
|
1667
|
+
stream.emit("error", "SSE subscribe failed: streaming not supported");
|
|
1668
|
+
stream.close();
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const decoder = new TextDecoder("utf-8");
|
|
1672
|
+
let buffer = "";
|
|
1673
|
+
let eventType = "";
|
|
1674
|
+
let dataLines = [];
|
|
1675
|
+
while (!stream.closed) {
|
|
1676
|
+
const { value, done } = await reader.read();
|
|
1677
|
+
if (done)
|
|
1678
|
+
break;
|
|
1679
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1680
|
+
const lines = buffer.split("\n");
|
|
1681
|
+
buffer = lines.pop() ?? "";
|
|
1682
|
+
for (const line of lines) {
|
|
1683
|
+
if (line === "") {
|
|
1684
|
+
// End of event block
|
|
1685
|
+
if (eventType === "mutation" && dataLines.length > 0) {
|
|
1686
|
+
try {
|
|
1687
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
1688
|
+
stream.emit("event", {
|
|
1689
|
+
collection: payload.collection,
|
|
1690
|
+
event: payload.event,
|
|
1691
|
+
recordIds: payload.record_ids,
|
|
1692
|
+
records: payload.records,
|
|
1693
|
+
timestamp: payload.timestamp,
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
// skip malformed data
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
eventType = "";
|
|
1701
|
+
dataLines = [];
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (line.startsWith("event: ")) {
|
|
1705
|
+
eventType = line.slice(7).trim();
|
|
1706
|
+
}
|
|
1707
|
+
else if (line.startsWith("data: ")) {
|
|
1708
|
+
dataLines.push(line.slice(6).trim());
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
stream.close();
|
|
1713
|
+
}
|
|
1714
|
+
catch (err) {
|
|
1715
|
+
stream.emit("error", err.message ?? String(err));
|
|
1716
|
+
stream.close();
|
|
1717
|
+
}
|
|
1718
|
+
})();
|
|
1719
|
+
return stream;
|
|
1720
|
+
}
|
|
1616
1721
|
/**
|
|
1617
1722
|
* Create a WebSocket client
|
|
1618
1723
|
*/
|
package/dist/client.test.js
CHANGED
|
@@ -590,14 +590,14 @@ function mockErrorResponse(status, message) {
|
|
|
590
590
|
parameters: {},
|
|
591
591
|
functions: [],
|
|
592
592
|
};
|
|
593
|
-
const result = await client.
|
|
593
|
+
const result = await client.saveFunction(script);
|
|
594
594
|
(0, vitest_1.expect)(result).toBeDefined();
|
|
595
595
|
});
|
|
596
596
|
(0, vitest_1.it)("gets script by ID", async () => {
|
|
597
597
|
const client = createTestClient();
|
|
598
598
|
mockTokenResponse();
|
|
599
599
|
mockJsonResponse({ id: "func_123", label: "my_function" });
|
|
600
|
-
const result = await client.
|
|
600
|
+
const result = await client.getFunction("func_123");
|
|
601
601
|
(0, vitest_1.expect)(result).toBeDefined();
|
|
602
602
|
});
|
|
603
603
|
(0, vitest_1.it)("updates script", async () => {
|
|
@@ -610,7 +610,7 @@ function mockErrorResponse(status, message) {
|
|
|
610
610
|
parameters: {},
|
|
611
611
|
functions: [],
|
|
612
612
|
};
|
|
613
|
-
await (0, vitest_1.expect)(client.
|
|
613
|
+
await (0, vitest_1.expect)(client.updateFunction("func_123", script)).resolves.not.toThrow();
|
|
614
614
|
});
|
|
615
615
|
});
|
|
616
616
|
(0, vitest_1.describe)("EkoDBClient chat advanced", () => {
|
|
@@ -2023,3 +2023,204 @@ function mockErrorResponse(status, message) {
|
|
|
2023
2023
|
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
|
|
2024
2024
|
});
|
|
2025
2025
|
});
|
|
2026
|
+
// ============================================================================
|
|
2027
|
+
// agent_id Tests
|
|
2028
|
+
// ============================================================================
|
|
2029
|
+
(0, vitest_1.describe)("agent_id on chat types", () => {
|
|
2030
|
+
(0, vitest_1.it)("CreateChatSessionRequest includes agent_id", () => {
|
|
2031
|
+
const req = {
|
|
2032
|
+
collections: [{ collection_name: "docs" }],
|
|
2033
|
+
llm_provider: "openai",
|
|
2034
|
+
agent_id: "my-agent",
|
|
2035
|
+
};
|
|
2036
|
+
(0, vitest_1.expect)(req.agent_id).toBe("my-agent");
|
|
2037
|
+
});
|
|
2038
|
+
(0, vitest_1.it)("CreateChatSessionRequest omits agent_id when undefined", () => {
|
|
2039
|
+
const req = {
|
|
2040
|
+
collections: [],
|
|
2041
|
+
llm_provider: "openai",
|
|
2042
|
+
};
|
|
2043
|
+
(0, vitest_1.expect)(req.agent_id).toBeUndefined();
|
|
2044
|
+
});
|
|
2045
|
+
(0, vitest_1.it)("ChatSession includes agent_id", () => {
|
|
2046
|
+
const session = {
|
|
2047
|
+
chat_id: "c1",
|
|
2048
|
+
created_at: "2026-01-01",
|
|
2049
|
+
updated_at: "2026-01-01",
|
|
2050
|
+
llm_provider: "openai",
|
|
2051
|
+
llm_model: "gpt-4",
|
|
2052
|
+
collections: [],
|
|
2053
|
+
agent_id: "bot-1",
|
|
2054
|
+
message_count: 0,
|
|
2055
|
+
};
|
|
2056
|
+
(0, vitest_1.expect)(session.agent_id).toBe("bot-1");
|
|
2057
|
+
});
|
|
2058
|
+
(0, vitest_1.it)("ChatSession allows missing agent_id", () => {
|
|
2059
|
+
const session = {
|
|
2060
|
+
chat_id: "c1",
|
|
2061
|
+
created_at: "2026-01-01",
|
|
2062
|
+
updated_at: "2026-01-01",
|
|
2063
|
+
llm_provider: "openai",
|
|
2064
|
+
llm_model: "gpt-4",
|
|
2065
|
+
collections: [],
|
|
2066
|
+
message_count: 0,
|
|
2067
|
+
};
|
|
2068
|
+
(0, vitest_1.expect)(session.agent_id).toBeUndefined();
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
// ============================================================================
|
|
2072
|
+
// client_tools / confirm_tools / exclude_tools Tests
|
|
2073
|
+
// ============================================================================
|
|
2074
|
+
(0, vitest_1.describe)("ChatMessageRequest tool fields", () => {
|
|
2075
|
+
(0, vitest_1.it)("includes client_tools, confirm_tools, exclude_tools", () => {
|
|
2076
|
+
const req = {
|
|
2077
|
+
message: "hello",
|
|
2078
|
+
client_tools: [
|
|
2079
|
+
{
|
|
2080
|
+
name: "weather",
|
|
2081
|
+
description: "Get weather",
|
|
2082
|
+
parameters: { type: "object" },
|
|
2083
|
+
},
|
|
2084
|
+
],
|
|
2085
|
+
confirm_tools: ["shell_exec"],
|
|
2086
|
+
exclude_tools: ["file_delete"],
|
|
2087
|
+
};
|
|
2088
|
+
(0, vitest_1.expect)(req.client_tools).toHaveLength(1);
|
|
2089
|
+
(0, vitest_1.expect)(req.client_tools[0].name).toBe("weather");
|
|
2090
|
+
(0, vitest_1.expect)(req.confirm_tools).toEqual(["shell_exec"]);
|
|
2091
|
+
(0, vitest_1.expect)(req.exclude_tools).toEqual(["file_delete"]);
|
|
2092
|
+
});
|
|
2093
|
+
(0, vitest_1.it)("tool fields are optional", () => {
|
|
2094
|
+
const req = { message: "hi" };
|
|
2095
|
+
(0, vitest_1.expect)(req.client_tools).toBeUndefined();
|
|
2096
|
+
(0, vitest_1.expect)(req.confirm_tools).toBeUndefined();
|
|
2097
|
+
(0, vitest_1.expect)(req.exclude_tools).toBeUndefined();
|
|
2098
|
+
});
|
|
2099
|
+
(0, vitest_1.it)("ClientToolDefinition has correct shape", () => {
|
|
2100
|
+
const tool = {
|
|
2101
|
+
name: "calc",
|
|
2102
|
+
description: "Calculator",
|
|
2103
|
+
parameters: { type: "object", properties: {} },
|
|
2104
|
+
};
|
|
2105
|
+
(0, vitest_1.expect)(tool.name).toBe("calc");
|
|
2106
|
+
(0, vitest_1.expect)(tool.description).toBe("Calculator");
|
|
2107
|
+
(0, vitest_1.expect)(tool.parameters.type).toBe("object");
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
// ============================================================================
|
|
2111
|
+
// submitChatToolResult Tests
|
|
2112
|
+
// ============================================================================
|
|
2113
|
+
(0, vitest_1.describe)("submitChatToolResult", () => {
|
|
2114
|
+
(0, vitest_1.it)("sends tool result to correct endpoint", async () => {
|
|
2115
|
+
const client = createTestClient();
|
|
2116
|
+
mockTokenResponse();
|
|
2117
|
+
mockJsonResponse({});
|
|
2118
|
+
await client.submitChatToolResult("chat-123", "call-456", true, {
|
|
2119
|
+
temp: "72F",
|
|
2120
|
+
});
|
|
2121
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
|
|
2122
|
+
const call = mockFetch.mock.calls[1];
|
|
2123
|
+
(0, vitest_1.expect)(call[0]).toContain("/api/chat/chat-123/tool-result");
|
|
2124
|
+
const body = JSON.parse(call[1].body);
|
|
2125
|
+
(0, vitest_1.expect)(body.call_id).toBe("call-456");
|
|
2126
|
+
(0, vitest_1.expect)(body.success).toBe(true);
|
|
2127
|
+
(0, vitest_1.expect)(body.result.temp).toBe("72F");
|
|
2128
|
+
});
|
|
2129
|
+
(0, vitest_1.it)("sends error result", async () => {
|
|
2130
|
+
const client = createTestClient();
|
|
2131
|
+
mockTokenResponse();
|
|
2132
|
+
mockJsonResponse({});
|
|
2133
|
+
await client.submitChatToolResult("chat-123", "call-456", false, undefined, "tool crashed");
|
|
2134
|
+
const call = mockFetch.mock.calls[1];
|
|
2135
|
+
const body = JSON.parse(call[1].body);
|
|
2136
|
+
(0, vitest_1.expect)(body.success).toBe(false);
|
|
2137
|
+
(0, vitest_1.expect)(body.error).toBe("tool crashed");
|
|
2138
|
+
(0, vitest_1.expect)(body.result).toBeUndefined();
|
|
2139
|
+
});
|
|
2140
|
+
});
|
|
2141
|
+
// ============================================================================
|
|
2142
|
+
// subscribeSSE Tests
|
|
2143
|
+
// ============================================================================
|
|
2144
|
+
(0, vitest_1.describe)("subscribeSSE", () => {
|
|
2145
|
+
/** Create a mock ReadableStream from a string */
|
|
2146
|
+
function mockReadableStream(data) {
|
|
2147
|
+
const encoder = new TextEncoder();
|
|
2148
|
+
const bytes = encoder.encode(data);
|
|
2149
|
+
let sent = false;
|
|
2150
|
+
return {
|
|
2151
|
+
getReader: () => ({
|
|
2152
|
+
read: async () => {
|
|
2153
|
+
if (!sent) {
|
|
2154
|
+
sent = true;
|
|
2155
|
+
return { value: bytes, done: false };
|
|
2156
|
+
}
|
|
2157
|
+
return { value: undefined, done: true };
|
|
2158
|
+
},
|
|
2159
|
+
}),
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
(0, vitest_1.it)("parses mutation events from SSE stream", async () => {
|
|
2163
|
+
const client = createTestClient();
|
|
2164
|
+
mockTokenResponse();
|
|
2165
|
+
const sseBody = "event: subscribed\ndata: {}\n\n" +
|
|
2166
|
+
'event: mutation\ndata: {"collection":"orders","event":"insert","record_ids":["r1"],"timestamp":"t1"}\n\n' +
|
|
2167
|
+
'event: mutation\ndata: {"collection":"orders","event":"update","record_ids":["r2"],"timestamp":"t2"}\n\n';
|
|
2168
|
+
mockFetch.mockResolvedValueOnce({
|
|
2169
|
+
ok: true,
|
|
2170
|
+
status: 200,
|
|
2171
|
+
body: mockReadableStream(sseBody),
|
|
2172
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2173
|
+
});
|
|
2174
|
+
const stream = client.subscribeSSE("orders");
|
|
2175
|
+
const events = [];
|
|
2176
|
+
await new Promise((resolve) => {
|
|
2177
|
+
stream.on("event", (e) => events.push(e));
|
|
2178
|
+
stream.on("close", resolve);
|
|
2179
|
+
setTimeout(resolve, 100);
|
|
2180
|
+
});
|
|
2181
|
+
(0, vitest_1.expect)(events).toHaveLength(2);
|
|
2182
|
+
(0, vitest_1.expect)(events[0].event).toBe("insert");
|
|
2183
|
+
(0, vitest_1.expect)(events[0].recordIds).toEqual(["r1"]);
|
|
2184
|
+
(0, vitest_1.expect)(events[1].event).toBe("update");
|
|
2185
|
+
(0, vitest_1.expect)(events[1].recordIds).toEqual(["r2"]);
|
|
2186
|
+
});
|
|
2187
|
+
(0, vitest_1.it)("passes filter params in URL", async () => {
|
|
2188
|
+
const client = createTestClient();
|
|
2189
|
+
mockTokenResponse();
|
|
2190
|
+
mockFetch.mockResolvedValueOnce({
|
|
2191
|
+
ok: true,
|
|
2192
|
+
status: 200,
|
|
2193
|
+
body: mockReadableStream(""),
|
|
2194
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2195
|
+
});
|
|
2196
|
+
client.subscribeSSE("orders", {
|
|
2197
|
+
filterField: "status",
|
|
2198
|
+
filterValue: "active",
|
|
2199
|
+
});
|
|
2200
|
+
// Wait for async fetch
|
|
2201
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2202
|
+
const call = mockFetch.mock.calls[1];
|
|
2203
|
+
(0, vitest_1.expect)(call[0]).toContain("/api/subscribe/orders");
|
|
2204
|
+
(0, vitest_1.expect)(call[0]).toContain("filter_field=status");
|
|
2205
|
+
(0, vitest_1.expect)(call[0]).toContain("filter_value=active");
|
|
2206
|
+
});
|
|
2207
|
+
(0, vitest_1.it)("emits error on HTTP failure", async () => {
|
|
2208
|
+
const client = createTestClient();
|
|
2209
|
+
mockTokenResponse();
|
|
2210
|
+
mockFetch.mockResolvedValueOnce({
|
|
2211
|
+
ok: false,
|
|
2212
|
+
status: 401,
|
|
2213
|
+
text: async () => "Unauthorized",
|
|
2214
|
+
headers: new Headers(),
|
|
2215
|
+
});
|
|
2216
|
+
const stream = client.subscribeSSE("orders");
|
|
2217
|
+
const errors = [];
|
|
2218
|
+
await new Promise((resolve) => {
|
|
2219
|
+
stream.on("error", (e) => errors.push(e));
|
|
2220
|
+
stream.on("close", resolve);
|
|
2221
|
+
setTimeout(resolve, 100);
|
|
2222
|
+
});
|
|
2223
|
+
(0, vitest_1.expect)(errors).toHaveLength(1);
|
|
2224
|
+
(0, vitest_1.expect)(errors[0]).toContain("401");
|
|
2225
|
+
});
|
|
2226
|
+
});
|
package/dist/functions.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Functions API for ekoDB TypeScript client
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
/** A reusable sequence of Functions stored in ekoDB. */
|
|
5
|
+
export interface UserFunction {
|
|
6
|
+
id?: string;
|
|
5
7
|
label: string;
|
|
6
8
|
name: string;
|
|
7
9
|
description?: string;
|
|
@@ -140,7 +142,7 @@ export type FunctionStageConfig = {
|
|
|
140
142
|
value: any;
|
|
141
143
|
} | {
|
|
142
144
|
type: "If";
|
|
143
|
-
condition:
|
|
145
|
+
condition: FunctionCondition;
|
|
144
146
|
then_functions: FunctionStageConfig[];
|
|
145
147
|
else_functions?: FunctionStageConfig[];
|
|
146
148
|
} | {
|
|
@@ -224,7 +226,7 @@ export interface SortFieldConfig {
|
|
|
224
226
|
field: string;
|
|
225
227
|
ascending: boolean;
|
|
226
228
|
}
|
|
227
|
-
export type
|
|
229
|
+
export type FunctionCondition = {
|
|
228
230
|
type: "FieldEquals";
|
|
229
231
|
value: {
|
|
230
232
|
field: string;
|
|
@@ -255,17 +257,17 @@ export type ScriptCondition = {
|
|
|
255
257
|
} | {
|
|
256
258
|
type: "And";
|
|
257
259
|
value: {
|
|
258
|
-
conditions:
|
|
260
|
+
conditions: FunctionCondition[];
|
|
259
261
|
};
|
|
260
262
|
} | {
|
|
261
263
|
type: "Or";
|
|
262
264
|
value: {
|
|
263
|
-
conditions:
|
|
265
|
+
conditions: FunctionCondition[];
|
|
264
266
|
};
|
|
265
267
|
} | {
|
|
266
268
|
type: "Not";
|
|
267
269
|
value: {
|
|
268
|
-
condition:
|
|
270
|
+
condition: FunctionCondition;
|
|
269
271
|
};
|
|
270
272
|
};
|
|
271
273
|
export interface FunctionResult {
|
|
@@ -314,7 +316,7 @@ export declare const Stage: {
|
|
|
314
316
|
embed: (input_field: string, output_field: string, model?: string) => FunctionStageConfig;
|
|
315
317
|
findById: (collection: string, record_id: string) => FunctionStageConfig;
|
|
316
318
|
findOne: (collection: string, key: string, value: any) => FunctionStageConfig;
|
|
317
|
-
if: (condition:
|
|
319
|
+
if: (condition: FunctionCondition, thenFunctions: FunctionStageConfig[], elseFunctions?: FunctionStageConfig[]) => FunctionStageConfig;
|
|
318
320
|
forEach: (functions: FunctionStageConfig[]) => FunctionStageConfig;
|
|
319
321
|
callFunction: (function_label: string, params?: Record<string, any>) => FunctionStageConfig;
|
|
320
322
|
findOneAndUpdate: (collection: string, record_id: string, updates: Record<string, any>, bypassRipple?: boolean, ttl?: number) => FunctionStageConfig;
|
package/dist/functions.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,6 @@ export type { WrappedFieldValue } from "./utils";
|
|
|
9
9
|
export type { SearchQuery, SearchResult, SearchResponse } from "./search";
|
|
10
10
|
export type { Schema, FieldTypeSchema, IndexConfig, CollectionMetadata, } from "./schema";
|
|
11
11
|
export type { JoinConfig } from "./join";
|
|
12
|
-
export type {
|
|
12
|
+
export type { UserFunction, ParameterDefinition, FunctionStageConfig, GroupFunctionConfig, SortFieldConfig, FunctionResult, FunctionStats, StageStats, } from "./functions";
|
|
13
13
|
export type { MutationNotification, ChatStreamEvent, ClientToolDefinition, ChatSendOptions, SubscribeOptions, } from "./client";
|
|
14
|
-
export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse,
|
|
14
|
+
export type { Record, Query, BatchOperationResult, ClientConfig, RateLimitInfo, CollectionConfig, ChatRequest, CreateChatSessionRequest, ChatMessageRequest, TokenUsage, ChatResponse, ChatSession, ChatSessionResponse, ListSessionsQuery, ListSessionsResponse, GetMessagesQuery, GetMessagesResponse, UpdateSessionRequest, MergeSessionsRequest, ChatModels, EmbedRequest, EmbedResponse, RawCompletionRequest, RawCompletionResponse, ToolChoice, ToolConfig, } from "./client";
|
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -800,7 +800,7 @@ describe("EkoDBClient scripts advanced", () => {
|
|
|
800
800
|
parameters: {},
|
|
801
801
|
functions: [],
|
|
802
802
|
};
|
|
803
|
-
const result = await client.
|
|
803
|
+
const result = await client.saveFunction(script);
|
|
804
804
|
|
|
805
805
|
expect(result).toBeDefined();
|
|
806
806
|
});
|
|
@@ -811,7 +811,7 @@ describe("EkoDBClient scripts advanced", () => {
|
|
|
811
811
|
mockTokenResponse();
|
|
812
812
|
mockJsonResponse({ id: "func_123", label: "my_function" });
|
|
813
813
|
|
|
814
|
-
const result = await client.
|
|
814
|
+
const result = await client.getFunction("func_123");
|
|
815
815
|
|
|
816
816
|
expect(result).toBeDefined();
|
|
817
817
|
});
|
|
@@ -829,7 +829,7 @@ describe("EkoDBClient scripts advanced", () => {
|
|
|
829
829
|
functions: [],
|
|
830
830
|
};
|
|
831
831
|
await expect(
|
|
832
|
-
client.
|
|
832
|
+
client.updateFunction("func_123", script),
|
|
833
833
|
).resolves.not.toThrow();
|
|
834
834
|
});
|
|
835
835
|
});
|
|
@@ -2674,3 +2674,243 @@ describe("EkoDBClient auth token management", () => {
|
|
|
2674
2674
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2675
2675
|
});
|
|
2676
2676
|
});
|
|
2677
|
+
|
|
2678
|
+
// ============================================================================
|
|
2679
|
+
// agent_id Tests
|
|
2680
|
+
// ============================================================================
|
|
2681
|
+
|
|
2682
|
+
describe("agent_id on chat types", () => {
|
|
2683
|
+
it("CreateChatSessionRequest includes agent_id", () => {
|
|
2684
|
+
const req: any = {
|
|
2685
|
+
collections: [{ collection_name: "docs" }],
|
|
2686
|
+
llm_provider: "openai",
|
|
2687
|
+
agent_id: "my-agent",
|
|
2688
|
+
};
|
|
2689
|
+
expect(req.agent_id).toBe("my-agent");
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2692
|
+
it("CreateChatSessionRequest omits agent_id when undefined", () => {
|
|
2693
|
+
const req: any = {
|
|
2694
|
+
collections: [],
|
|
2695
|
+
llm_provider: "openai",
|
|
2696
|
+
};
|
|
2697
|
+
expect(req.agent_id).toBeUndefined();
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
it("ChatSession includes agent_id", () => {
|
|
2701
|
+
const session: any = {
|
|
2702
|
+
chat_id: "c1",
|
|
2703
|
+
created_at: "2026-01-01",
|
|
2704
|
+
updated_at: "2026-01-01",
|
|
2705
|
+
llm_provider: "openai",
|
|
2706
|
+
llm_model: "gpt-4",
|
|
2707
|
+
collections: [],
|
|
2708
|
+
agent_id: "bot-1",
|
|
2709
|
+
message_count: 0,
|
|
2710
|
+
};
|
|
2711
|
+
expect(session.agent_id).toBe("bot-1");
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
it("ChatSession allows missing agent_id", () => {
|
|
2715
|
+
const session: any = {
|
|
2716
|
+
chat_id: "c1",
|
|
2717
|
+
created_at: "2026-01-01",
|
|
2718
|
+
updated_at: "2026-01-01",
|
|
2719
|
+
llm_provider: "openai",
|
|
2720
|
+
llm_model: "gpt-4",
|
|
2721
|
+
collections: [],
|
|
2722
|
+
message_count: 0,
|
|
2723
|
+
};
|
|
2724
|
+
expect(session.agent_id).toBeUndefined();
|
|
2725
|
+
});
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
// ============================================================================
|
|
2729
|
+
// client_tools / confirm_tools / exclude_tools Tests
|
|
2730
|
+
// ============================================================================
|
|
2731
|
+
|
|
2732
|
+
describe("ChatMessageRequest tool fields", () => {
|
|
2733
|
+
it("includes client_tools, confirm_tools, exclude_tools", () => {
|
|
2734
|
+
const req: any = {
|
|
2735
|
+
message: "hello",
|
|
2736
|
+
client_tools: [
|
|
2737
|
+
{
|
|
2738
|
+
name: "weather",
|
|
2739
|
+
description: "Get weather",
|
|
2740
|
+
parameters: { type: "object" },
|
|
2741
|
+
},
|
|
2742
|
+
],
|
|
2743
|
+
confirm_tools: ["shell_exec"],
|
|
2744
|
+
exclude_tools: ["file_delete"],
|
|
2745
|
+
};
|
|
2746
|
+
expect(req.client_tools).toHaveLength(1);
|
|
2747
|
+
expect(req.client_tools[0].name).toBe("weather");
|
|
2748
|
+
expect(req.confirm_tools).toEqual(["shell_exec"]);
|
|
2749
|
+
expect(req.exclude_tools).toEqual(["file_delete"]);
|
|
2750
|
+
});
|
|
2751
|
+
|
|
2752
|
+
it("tool fields are optional", () => {
|
|
2753
|
+
const req: any = { message: "hi" };
|
|
2754
|
+
expect(req.client_tools).toBeUndefined();
|
|
2755
|
+
expect(req.confirm_tools).toBeUndefined();
|
|
2756
|
+
expect(req.exclude_tools).toBeUndefined();
|
|
2757
|
+
});
|
|
2758
|
+
|
|
2759
|
+
it("ClientToolDefinition has correct shape", () => {
|
|
2760
|
+
const tool: any = {
|
|
2761
|
+
name: "calc",
|
|
2762
|
+
description: "Calculator",
|
|
2763
|
+
parameters: { type: "object", properties: {} },
|
|
2764
|
+
};
|
|
2765
|
+
expect(tool.name).toBe("calc");
|
|
2766
|
+
expect(tool.description).toBe("Calculator");
|
|
2767
|
+
expect(tool.parameters.type).toBe("object");
|
|
2768
|
+
});
|
|
2769
|
+
});
|
|
2770
|
+
|
|
2771
|
+
// ============================================================================
|
|
2772
|
+
// submitChatToolResult Tests
|
|
2773
|
+
// ============================================================================
|
|
2774
|
+
|
|
2775
|
+
describe("submitChatToolResult", () => {
|
|
2776
|
+
it("sends tool result to correct endpoint", async () => {
|
|
2777
|
+
const client = createTestClient();
|
|
2778
|
+
mockTokenResponse();
|
|
2779
|
+
mockJsonResponse({});
|
|
2780
|
+
|
|
2781
|
+
await client.submitChatToolResult("chat-123", "call-456", true, {
|
|
2782
|
+
temp: "72F",
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2786
|
+
const call = mockFetch.mock.calls[1];
|
|
2787
|
+
expect(call[0]).toContain("/api/chat/chat-123/tool-result");
|
|
2788
|
+
const body = JSON.parse(call[1].body);
|
|
2789
|
+
expect(body.call_id).toBe("call-456");
|
|
2790
|
+
expect(body.success).toBe(true);
|
|
2791
|
+
expect(body.result.temp).toBe("72F");
|
|
2792
|
+
});
|
|
2793
|
+
|
|
2794
|
+
it("sends error result", async () => {
|
|
2795
|
+
const client = createTestClient();
|
|
2796
|
+
mockTokenResponse();
|
|
2797
|
+
mockJsonResponse({});
|
|
2798
|
+
|
|
2799
|
+
await client.submitChatToolResult(
|
|
2800
|
+
"chat-123",
|
|
2801
|
+
"call-456",
|
|
2802
|
+
false,
|
|
2803
|
+
undefined,
|
|
2804
|
+
"tool crashed",
|
|
2805
|
+
);
|
|
2806
|
+
|
|
2807
|
+
const call = mockFetch.mock.calls[1];
|
|
2808
|
+
const body = JSON.parse(call[1].body);
|
|
2809
|
+
expect(body.success).toBe(false);
|
|
2810
|
+
expect(body.error).toBe("tool crashed");
|
|
2811
|
+
expect(body.result).toBeUndefined();
|
|
2812
|
+
});
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
// ============================================================================
|
|
2816
|
+
// subscribeSSE Tests
|
|
2817
|
+
// ============================================================================
|
|
2818
|
+
|
|
2819
|
+
describe("subscribeSSE", () => {
|
|
2820
|
+
/** Create a mock ReadableStream from a string */
|
|
2821
|
+
function mockReadableStream(data: string) {
|
|
2822
|
+
const encoder = new TextEncoder();
|
|
2823
|
+
const bytes = encoder.encode(data);
|
|
2824
|
+
let sent = false;
|
|
2825
|
+
return {
|
|
2826
|
+
getReader: () => ({
|
|
2827
|
+
read: async () => {
|
|
2828
|
+
if (!sent) {
|
|
2829
|
+
sent = true;
|
|
2830
|
+
return { value: bytes, done: false };
|
|
2831
|
+
}
|
|
2832
|
+
return { value: undefined, done: true };
|
|
2833
|
+
},
|
|
2834
|
+
}),
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
it("parses mutation events from SSE stream", async () => {
|
|
2839
|
+
const client = createTestClient();
|
|
2840
|
+
mockTokenResponse();
|
|
2841
|
+
|
|
2842
|
+
const sseBody =
|
|
2843
|
+
"event: subscribed\ndata: {}\n\n" +
|
|
2844
|
+
'event: mutation\ndata: {"collection":"orders","event":"insert","record_ids":["r1"],"timestamp":"t1"}\n\n' +
|
|
2845
|
+
'event: mutation\ndata: {"collection":"orders","event":"update","record_ids":["r2"],"timestamp":"t2"}\n\n';
|
|
2846
|
+
|
|
2847
|
+
mockFetch.mockResolvedValueOnce({
|
|
2848
|
+
ok: true,
|
|
2849
|
+
status: 200,
|
|
2850
|
+
body: mockReadableStream(sseBody),
|
|
2851
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
const stream = client.subscribeSSE("orders");
|
|
2855
|
+
const events: any[] = [];
|
|
2856
|
+
await new Promise<void>((resolve) => {
|
|
2857
|
+
stream.on("event", (e: any) => events.push(e));
|
|
2858
|
+
stream.on("close", resolve);
|
|
2859
|
+
setTimeout(resolve, 100);
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
expect(events).toHaveLength(2);
|
|
2863
|
+
expect(events[0].event).toBe("insert");
|
|
2864
|
+
expect(events[0].recordIds).toEqual(["r1"]);
|
|
2865
|
+
expect(events[1].event).toBe("update");
|
|
2866
|
+
expect(events[1].recordIds).toEqual(["r2"]);
|
|
2867
|
+
});
|
|
2868
|
+
|
|
2869
|
+
it("passes filter params in URL", async () => {
|
|
2870
|
+
const client = createTestClient();
|
|
2871
|
+
mockTokenResponse();
|
|
2872
|
+
|
|
2873
|
+
mockFetch.mockResolvedValueOnce({
|
|
2874
|
+
ok: true,
|
|
2875
|
+
status: 200,
|
|
2876
|
+
body: mockReadableStream(""),
|
|
2877
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2878
|
+
});
|
|
2879
|
+
|
|
2880
|
+
client.subscribeSSE("orders", {
|
|
2881
|
+
filterField: "status",
|
|
2882
|
+
filterValue: "active",
|
|
2883
|
+
});
|
|
2884
|
+
|
|
2885
|
+
// Wait for async fetch
|
|
2886
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2887
|
+
|
|
2888
|
+
const call = mockFetch.mock.calls[1];
|
|
2889
|
+
expect(call[0]).toContain("/api/subscribe/orders");
|
|
2890
|
+
expect(call[0]).toContain("filter_field=status");
|
|
2891
|
+
expect(call[0]).toContain("filter_value=active");
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2894
|
+
it("emits error on HTTP failure", async () => {
|
|
2895
|
+
const client = createTestClient();
|
|
2896
|
+
mockTokenResponse();
|
|
2897
|
+
|
|
2898
|
+
mockFetch.mockResolvedValueOnce({
|
|
2899
|
+
ok: false,
|
|
2900
|
+
status: 401,
|
|
2901
|
+
text: async () => "Unauthorized",
|
|
2902
|
+
headers: new Headers(),
|
|
2903
|
+
});
|
|
2904
|
+
|
|
2905
|
+
const stream = client.subscribeSSE("orders");
|
|
2906
|
+
const errors: string[] = [];
|
|
2907
|
+
await new Promise<void>((resolve) => {
|
|
2908
|
+
stream.on("error", (e: string) => errors.push(e));
|
|
2909
|
+
stream.on("close", resolve);
|
|
2910
|
+
setTimeout(resolve, 100);
|
|
2911
|
+
});
|
|
2912
|
+
|
|
2913
|
+
expect(errors).toHaveLength(1);
|
|
2914
|
+
expect(errors[0]).toContain("401");
|
|
2915
|
+
});
|
|
2916
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { encode, decode } from "@msgpack/msgpack";
|
|
|
6
6
|
import { QueryBuilder, Query as QueryBuilderQuery } from "./query-builder";
|
|
7
7
|
import { SearchQuery, SearchResponse } from "./search";
|
|
8
8
|
import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
|
|
9
|
-
import {
|
|
9
|
+
import { UserFunction, FunctionResult } from "./functions";
|
|
10
10
|
|
|
11
11
|
export interface Record {
|
|
12
12
|
[key: string]: any;
|
|
@@ -188,6 +188,7 @@ export interface CreateChatSessionRequest {
|
|
|
188
188
|
llm_provider: string;
|
|
189
189
|
llm_model?: string;
|
|
190
190
|
system_prompt?: string;
|
|
191
|
+
agent_id?: string;
|
|
191
192
|
bypass_ripple?: boolean;
|
|
192
193
|
parent_id?: string;
|
|
193
194
|
branch_point_idx?: number;
|
|
@@ -204,6 +205,9 @@ export interface ChatMessageRequest {
|
|
|
204
205
|
max_iterations?: number;
|
|
205
206
|
tool_config?: ToolConfig;
|
|
206
207
|
llm_model?: string;
|
|
208
|
+
client_tools?: ClientToolDefinition[];
|
|
209
|
+
confirm_tools?: string[];
|
|
210
|
+
exclude_tools?: string[];
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
export interface TokenUsage {
|
|
@@ -229,6 +233,7 @@ export interface ChatSession {
|
|
|
229
233
|
llm_model: string;
|
|
230
234
|
collections: CollectionConfig[];
|
|
231
235
|
system_prompt?: string;
|
|
236
|
+
agent_id?: string;
|
|
232
237
|
title?: string;
|
|
233
238
|
message_count: number;
|
|
234
239
|
}
|
|
@@ -335,21 +340,7 @@ export interface RawCompletionResponse {
|
|
|
335
340
|
content: string;
|
|
336
341
|
}
|
|
337
342
|
|
|
338
|
-
|
|
339
|
-
* User function definition - reusable sequence of Functions that can be called by Scripts
|
|
340
|
-
*/
|
|
341
|
-
export interface UserFunction {
|
|
342
|
-
label: string;
|
|
343
|
-
name: string;
|
|
344
|
-
description?: string;
|
|
345
|
-
version?: string;
|
|
346
|
-
parameters: { [key: string]: ParameterDefinition };
|
|
347
|
-
functions: FunctionStageConfig[];
|
|
348
|
-
tags?: string[];
|
|
349
|
-
id?: string;
|
|
350
|
-
created_at?: string;
|
|
351
|
-
updated_at?: string;
|
|
352
|
-
}
|
|
343
|
+
// UserFunction is defined in functions.ts and re-exported from there.
|
|
353
344
|
|
|
354
345
|
/**
|
|
355
346
|
* Parameter definition for functions
|
|
@@ -1769,6 +1760,31 @@ export class EkoDBClient {
|
|
|
1769
1760
|
);
|
|
1770
1761
|
}
|
|
1771
1762
|
|
|
1763
|
+
/**
|
|
1764
|
+
* Submit a client tool result for an in-flight SSE chat stream.
|
|
1765
|
+
* Unblocks ekoDB's tool loop so it can feed the result to the LLM.
|
|
1766
|
+
*/
|
|
1767
|
+
async submitChatToolResult(
|
|
1768
|
+
chatId: string,
|
|
1769
|
+
callId: string,
|
|
1770
|
+
success: boolean,
|
|
1771
|
+
result?: any,
|
|
1772
|
+
error?: string,
|
|
1773
|
+
): Promise<void> {
|
|
1774
|
+
await this.makeRequest(
|
|
1775
|
+
"POST",
|
|
1776
|
+
`/api/chat/${chatId}/tool-result`,
|
|
1777
|
+
{
|
|
1778
|
+
call_id: callId,
|
|
1779
|
+
success,
|
|
1780
|
+
...(result !== undefined && { result }),
|
|
1781
|
+
...(error !== undefined && { error }),
|
|
1782
|
+
},
|
|
1783
|
+
0,
|
|
1784
|
+
true,
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1772
1788
|
/**
|
|
1773
1789
|
* Send a message in an existing chat session via SSE streaming.
|
|
1774
1790
|
*
|
|
@@ -2106,9 +2122,9 @@ export class EkoDBClient {
|
|
|
2106
2122
|
// ========================================================================
|
|
2107
2123
|
|
|
2108
2124
|
/**
|
|
2109
|
-
* Save a new
|
|
2125
|
+
* Save a new function definition
|
|
2110
2126
|
*/
|
|
2111
|
-
async
|
|
2127
|
+
async saveFunction(script: UserFunction): Promise<string> {
|
|
2112
2128
|
const result = await this.makeRequest<{ id: string }>(
|
|
2113
2129
|
"POST",
|
|
2114
2130
|
"/api/functions",
|
|
@@ -2118,38 +2134,38 @@ export class EkoDBClient {
|
|
|
2118
2134
|
}
|
|
2119
2135
|
|
|
2120
2136
|
/**
|
|
2121
|
-
* Get a
|
|
2137
|
+
* Get a function by ID
|
|
2122
2138
|
*/
|
|
2123
|
-
async
|
|
2124
|
-
return this.makeRequest<
|
|
2139
|
+
async getFunction(id: string): Promise<UserFunction> {
|
|
2140
|
+
return this.makeRequest<UserFunction>("GET", `/api/functions/${id}`);
|
|
2125
2141
|
}
|
|
2126
2142
|
|
|
2127
2143
|
/**
|
|
2128
|
-
* List all
|
|
2144
|
+
* List all functions, optionally filtered by tags
|
|
2129
2145
|
*/
|
|
2130
|
-
async
|
|
2146
|
+
async listFunctions(tags?: string[]): Promise<UserFunction[]> {
|
|
2131
2147
|
const params = tags ? `?tags=${tags.join(",")}` : "";
|
|
2132
|
-
return this.makeRequest<
|
|
2148
|
+
return this.makeRequest<UserFunction[]>("GET", `/api/functions${params}`);
|
|
2133
2149
|
}
|
|
2134
2150
|
|
|
2135
2151
|
/**
|
|
2136
|
-
* Update an existing
|
|
2152
|
+
* Update an existing function by ID
|
|
2137
2153
|
*/
|
|
2138
|
-
async
|
|
2154
|
+
async updateFunction(id: string, script: UserFunction): Promise<void> {
|
|
2139
2155
|
await this.makeRequest<void>("PUT", `/api/functions/${id}`, script);
|
|
2140
2156
|
}
|
|
2141
2157
|
|
|
2142
2158
|
/**
|
|
2143
|
-
* Delete a
|
|
2159
|
+
* Delete a function by ID
|
|
2144
2160
|
*/
|
|
2145
|
-
async
|
|
2161
|
+
async deleteFunction(id: string): Promise<void> {
|
|
2146
2162
|
await this.makeRequest<void>("DELETE", `/api/functions/${id}`);
|
|
2147
2163
|
}
|
|
2148
2164
|
|
|
2149
2165
|
/**
|
|
2150
|
-
* Call a saved
|
|
2166
|
+
* Call a saved function by ID or label
|
|
2151
2167
|
*/
|
|
2152
|
-
async
|
|
2168
|
+
async callFunction(
|
|
2153
2169
|
idOrLabel: string,
|
|
2154
2170
|
params?: { [key: string]: any },
|
|
2155
2171
|
): Promise<FunctionResult> {
|
|
@@ -2783,6 +2799,115 @@ export class EkoDBClient {
|
|
|
2783
2799
|
return records.length;
|
|
2784
2800
|
}
|
|
2785
2801
|
|
|
2802
|
+
/**
|
|
2803
|
+
* Subscribe to collection mutations via SSE (Server-Sent Events).
|
|
2804
|
+
*
|
|
2805
|
+
* Returns an EventStream that emits MutationNotification events.
|
|
2806
|
+
* Use this when WebSocket connections aren't available (e.g. behind
|
|
2807
|
+
* reverse proxies that block WS upgrades).
|
|
2808
|
+
*/
|
|
2809
|
+
subscribeSSE(
|
|
2810
|
+
collection: string,
|
|
2811
|
+
options?: { filterField?: string; filterValue?: string },
|
|
2812
|
+
): EventStream<MutationNotification> {
|
|
2813
|
+
const stream = new EventStream<MutationNotification>();
|
|
2814
|
+
|
|
2815
|
+
(async () => {
|
|
2816
|
+
try {
|
|
2817
|
+
let token = await this.getToken();
|
|
2818
|
+
if (!token) {
|
|
2819
|
+
await this.refreshToken();
|
|
2820
|
+
token = await this.getToken();
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
let url = `${this.baseURL}/api/subscribe/${encodeURIComponent(collection)}`;
|
|
2824
|
+
const params: string[] = [];
|
|
2825
|
+
if (options?.filterField)
|
|
2826
|
+
params.push(
|
|
2827
|
+
`filter_field=${encodeURIComponent(options.filterField)}`,
|
|
2828
|
+
);
|
|
2829
|
+
if (options?.filterValue)
|
|
2830
|
+
params.push(
|
|
2831
|
+
`filter_value=${encodeURIComponent(options.filterValue)}`,
|
|
2832
|
+
);
|
|
2833
|
+
if (params.length > 0) url += `?${params.join("&")}`;
|
|
2834
|
+
|
|
2835
|
+
const response = await fetch(url, {
|
|
2836
|
+
method: "GET",
|
|
2837
|
+
headers: {
|
|
2838
|
+
Accept: "text/event-stream",
|
|
2839
|
+
Authorization: `Bearer ${token}`,
|
|
2840
|
+
},
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
if (!response.ok) {
|
|
2844
|
+
const body = await response.text();
|
|
2845
|
+
stream.emit(
|
|
2846
|
+
"error",
|
|
2847
|
+
`SSE subscribe failed (${response.status}): ${body}`,
|
|
2848
|
+
);
|
|
2849
|
+
stream.close();
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
const reader = response.body?.getReader();
|
|
2854
|
+
if (!reader) {
|
|
2855
|
+
stream.emit("error", "SSE subscribe failed: streaming not supported");
|
|
2856
|
+
stream.close();
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
const decoder = new TextDecoder("utf-8");
|
|
2861
|
+
let buffer = "";
|
|
2862
|
+
let eventType = "";
|
|
2863
|
+
let dataLines: string[] = [];
|
|
2864
|
+
|
|
2865
|
+
while (!stream.closed) {
|
|
2866
|
+
const { value, done } = await reader.read();
|
|
2867
|
+
if (done) break;
|
|
2868
|
+
|
|
2869
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2870
|
+
const lines = buffer.split("\n");
|
|
2871
|
+
buffer = lines.pop() ?? "";
|
|
2872
|
+
|
|
2873
|
+
for (const line of lines) {
|
|
2874
|
+
if (line === "") {
|
|
2875
|
+
// End of event block
|
|
2876
|
+
if (eventType === "mutation" && dataLines.length > 0) {
|
|
2877
|
+
try {
|
|
2878
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
2879
|
+
stream.emit("event", {
|
|
2880
|
+
collection: payload.collection,
|
|
2881
|
+
event: payload.event,
|
|
2882
|
+
recordIds: payload.record_ids,
|
|
2883
|
+
records: payload.records,
|
|
2884
|
+
timestamp: payload.timestamp,
|
|
2885
|
+
} as MutationNotification);
|
|
2886
|
+
} catch {
|
|
2887
|
+
// skip malformed data
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
eventType = "";
|
|
2891
|
+
dataLines = [];
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
if (line.startsWith("event: ")) {
|
|
2895
|
+
eventType = line.slice(7).trim();
|
|
2896
|
+
} else if (line.startsWith("data: ")) {
|
|
2897
|
+
dataLines.push(line.slice(6).trim());
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
stream.close();
|
|
2902
|
+
} catch (err: any) {
|
|
2903
|
+
stream.emit("error", err.message ?? String(err));
|
|
2904
|
+
stream.close();
|
|
2905
|
+
}
|
|
2906
|
+
})();
|
|
2907
|
+
|
|
2908
|
+
return stream;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2786
2911
|
/**
|
|
2787
2912
|
* Create a WebSocket client
|
|
2788
2913
|
*/
|
package/src/functions.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Functions API for ekoDB TypeScript client
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** A reusable sequence of Functions stored in ekoDB. */
|
|
6
|
+
export interface UserFunction {
|
|
7
|
+
id?: string;
|
|
6
8
|
label: string;
|
|
7
9
|
name: string;
|
|
8
10
|
description?: string;
|
|
@@ -146,7 +148,7 @@ export type FunctionStageConfig =
|
|
|
146
148
|
}
|
|
147
149
|
| {
|
|
148
150
|
type: "If";
|
|
149
|
-
condition:
|
|
151
|
+
condition: FunctionCondition;
|
|
150
152
|
then_functions: FunctionStageConfig[];
|
|
151
153
|
else_functions?: FunctionStageConfig[];
|
|
152
154
|
}
|
|
@@ -265,18 +267,18 @@ export interface SortFieldConfig {
|
|
|
265
267
|
ascending: boolean;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
//
|
|
270
|
+
// FunctionCondition uses adjacently-tagged format: { type: "...", value: { ...data } }
|
|
269
271
|
// Unit variants (HasRecords) have no value field
|
|
270
|
-
export type
|
|
272
|
+
export type FunctionCondition =
|
|
271
273
|
| { type: "FieldEquals"; value: { field: string; value: any } }
|
|
272
274
|
| { type: "FieldExists"; value: { field: string } }
|
|
273
275
|
| { type: "HasRecords" }
|
|
274
276
|
| { type: "CountEquals"; value: { count: number } }
|
|
275
277
|
| { type: "CountGreaterThan"; value: { count: number } }
|
|
276
278
|
| { type: "CountLessThan"; value: { count: number } }
|
|
277
|
-
| { type: "And"; value: { conditions:
|
|
278
|
-
| { type: "Or"; value: { conditions:
|
|
279
|
-
| { type: "Not"; value: { condition:
|
|
279
|
+
| { type: "And"; value: { conditions: FunctionCondition[] } }
|
|
280
|
+
| { type: "Or"; value: { conditions: FunctionCondition[] } }
|
|
281
|
+
| { type: "Not"; value: { condition: FunctionCondition } };
|
|
280
282
|
|
|
281
283
|
export interface FunctionResult {
|
|
282
284
|
records: Record<string, any>[];
|
|
@@ -541,7 +543,7 @@ export const Stage = {
|
|
|
541
543
|
}),
|
|
542
544
|
|
|
543
545
|
if: (
|
|
544
|
-
condition:
|
|
546
|
+
condition: FunctionCondition,
|
|
545
547
|
thenFunctions: FunctionStageConfig[],
|
|
546
548
|
elseFunctions?: FunctionStageConfig[],
|
|
547
549
|
): FunctionStageConfig => ({
|
package/src/index.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type {
|
|
|
44
44
|
} from "./schema";
|
|
45
45
|
export type { JoinConfig } from "./join";
|
|
46
46
|
export type {
|
|
47
|
-
|
|
47
|
+
UserFunction,
|
|
48
48
|
ParameterDefinition,
|
|
49
49
|
FunctionStageConfig,
|
|
50
50
|
GroupFunctionConfig,
|
|
@@ -85,7 +85,6 @@ export type {
|
|
|
85
85
|
EmbedResponse,
|
|
86
86
|
RawCompletionRequest,
|
|
87
87
|
RawCompletionResponse,
|
|
88
|
-
UserFunction,
|
|
89
88
|
ToolChoice,
|
|
90
89
|
ToolConfig,
|
|
91
90
|
} from "./client";
|