@ekodb/ekodb-client 0.12.0 → 0.14.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/README.md +70 -0
- package/dist/client.d.ts +351 -9
- package/dist/client.js +903 -25
- package/dist/client.test.js +1056 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/websocket.test.d.ts +6 -0
- package/dist/websocket.test.js +496 -0
- package/package.json +2 -1
- package/src/client.test.ts +1357 -1
- package/src/client.ts +1484 -86
- package/src/index.ts +10 -0
- package/src/websocket.test.ts +712 -0
- package/tsconfig.json +3 -1
package/src/client.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { encode, decode } from "@msgpack/msgpack";
|
|
6
6
|
import { QueryBuilder, Query as QueryBuilderQuery } from "./query-builder";
|
|
7
|
-
import { SearchQuery,
|
|
7
|
+
import { SearchQuery, SearchResponse } from "./search";
|
|
8
8
|
import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
|
|
9
9
|
import { Script, FunctionResult } from "./functions";
|
|
10
10
|
|
|
@@ -46,8 +46,6 @@ export interface ClientConfig {
|
|
|
46
46
|
shouldRetry?: boolean;
|
|
47
47
|
/** Maximum number of retry attempts (default: 3) */
|
|
48
48
|
maxRetries?: number;
|
|
49
|
-
/** Request timeout in milliseconds (default: 30000) */
|
|
50
|
-
timeout?: number;
|
|
51
49
|
/** Serialization format (default: MessagePack for best performance, use Json for debugging) */
|
|
52
50
|
format?: SerializationFormat;
|
|
53
51
|
}
|
|
@@ -134,6 +132,26 @@ export interface BatchDeleteOptions {
|
|
|
134
132
|
transactionId?: string;
|
|
135
133
|
}
|
|
136
134
|
|
|
135
|
+
export interface DistinctValuesOptions {
|
|
136
|
+
/** Optional filter expression (same format as find() filter). */
|
|
137
|
+
filter?: any;
|
|
138
|
+
/** Bypass ripple propagation for this query. */
|
|
139
|
+
bypassRipple?: boolean;
|
|
140
|
+
/** Bypass cache for this query. */
|
|
141
|
+
bypassCache?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface DistinctValuesResponse {
|
|
145
|
+
/** Collection that was queried. */
|
|
146
|
+
collection: string;
|
|
147
|
+
/** Field whose distinct values were returned. */
|
|
148
|
+
field: string;
|
|
149
|
+
/** Unique values, sorted alphabetically. */
|
|
150
|
+
values: any[];
|
|
151
|
+
/** Number of distinct values. */
|
|
152
|
+
count: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
// ========== Chat Interfaces ==========
|
|
138
156
|
|
|
139
157
|
export interface CollectionConfig {
|
|
@@ -298,6 +316,24 @@ export interface EmbedResponse {
|
|
|
298
316
|
dimensions: number;
|
|
299
317
|
}
|
|
300
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Request for stateless raw LLM completion — no session, no history, no RAG.
|
|
321
|
+
*/
|
|
322
|
+
export interface RawCompletionRequest {
|
|
323
|
+
system_prompt: string;
|
|
324
|
+
message: string;
|
|
325
|
+
provider?: string;
|
|
326
|
+
model?: string;
|
|
327
|
+
max_tokens?: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Response from a raw LLM completion request.
|
|
332
|
+
*/
|
|
333
|
+
export interface RawCompletionResponse {
|
|
334
|
+
content: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
301
337
|
/**
|
|
302
338
|
* User function definition - reusable sequence of Functions that can be called by Scripts
|
|
303
339
|
*/
|
|
@@ -335,9 +371,9 @@ export class EkoDBClient {
|
|
|
335
371
|
private baseURL: string;
|
|
336
372
|
private apiKey: string;
|
|
337
373
|
private token: string | null = null;
|
|
374
|
+
private tokenExpiry: number = 0;
|
|
338
375
|
private shouldRetry: boolean;
|
|
339
376
|
private maxRetries: number;
|
|
340
|
-
private timeout: number;
|
|
341
377
|
private format: SerializationFormat;
|
|
342
378
|
private rateLimitInfo: RateLimitInfo | null = null;
|
|
343
379
|
|
|
@@ -348,14 +384,12 @@ export class EkoDBClient {
|
|
|
348
384
|
this.apiKey = apiKey!;
|
|
349
385
|
this.shouldRetry = true;
|
|
350
386
|
this.maxRetries = 3;
|
|
351
|
-
this.timeout = 30000;
|
|
352
387
|
this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
|
|
353
388
|
} else {
|
|
354
389
|
this.baseURL = config.baseURL;
|
|
355
390
|
this.apiKey = config.apiKey;
|
|
356
391
|
this.shouldRetry = config.shouldRetry ?? true;
|
|
357
392
|
this.maxRetries = config.maxRetries ?? 3;
|
|
358
|
-
this.timeout = config.timeout ?? 30000;
|
|
359
393
|
this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
|
|
360
394
|
}
|
|
361
395
|
}
|
|
@@ -386,7 +420,7 @@ export class EkoDBClient {
|
|
|
386
420
|
/**
|
|
387
421
|
* Refresh the authentication token
|
|
388
422
|
*/
|
|
389
|
-
|
|
423
|
+
async refreshToken(): Promise<void> {
|
|
390
424
|
const response = await fetch(`${this.baseURL}/api/auth/token`, {
|
|
391
425
|
method: "POST",
|
|
392
426
|
headers: { "Content-Type": "application/json" },
|
|
@@ -408,6 +442,74 @@ export class EkoDBClient {
|
|
|
408
442
|
|
|
409
443
|
const result = (await response.json()) as { token: string };
|
|
410
444
|
this.token = result.token;
|
|
445
|
+
|
|
446
|
+
// Extract and cache JWT expiry for proactive refresh
|
|
447
|
+
const expiry = this.extractJWTExpiry(result.token);
|
|
448
|
+
this.tokenExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600; // fallback: 1 hour
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get a valid authentication token.
|
|
453
|
+
*
|
|
454
|
+
* Returns a cached token if it has more than 60s of validity remaining.
|
|
455
|
+
* Otherwise fetches a new one via refreshToken(). This means callers
|
|
456
|
+
* never need to handle token refresh themselves — every getToken() call
|
|
457
|
+
* returns a token that's valid for at least 60 more seconds.
|
|
458
|
+
*/
|
|
459
|
+
async getToken(): Promise<string | null> {
|
|
460
|
+
if (this.token) {
|
|
461
|
+
const now = Math.floor(Date.now() / 1000);
|
|
462
|
+
if (now + 60 >= this.tokenExpiry) {
|
|
463
|
+
// Token is about to expire or already expired — refresh proactively
|
|
464
|
+
await this.refreshToken();
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// No token yet — fetch one
|
|
468
|
+
await this.refreshToken();
|
|
469
|
+
}
|
|
470
|
+
return this.token;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Clear the cached authentication token and expiry.
|
|
475
|
+
* The next request will trigger a fresh token exchange.
|
|
476
|
+
*/
|
|
477
|
+
clearTokenCache(): void {
|
|
478
|
+
this.token = null;
|
|
479
|
+
this.tokenExpiry = 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Extract the `exp` claim from a JWT without verifying the signature.
|
|
484
|
+
* Returns the Unix timestamp (seconds) of expiry, or null if parsing fails.
|
|
485
|
+
*/
|
|
486
|
+
private extractJWTExpiry(token: string): number | null {
|
|
487
|
+
try {
|
|
488
|
+
const parts = token.split(".");
|
|
489
|
+
if (parts.length !== 3) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Convert base64url to standard base64
|
|
494
|
+
let payload = parts[1];
|
|
495
|
+
payload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
496
|
+
|
|
497
|
+
// Pad to multiple of 4
|
|
498
|
+
const pad = payload.length % 4;
|
|
499
|
+
if (pad) {
|
|
500
|
+
payload += "=".repeat(4 - pad);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const decoded = atob(payload);
|
|
504
|
+
const claims = JSON.parse(decoded);
|
|
505
|
+
|
|
506
|
+
if (typeof claims.exp === "number") {
|
|
507
|
+
return claims.exp;
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
411
513
|
}
|
|
412
514
|
|
|
413
515
|
/**
|
|
@@ -660,6 +762,32 @@ export class EkoDBClient {
|
|
|
660
762
|
return this.makeRequest<Record>("GET", `/api/find/${collection}/${id}`);
|
|
661
763
|
}
|
|
662
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Find a document by ID with field projection
|
|
767
|
+
* @param collection - Collection name
|
|
768
|
+
* @param id - Document ID
|
|
769
|
+
* @param selectFields - Fields to include in the result
|
|
770
|
+
* @param excludeFields - Fields to exclude from the result
|
|
771
|
+
*/
|
|
772
|
+
async findByIdWithProjection(
|
|
773
|
+
collection: string,
|
|
774
|
+
id: string,
|
|
775
|
+
selectFields?: string[],
|
|
776
|
+
excludeFields?: string[],
|
|
777
|
+
): Promise<Record> {
|
|
778
|
+
const params = new URLSearchParams();
|
|
779
|
+
if (selectFields?.length) {
|
|
780
|
+
params.append("select_fields", selectFields.join(","));
|
|
781
|
+
}
|
|
782
|
+
if (excludeFields?.length) {
|
|
783
|
+
params.append("exclude_fields", excludeFields.join(","));
|
|
784
|
+
}
|
|
785
|
+
const url = params.toString()
|
|
786
|
+
? `/api/find/${collection}/${id}?${params.toString()}`
|
|
787
|
+
: `/api/find/${collection}/${id}`;
|
|
788
|
+
return this.makeRequest<Record>("GET", url);
|
|
789
|
+
}
|
|
790
|
+
|
|
663
791
|
/**
|
|
664
792
|
* Update a document
|
|
665
793
|
* @param collection - Collection name
|
|
@@ -688,6 +816,52 @@ export class EkoDBClient {
|
|
|
688
816
|
return this.makeRequest<Record>("PUT", url, record);
|
|
689
817
|
}
|
|
690
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Apply an atomic field action to a single field of a record.
|
|
821
|
+
*
|
|
822
|
+
* Use this instead of `update()` for safe concurrent modifications like
|
|
823
|
+
* incrementing counters, pushing to arrays, or arithmetic operations.
|
|
824
|
+
*
|
|
825
|
+
* @param collection - Collection name
|
|
826
|
+
* @param id - Record ID
|
|
827
|
+
* @param action - The atomic action: increment, decrement, multiply, divide, modulo,
|
|
828
|
+
* push, pop, shift, unshift, remove, append, clear
|
|
829
|
+
* @param field - The field name to apply the action to
|
|
830
|
+
* @param value - The value for the action (omit for pop/shift/clear)
|
|
831
|
+
*/
|
|
832
|
+
async updateWithAction(
|
|
833
|
+
collection: string,
|
|
834
|
+
id: string,
|
|
835
|
+
action: string,
|
|
836
|
+
field: string,
|
|
837
|
+
value?: any,
|
|
838
|
+
): Promise<Record> {
|
|
839
|
+
const url = `/api/update/${collection}/${id}/action/${action}`;
|
|
840
|
+
return this.makeRequest<Record>("PUT", url, {
|
|
841
|
+
field,
|
|
842
|
+
value: value ?? null,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Apply a sequence of atomic field actions to a record in a single request.
|
|
848
|
+
*
|
|
849
|
+
* All actions are applied atomically — the record is fetched once, all actions
|
|
850
|
+
* run in order, and the result is persisted in a single update.
|
|
851
|
+
*
|
|
852
|
+
* @param collection - Collection name
|
|
853
|
+
* @param id - Record ID
|
|
854
|
+
* @param actions - Array of [action, field, value] tuples
|
|
855
|
+
*/
|
|
856
|
+
async updateWithActionSequence(
|
|
857
|
+
collection: string,
|
|
858
|
+
id: string,
|
|
859
|
+
actions: [string, string, any][],
|
|
860
|
+
): Promise<Record> {
|
|
861
|
+
const url = `/api/update/sequence/${collection}/${id}`;
|
|
862
|
+
return this.makeRequest<Record>("PUT", url, actions);
|
|
863
|
+
}
|
|
864
|
+
|
|
691
865
|
/**
|
|
692
866
|
* Delete a document
|
|
693
867
|
* @param collection - Collection name
|
|
@@ -1313,6 +1487,51 @@ export class EkoDBClient {
|
|
|
1313
1487
|
);
|
|
1314
1488
|
}
|
|
1315
1489
|
|
|
1490
|
+
/**
|
|
1491
|
+
* Get distinct (unique) values for a field across all records in a collection.
|
|
1492
|
+
*
|
|
1493
|
+
* Results are deduplicated and sorted alphabetically. Supports an optional filter
|
|
1494
|
+
* to restrict which records are examined.
|
|
1495
|
+
*
|
|
1496
|
+
* @param collection - Collection name
|
|
1497
|
+
* @param field - Field to get distinct values for
|
|
1498
|
+
* @param options - Optional filter and bypass flags
|
|
1499
|
+
*
|
|
1500
|
+
* @example
|
|
1501
|
+
* // All distinct statuses
|
|
1502
|
+
* const resp = await client.distinctValues("orders", "status");
|
|
1503
|
+
* console.log(resp.values); // ["active", "cancelled", "shipped"]
|
|
1504
|
+
*
|
|
1505
|
+
* // Only statuses for US orders
|
|
1506
|
+
* const resp = await client.distinctValues("orders", "status", {
|
|
1507
|
+
* filter: { type: "Condition", content: { field: "region", operator: "Eq", value: "us" } }
|
|
1508
|
+
* });
|
|
1509
|
+
*/
|
|
1510
|
+
async distinctValues(
|
|
1511
|
+
collection: string,
|
|
1512
|
+
field: string,
|
|
1513
|
+
options: DistinctValuesOptions = {},
|
|
1514
|
+
): Promise<DistinctValuesResponse> {
|
|
1515
|
+
const body: {
|
|
1516
|
+
filter?: any;
|
|
1517
|
+
bypass_ripple?: boolean;
|
|
1518
|
+
bypass_cache?: boolean;
|
|
1519
|
+
} = {};
|
|
1520
|
+
if (options.filter !== undefined) body.filter = options.filter;
|
|
1521
|
+
if (options.bypassRipple !== undefined)
|
|
1522
|
+
body.bypass_ripple = options.bypassRipple;
|
|
1523
|
+
if (options.bypassCache !== undefined)
|
|
1524
|
+
body.bypass_cache = options.bypassCache;
|
|
1525
|
+
|
|
1526
|
+
return this.makeRequest<DistinctValuesResponse>(
|
|
1527
|
+
"POST",
|
|
1528
|
+
`/api/distinct/${collection}/${field}`,
|
|
1529
|
+
body,
|
|
1530
|
+
0,
|
|
1531
|
+
true, // Force JSON
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1316
1535
|
/**
|
|
1317
1536
|
* Health check - verify the ekoDB server is responding
|
|
1318
1537
|
*/
|
|
@@ -1348,6 +1567,143 @@ export class EkoDBClient {
|
|
|
1348
1567
|
);
|
|
1349
1568
|
}
|
|
1350
1569
|
|
|
1570
|
+
/**
|
|
1571
|
+
* Stateless raw LLM completion — no session, no history, no RAG.
|
|
1572
|
+
*
|
|
1573
|
+
* Sends a system prompt and user message directly to the LLM via ekoDB
|
|
1574
|
+
* and returns the raw text response without any context injection or
|
|
1575
|
+
* conversation management. Use this for structured-output tasks such as
|
|
1576
|
+
* planning where the response must be parsed programmatically.
|
|
1577
|
+
*
|
|
1578
|
+
* @example
|
|
1579
|
+
* const resp = await client.rawCompletion({
|
|
1580
|
+
* system_prompt: "You are a helpful assistant.",
|
|
1581
|
+
* message: "Summarize this in JSON.",
|
|
1582
|
+
* max_tokens: 2048,
|
|
1583
|
+
* });
|
|
1584
|
+
* console.log(resp.content);
|
|
1585
|
+
*/
|
|
1586
|
+
async rawCompletion(
|
|
1587
|
+
request: RawCompletionRequest,
|
|
1588
|
+
): Promise<RawCompletionResponse> {
|
|
1589
|
+
return this.makeRequest<RawCompletionResponse>(
|
|
1590
|
+
"POST",
|
|
1591
|
+
"/api/chat/complete",
|
|
1592
|
+
request,
|
|
1593
|
+
0,
|
|
1594
|
+
true, // Force JSON
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Stateless raw LLM completion via SSE streaming.
|
|
1600
|
+
*
|
|
1601
|
+
* Same as rawCompletion() but uses Server-Sent Events to keep the
|
|
1602
|
+
* connection alive. Preferred for deployed instances where reverse proxies
|
|
1603
|
+
* may kill idle HTTP connections before the LLM responds.
|
|
1604
|
+
*/
|
|
1605
|
+
async rawCompletionStream(
|
|
1606
|
+
request: RawCompletionRequest,
|
|
1607
|
+
): Promise<RawCompletionResponse> {
|
|
1608
|
+
let token = await this.getToken();
|
|
1609
|
+
const url = `${this.baseURL}/api/chat/complete/stream`;
|
|
1610
|
+
|
|
1611
|
+
const response = await fetch(url, {
|
|
1612
|
+
method: "POST",
|
|
1613
|
+
headers: {
|
|
1614
|
+
"Content-Type": "application/json",
|
|
1615
|
+
Accept: "text/event-stream",
|
|
1616
|
+
Authorization: `Bearer ${token}`,
|
|
1617
|
+
},
|
|
1618
|
+
body: JSON.stringify(request),
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
if (!response.ok) {
|
|
1622
|
+
const body = await response.text();
|
|
1623
|
+
throw new Error(
|
|
1624
|
+
`SSE raw completion failed (${response.status}): ${body}`,
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const body = await response.text();
|
|
1629
|
+
let content = "";
|
|
1630
|
+
let lastError: string | null = null;
|
|
1631
|
+
|
|
1632
|
+
for (const line of body.split("\n")) {
|
|
1633
|
+
if (line.startsWith("data:")) {
|
|
1634
|
+
const dataStr = line.slice(5).trim();
|
|
1635
|
+
if (!dataStr) continue;
|
|
1636
|
+
try {
|
|
1637
|
+
const eventData = JSON.parse(dataStr);
|
|
1638
|
+
if (eventData.token) content += eventData.token;
|
|
1639
|
+
if (eventData.content) content = eventData.content;
|
|
1640
|
+
if (eventData.error) lastError = eventData.error;
|
|
1641
|
+
} catch {
|
|
1642
|
+
// skip malformed SSE data
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (lastError) throw new Error(lastError);
|
|
1648
|
+
return { content };
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Stateless raw LLM completion via SSE streaming with token-level progress.
|
|
1653
|
+
*
|
|
1654
|
+
* Same as rawCompletionStream() but invokes `onToken` with each token as it
|
|
1655
|
+
* arrives, allowing callers to show real-time progress.
|
|
1656
|
+
*/
|
|
1657
|
+
async rawCompletionStreamWithProgress(
|
|
1658
|
+
request: RawCompletionRequest,
|
|
1659
|
+
onToken: (token: string) => void,
|
|
1660
|
+
): Promise<RawCompletionResponse> {
|
|
1661
|
+
let token = await this.getToken();
|
|
1662
|
+
const url = `${this.baseURL}/api/chat/complete/stream`;
|
|
1663
|
+
|
|
1664
|
+
const response = await fetch(url, {
|
|
1665
|
+
method: "POST",
|
|
1666
|
+
headers: {
|
|
1667
|
+
"Content-Type": "application/json",
|
|
1668
|
+
Accept: "text/event-stream",
|
|
1669
|
+
Authorization: `Bearer ${token}`,
|
|
1670
|
+
},
|
|
1671
|
+
body: JSON.stringify(request),
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
if (!response.ok) {
|
|
1675
|
+
const body = await response.text();
|
|
1676
|
+
throw new Error(
|
|
1677
|
+
`SSE raw completion failed (${response.status}): ${body}`,
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const body = await response.text();
|
|
1682
|
+
let content = "";
|
|
1683
|
+
let lastError: string | null = null;
|
|
1684
|
+
|
|
1685
|
+
for (const line of body.split("\n")) {
|
|
1686
|
+
if (line.startsWith("data:")) {
|
|
1687
|
+
const dataStr = line.slice(5).trim();
|
|
1688
|
+
if (!dataStr) continue;
|
|
1689
|
+
try {
|
|
1690
|
+
const eventData = JSON.parse(dataStr);
|
|
1691
|
+
if (eventData.token) {
|
|
1692
|
+
content += eventData.token;
|
|
1693
|
+
onToken(eventData.token);
|
|
1694
|
+
}
|
|
1695
|
+
if (eventData.content) content = eventData.content;
|
|
1696
|
+
if (eventData.error) lastError = eventData.error;
|
|
1697
|
+
} catch {
|
|
1698
|
+
// skip malformed SSE data
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
if (lastError) throw new Error(lastError);
|
|
1704
|
+
return { content };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1351
1707
|
/**
|
|
1352
1708
|
* Send a message in an existing chat session
|
|
1353
1709
|
*/
|
|
@@ -1364,6 +1720,96 @@ export class EkoDBClient {
|
|
|
1364
1720
|
);
|
|
1365
1721
|
}
|
|
1366
1722
|
|
|
1723
|
+
/**
|
|
1724
|
+
* Send a message in an existing chat session via SSE streaming.
|
|
1725
|
+
*
|
|
1726
|
+
* Returns an EventStream that emits ChatStreamEvent objects as they arrive:
|
|
1727
|
+
* - `{ type: "chunk", content: "..." }` for each token
|
|
1728
|
+
* - `{ type: "end", messageId, executionTimeMs, tokenUsage?, contextWindow? }` when complete
|
|
1729
|
+
* - `{ type: "error", error: "..." }` on failure
|
|
1730
|
+
*
|
|
1731
|
+
* Preferred over chatMessage() for long-running responses where reverse
|
|
1732
|
+
* proxies may kill idle HTTP connections before the LLM responds.
|
|
1733
|
+
*/
|
|
1734
|
+
chatMessageStream(
|
|
1735
|
+
chatId: string,
|
|
1736
|
+
request: ChatMessageRequest,
|
|
1737
|
+
): EventStream<ChatStreamEvent> {
|
|
1738
|
+
const stream = new EventStream<ChatStreamEvent>();
|
|
1739
|
+
|
|
1740
|
+
(async () => {
|
|
1741
|
+
try {
|
|
1742
|
+
let token = this.getToken();
|
|
1743
|
+
if (!token) {
|
|
1744
|
+
await this.refreshToken();
|
|
1745
|
+
token = this.getToken();
|
|
1746
|
+
}
|
|
1747
|
+
const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
|
|
1748
|
+
|
|
1749
|
+
const response = await fetch(url, {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
headers: {
|
|
1752
|
+
"Content-Type": "application/json",
|
|
1753
|
+
Accept: "text/event-stream",
|
|
1754
|
+
Authorization: `Bearer ${token}`,
|
|
1755
|
+
},
|
|
1756
|
+
body: JSON.stringify(request),
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
if (!response.ok) {
|
|
1760
|
+
const body = await response.text();
|
|
1761
|
+
stream.emit("event", {
|
|
1762
|
+
type: "error",
|
|
1763
|
+
error: `SSE chat message stream failed (${response.status}): ${body}`,
|
|
1764
|
+
} as ChatStreamEvent);
|
|
1765
|
+
stream.close();
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const body = await response.text();
|
|
1770
|
+
for (const line of body.split("\n")) {
|
|
1771
|
+
if (!line.startsWith("data:")) continue;
|
|
1772
|
+
const dataStr = line.slice(5).trim();
|
|
1773
|
+
if (!dataStr) continue;
|
|
1774
|
+
try {
|
|
1775
|
+
const eventData = JSON.parse(dataStr);
|
|
1776
|
+
if (eventData.error) {
|
|
1777
|
+
stream.emit("event", {
|
|
1778
|
+
type: "error",
|
|
1779
|
+
error: eventData.error,
|
|
1780
|
+
} as ChatStreamEvent);
|
|
1781
|
+
} else if (eventData.content && eventData.message_id) {
|
|
1782
|
+
// Done event — has full content + message_id
|
|
1783
|
+
stream.emit("event", {
|
|
1784
|
+
type: "end",
|
|
1785
|
+
messageId: eventData.message_id,
|
|
1786
|
+
executionTimeMs: eventData.execution_time_ms ?? 0,
|
|
1787
|
+
tokenUsage: eventData.token_usage,
|
|
1788
|
+
contextWindow: eventData.context_window,
|
|
1789
|
+
} as ChatStreamEvent);
|
|
1790
|
+
} else if (eventData.token) {
|
|
1791
|
+
stream.emit("event", {
|
|
1792
|
+
type: "chunk",
|
|
1793
|
+
content: eventData.token,
|
|
1794
|
+
} as ChatStreamEvent);
|
|
1795
|
+
}
|
|
1796
|
+
} catch {
|
|
1797
|
+
// skip malformed SSE data
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
stream.close();
|
|
1801
|
+
} catch (err: any) {
|
|
1802
|
+
stream.emit("event", {
|
|
1803
|
+
type: "error",
|
|
1804
|
+
error: err.message ?? String(err),
|
|
1805
|
+
} as ChatStreamEvent);
|
|
1806
|
+
stream.close();
|
|
1807
|
+
}
|
|
1808
|
+
})();
|
|
1809
|
+
|
|
1810
|
+
return stream;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1367
1813
|
/**
|
|
1368
1814
|
* Get a chat session by ID
|
|
1369
1815
|
*/
|
|
@@ -1560,6 +2006,21 @@ export class EkoDBClient {
|
|
|
1560
2006
|
);
|
|
1561
2007
|
}
|
|
1562
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* Get all built-in server-side chat tool definitions.
|
|
2011
|
+
* Returns a list of tool objects with name, description, and parameters fields.
|
|
2012
|
+
* Used by planning agents to discover available tools dynamically.
|
|
2013
|
+
*/
|
|
2014
|
+
async getChatTools(): Promise<object[]> {
|
|
2015
|
+
return this.makeRequest<object[]>(
|
|
2016
|
+
"GET",
|
|
2017
|
+
"/api/chat/tools",
|
|
2018
|
+
undefined,
|
|
2019
|
+
0,
|
|
2020
|
+
true, // Force JSON
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1563
2024
|
/**
|
|
1564
2025
|
* Get available models for a specific provider
|
|
1565
2026
|
* @param provider - Provider name (e.g., "openai", "anthropic", "perplexity")
|
|
@@ -1734,74 +2195,585 @@ export class EkoDBClient {
|
|
|
1734
2195
|
}
|
|
1735
2196
|
|
|
1736
2197
|
// ========================================================================
|
|
1737
|
-
//
|
|
2198
|
+
// GOAL API
|
|
1738
2199
|
// ========================================================================
|
|
1739
2200
|
|
|
1740
|
-
/**
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
* @returns true if the collection exists, false otherwise
|
|
1744
|
-
*/
|
|
1745
|
-
async collectionExists(collection: string): Promise<boolean> {
|
|
1746
|
-
try {
|
|
1747
|
-
const collections = await this.listCollections();
|
|
1748
|
-
return collections.includes(collection);
|
|
1749
|
-
} catch {
|
|
1750
|
-
return false;
|
|
1751
|
-
}
|
|
2201
|
+
/** Create a new goal */
|
|
2202
|
+
async goalCreate(data: Record): Promise<Record> {
|
|
2203
|
+
return this.makeRequest<Record>("POST", "/api/chat/goals", data, 0, true);
|
|
1752
2204
|
}
|
|
1753
2205
|
|
|
1754
|
-
/**
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2206
|
+
/** List all goals */
|
|
2207
|
+
async goalList(): Promise<Record> {
|
|
2208
|
+
return this.makeRequest<Record>(
|
|
2209
|
+
"GET",
|
|
2210
|
+
"/api/chat/goals",
|
|
2211
|
+
undefined,
|
|
2212
|
+
0,
|
|
2213
|
+
true,
|
|
2214
|
+
);
|
|
1763
2215
|
}
|
|
1764
2216
|
|
|
1765
|
-
/**
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2217
|
+
/** Get a goal by ID */
|
|
2218
|
+
async goalGet(id: string): Promise<Record> {
|
|
2219
|
+
return this.makeRequest<Record>(
|
|
2220
|
+
"GET",
|
|
2221
|
+
`/api/chat/goals/${encodeURIComponent(id)}`,
|
|
2222
|
+
undefined,
|
|
2223
|
+
0,
|
|
2224
|
+
true,
|
|
2225
|
+
);
|
|
1770
2226
|
}
|
|
1771
2227
|
|
|
1772
|
-
|
|
2228
|
+
/** Update a goal by ID */
|
|
2229
|
+
async goalUpdate(id: string, data: Record): Promise<Record> {
|
|
2230
|
+
return this.makeRequest<Record>(
|
|
2231
|
+
"PUT",
|
|
2232
|
+
`/api/chat/goals/${encodeURIComponent(id)}`,
|
|
2233
|
+
data,
|
|
2234
|
+
0,
|
|
2235
|
+
true,
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
1773
2238
|
|
|
1774
|
-
/**
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
* const embedding = await client.embed(
|
|
1784
|
-
* "Hello world",
|
|
1785
|
-
* "text-embedding-3-small"
|
|
1786
|
-
* );
|
|
1787
|
-
* console.log(`Generated ${embedding.length} dimensions`);
|
|
1788
|
-
* ```
|
|
1789
|
-
*/
|
|
1790
|
-
async embed(text: string, model: string): Promise<number[]> {
|
|
1791
|
-
const response = await this.embedRequest({ text, model });
|
|
1792
|
-
if (response.embeddings.length === 0) {
|
|
1793
|
-
throw new Error("No embedding returned");
|
|
1794
|
-
}
|
|
1795
|
-
return response.embeddings[0];
|
|
2239
|
+
/** Delete a goal by ID */
|
|
2240
|
+
async goalDelete(id: string): Promise<void> {
|
|
2241
|
+
await this.makeRequest<void>(
|
|
2242
|
+
"DELETE",
|
|
2243
|
+
`/api/chat/goals/${encodeURIComponent(id)}`,
|
|
2244
|
+
undefined,
|
|
2245
|
+
0,
|
|
2246
|
+
true,
|
|
2247
|
+
);
|
|
1796
2248
|
}
|
|
1797
2249
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2250
|
+
// ── Goal Templates ──
|
|
2251
|
+
|
|
2252
|
+
/** Create a new goal template */
|
|
2253
|
+
async goalTemplateCreate(data: Record): Promise<Record> {
|
|
2254
|
+
return this.makeRequest<Record>(
|
|
2255
|
+
"POST",
|
|
2256
|
+
"/api/chat/goal-templates",
|
|
2257
|
+
data,
|
|
2258
|
+
0,
|
|
2259
|
+
true,
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/** List all goal templates */
|
|
2264
|
+
async goalTemplateList(): Promise<Record> {
|
|
2265
|
+
return this.makeRequest<Record>(
|
|
2266
|
+
"GET",
|
|
2267
|
+
"/api/chat/goal-templates",
|
|
2268
|
+
undefined,
|
|
2269
|
+
0,
|
|
2270
|
+
true,
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/** Get a goal template by ID */
|
|
2275
|
+
async goalTemplateGet(id: string): Promise<Record> {
|
|
2276
|
+
return this.makeRequest<Record>(
|
|
2277
|
+
"GET",
|
|
2278
|
+
`/api/chat/goal-templates/${encodeURIComponent(id)}`,
|
|
2279
|
+
undefined,
|
|
2280
|
+
0,
|
|
2281
|
+
true,
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/** Update a goal template by ID */
|
|
2286
|
+
async goalTemplateUpdate(id: string, data: Record): Promise<Record> {
|
|
2287
|
+
return this.makeRequest<Record>(
|
|
2288
|
+
"PUT",
|
|
2289
|
+
`/api/chat/goal-templates/${encodeURIComponent(id)}`,
|
|
2290
|
+
data,
|
|
2291
|
+
0,
|
|
2292
|
+
true,
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/** Delete a goal template by ID */
|
|
2297
|
+
async goalTemplateDelete(id: string): Promise<void> {
|
|
2298
|
+
await this.makeRequest<void>(
|
|
2299
|
+
"DELETE",
|
|
2300
|
+
`/api/chat/goal-templates/${encodeURIComponent(id)}`,
|
|
2301
|
+
undefined,
|
|
2302
|
+
0,
|
|
2303
|
+
true,
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/** Search goals */
|
|
2308
|
+
async goalSearch(query: string): Promise<Record> {
|
|
2309
|
+
const params = new URLSearchParams({ q: query });
|
|
2310
|
+
return this.makeRequest<Record>(
|
|
2311
|
+
"GET",
|
|
2312
|
+
`/api/chat/goals/search?${params}`,
|
|
2313
|
+
undefined,
|
|
2314
|
+
0,
|
|
2315
|
+
true,
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/** Mark a goal as complete (status -> pending_review) */
|
|
2320
|
+
async goalComplete(id: string, data: Record): Promise<Record> {
|
|
2321
|
+
return this.makeRequest<Record>(
|
|
2322
|
+
"POST",
|
|
2323
|
+
`/api/chat/goals/${encodeURIComponent(id)}/complete`,
|
|
2324
|
+
data,
|
|
2325
|
+
0,
|
|
2326
|
+
true,
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/** Approve a goal (status -> in_progress) */
|
|
2331
|
+
async goalApprove(id: string): Promise<Record> {
|
|
2332
|
+
return this.makeRequest<Record>(
|
|
2333
|
+
"POST",
|
|
2334
|
+
`/api/chat/goals/${encodeURIComponent(id)}/approve`,
|
|
2335
|
+
undefined,
|
|
2336
|
+
0,
|
|
2337
|
+
true,
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/** Reject a goal (status -> failed) */
|
|
2342
|
+
async goalReject(id: string, data: Record): Promise<Record> {
|
|
2343
|
+
return this.makeRequest<Record>(
|
|
2344
|
+
"POST",
|
|
2345
|
+
`/api/chat/goals/${encodeURIComponent(id)}/reject`,
|
|
2346
|
+
data,
|
|
2347
|
+
0,
|
|
2348
|
+
true,
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/** Start a goal step (status -> in_progress) */
|
|
2353
|
+
async goalStepStart(id: string, stepIndex: number): Promise<Record> {
|
|
2354
|
+
return this.makeRequest<Record>(
|
|
2355
|
+
"POST",
|
|
2356
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`,
|
|
2357
|
+
undefined,
|
|
2358
|
+
0,
|
|
2359
|
+
true,
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/** Complete a goal step with result */
|
|
2364
|
+
async goalStepComplete(
|
|
2365
|
+
id: string,
|
|
2366
|
+
stepIndex: number,
|
|
2367
|
+
data: Record,
|
|
2368
|
+
): Promise<Record> {
|
|
2369
|
+
return this.makeRequest<Record>(
|
|
2370
|
+
"POST",
|
|
2371
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`,
|
|
2372
|
+
data,
|
|
2373
|
+
0,
|
|
2374
|
+
true,
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/** Fail a goal step with error */
|
|
2379
|
+
async goalStepFail(
|
|
2380
|
+
id: string,
|
|
2381
|
+
stepIndex: number,
|
|
2382
|
+
data: Record,
|
|
2383
|
+
): Promise<Record> {
|
|
2384
|
+
return this.makeRequest<Record>(
|
|
2385
|
+
"POST",
|
|
2386
|
+
`/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`,
|
|
2387
|
+
data,
|
|
2388
|
+
0,
|
|
2389
|
+
true,
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// ========================================================================
|
|
2394
|
+
// TASK API
|
|
2395
|
+
// ========================================================================
|
|
2396
|
+
|
|
2397
|
+
/** Create a new scheduled task */
|
|
2398
|
+
async taskCreate(data: Record): Promise<Record> {
|
|
2399
|
+
return this.makeRequest<Record>("POST", "/api/chat/tasks", data, 0, true);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
/** List all scheduled tasks */
|
|
2403
|
+
async taskList(): Promise<Record> {
|
|
2404
|
+
return this.makeRequest<Record>(
|
|
2405
|
+
"GET",
|
|
2406
|
+
"/api/chat/tasks",
|
|
2407
|
+
undefined,
|
|
2408
|
+
0,
|
|
2409
|
+
true,
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/** Get a task by ID */
|
|
2414
|
+
async taskGet(id: string): Promise<Record> {
|
|
2415
|
+
return this.makeRequest<Record>(
|
|
2416
|
+
"GET",
|
|
2417
|
+
`/api/chat/tasks/${encodeURIComponent(id)}`,
|
|
2418
|
+
undefined,
|
|
2419
|
+
0,
|
|
2420
|
+
true,
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
/** Update a task by ID */
|
|
2425
|
+
async taskUpdate(id: string, data: Record): Promise<Record> {
|
|
2426
|
+
return this.makeRequest<Record>(
|
|
2427
|
+
"PUT",
|
|
2428
|
+
`/api/chat/tasks/${encodeURIComponent(id)}`,
|
|
2429
|
+
data,
|
|
2430
|
+
0,
|
|
2431
|
+
true,
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** Delete a task by ID */
|
|
2436
|
+
async taskDelete(id: string): Promise<void> {
|
|
2437
|
+
await this.makeRequest<void>(
|
|
2438
|
+
"DELETE",
|
|
2439
|
+
`/api/chat/tasks/${encodeURIComponent(id)}`,
|
|
2440
|
+
undefined,
|
|
2441
|
+
0,
|
|
2442
|
+
true,
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
/** Get tasks that are due at the given time */
|
|
2447
|
+
async taskDue(now: string): Promise<Record> {
|
|
2448
|
+
const params = new URLSearchParams({ now });
|
|
2449
|
+
return this.makeRequest<Record>(
|
|
2450
|
+
"GET",
|
|
2451
|
+
`/api/chat/tasks/due?${params}`,
|
|
2452
|
+
undefined,
|
|
2453
|
+
0,
|
|
2454
|
+
true,
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/** Start a task (status -> running) */
|
|
2459
|
+
async taskStart(id: string): Promise<Record> {
|
|
2460
|
+
return this.makeRequest<Record>(
|
|
2461
|
+
"POST",
|
|
2462
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/start`,
|
|
2463
|
+
undefined,
|
|
2464
|
+
0,
|
|
2465
|
+
true,
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/** Mark a task as succeeded */
|
|
2470
|
+
async taskSucceed(id: string, data: Record): Promise<Record> {
|
|
2471
|
+
return this.makeRequest<Record>(
|
|
2472
|
+
"POST",
|
|
2473
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/succeed`,
|
|
2474
|
+
data,
|
|
2475
|
+
0,
|
|
2476
|
+
true,
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
/** Mark a task as failed */
|
|
2481
|
+
async taskFail(id: string, data: Record): Promise<Record> {
|
|
2482
|
+
return this.makeRequest<Record>(
|
|
2483
|
+
"POST",
|
|
2484
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/fail`,
|
|
2485
|
+
data,
|
|
2486
|
+
0,
|
|
2487
|
+
true,
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/** Pause a task */
|
|
2492
|
+
async taskPause(id: string): Promise<Record> {
|
|
2493
|
+
return this.makeRequest<Record>(
|
|
2494
|
+
"POST",
|
|
2495
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/pause`,
|
|
2496
|
+
undefined,
|
|
2497
|
+
0,
|
|
2498
|
+
true,
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/** Resume a paused task */
|
|
2503
|
+
async taskResume(id: string, data: Record): Promise<Record> {
|
|
2504
|
+
return this.makeRequest<Record>(
|
|
2505
|
+
"POST",
|
|
2506
|
+
`/api/chat/tasks/${encodeURIComponent(id)}/resume`,
|
|
2507
|
+
data,
|
|
2508
|
+
0,
|
|
2509
|
+
true,
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// ========================================================================
|
|
2514
|
+
// AGENT API
|
|
2515
|
+
// ========================================================================
|
|
2516
|
+
|
|
2517
|
+
/** Create a new agent */
|
|
2518
|
+
async agentCreate(data: Record): Promise<Record> {
|
|
2519
|
+
return this.makeRequest<Record>("POST", "/api/chat/agents", data, 0, true);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/** List all agents */
|
|
2523
|
+
async agentList(): Promise<Record> {
|
|
2524
|
+
return this.makeRequest<Record>(
|
|
2525
|
+
"GET",
|
|
2526
|
+
"/api/chat/agents",
|
|
2527
|
+
undefined,
|
|
2528
|
+
0,
|
|
2529
|
+
true,
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** Get an agent by ID */
|
|
2534
|
+
async agentGet(id: string): Promise<Record> {
|
|
2535
|
+
return this.makeRequest<Record>(
|
|
2536
|
+
"GET",
|
|
2537
|
+
`/api/chat/agents/${encodeURIComponent(id)}`,
|
|
2538
|
+
undefined,
|
|
2539
|
+
0,
|
|
2540
|
+
true,
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/** Get an agent by name */
|
|
2545
|
+
async agentGetByName(name: string): Promise<Record> {
|
|
2546
|
+
return this.makeRequest<Record>(
|
|
2547
|
+
"GET",
|
|
2548
|
+
`/api/chat/agents/by-name/${encodeURIComponent(name)}`,
|
|
2549
|
+
undefined,
|
|
2550
|
+
0,
|
|
2551
|
+
true,
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/** Update an agent by ID */
|
|
2556
|
+
async agentUpdate(id: string, data: Record): Promise<Record> {
|
|
2557
|
+
return this.makeRequest<Record>(
|
|
2558
|
+
"PUT",
|
|
2559
|
+
`/api/chat/agents/${encodeURIComponent(id)}`,
|
|
2560
|
+
data,
|
|
2561
|
+
0,
|
|
2562
|
+
true,
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/** Delete an agent by ID */
|
|
2567
|
+
async agentDelete(id: string): Promise<void> {
|
|
2568
|
+
await this.makeRequest<void>(
|
|
2569
|
+
"DELETE",
|
|
2570
|
+
`/api/chat/agents/${encodeURIComponent(id)}`,
|
|
2571
|
+
undefined,
|
|
2572
|
+
0,
|
|
2573
|
+
true,
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/** Get agents by deployment ID */
|
|
2578
|
+
async agentsByDeployment(deploymentId: string): Promise<Record> {
|
|
2579
|
+
return this.makeRequest<Record>(
|
|
2580
|
+
"GET",
|
|
2581
|
+
`/api/chat/agents/by-deployment/${encodeURIComponent(deploymentId)}`,
|
|
2582
|
+
undefined,
|
|
2583
|
+
0,
|
|
2584
|
+
true,
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// ========================================================================
|
|
2589
|
+
// KV DOCUMENT LINKING
|
|
2590
|
+
// ========================================================================
|
|
2591
|
+
|
|
2592
|
+
/** Get documents linked to a KV key */
|
|
2593
|
+
async kvGetLinks(key: string): Promise<Record> {
|
|
2594
|
+
return this.makeRequest<Record>(
|
|
2595
|
+
"GET",
|
|
2596
|
+
`/api/kv/links/${encodeURIComponent(key)}`,
|
|
2597
|
+
undefined,
|
|
2598
|
+
0,
|
|
2599
|
+
true,
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/** Link a document to a KV key */
|
|
2604
|
+
async kvLink(
|
|
2605
|
+
key: string,
|
|
2606
|
+
collection: string,
|
|
2607
|
+
documentId: string,
|
|
2608
|
+
): Promise<Record> {
|
|
2609
|
+
return this.makeRequest<Record>(
|
|
2610
|
+
"POST",
|
|
2611
|
+
`/api/kv/link`,
|
|
2612
|
+
{ key, collection, document_id: documentId },
|
|
2613
|
+
0,
|
|
2614
|
+
true,
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
/** Unlink a document from a KV key */
|
|
2619
|
+
async kvUnlink(
|
|
2620
|
+
key: string,
|
|
2621
|
+
collection: string,
|
|
2622
|
+
documentId: string,
|
|
2623
|
+
): Promise<Record> {
|
|
2624
|
+
return this.makeRequest<Record>(
|
|
2625
|
+
"POST",
|
|
2626
|
+
`/api/kv/unlink`,
|
|
2627
|
+
{ key, collection, document_id: documentId },
|
|
2628
|
+
0,
|
|
2629
|
+
true,
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// ========================================================================
|
|
2634
|
+
// SCHEDULE MANAGEMENT
|
|
2635
|
+
// ========================================================================
|
|
2636
|
+
|
|
2637
|
+
/** Create a new schedule */
|
|
2638
|
+
async createSchedule(data: Record): Promise<Record> {
|
|
2639
|
+
return this.makeRequest<Record>("POST", `/api/schedules`, data, 0, true);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/** List all schedules */
|
|
2643
|
+
async listSchedules(): Promise<Record> {
|
|
2644
|
+
return this.makeRequest<Record>(
|
|
2645
|
+
"GET",
|
|
2646
|
+
`/api/schedules`,
|
|
2647
|
+
undefined,
|
|
2648
|
+
0,
|
|
2649
|
+
true,
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
/** Get a schedule by ID */
|
|
2654
|
+
async getSchedule(id: string): Promise<Record> {
|
|
2655
|
+
return this.makeRequest<Record>(
|
|
2656
|
+
"GET",
|
|
2657
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
2658
|
+
undefined,
|
|
2659
|
+
0,
|
|
2660
|
+
true,
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/** Update a schedule */
|
|
2665
|
+
async updateSchedule(id: string, data: Record): Promise<Record> {
|
|
2666
|
+
return this.makeRequest<Record>(
|
|
2667
|
+
"PUT",
|
|
2668
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
2669
|
+
data,
|
|
2670
|
+
0,
|
|
2671
|
+
true,
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
/** Delete a schedule */
|
|
2676
|
+
async deleteSchedule(id: string): Promise<void> {
|
|
2677
|
+
await this.makeRequest<void>(
|
|
2678
|
+
"DELETE",
|
|
2679
|
+
`/api/schedules/${encodeURIComponent(id)}`,
|
|
2680
|
+
undefined,
|
|
2681
|
+
0,
|
|
2682
|
+
true,
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/** Pause a schedule */
|
|
2687
|
+
async pauseSchedule(id: string): Promise<Record> {
|
|
2688
|
+
return this.makeRequest<Record>(
|
|
2689
|
+
"POST",
|
|
2690
|
+
`/api/schedules/${encodeURIComponent(id)}/pause`,
|
|
2691
|
+
undefined,
|
|
2692
|
+
0,
|
|
2693
|
+
true,
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/** Resume a schedule */
|
|
2698
|
+
async resumeSchedule(id: string): Promise<Record> {
|
|
2699
|
+
return this.makeRequest<Record>(
|
|
2700
|
+
"POST",
|
|
2701
|
+
`/api/schedules/${encodeURIComponent(id)}/resume`,
|
|
2702
|
+
undefined,
|
|
2703
|
+
0,
|
|
2704
|
+
true,
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// ========================================================================
|
|
2709
|
+
// COLLECTION UTILITIES
|
|
2710
|
+
// ========================================================================
|
|
2711
|
+
|
|
2712
|
+
/**
|
|
2713
|
+
* Check if a collection exists
|
|
2714
|
+
* @param collection - Collection name to check
|
|
2715
|
+
* @returns true if the collection exists, false otherwise
|
|
2716
|
+
*/
|
|
2717
|
+
async collectionExists(collection: string): Promise<boolean> {
|
|
2718
|
+
try {
|
|
2719
|
+
const collections = await this.listCollections();
|
|
2720
|
+
return collections.includes(collection);
|
|
2721
|
+
} catch {
|
|
2722
|
+
return false;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* Count documents in a collection
|
|
2728
|
+
* @param collection - Collection name
|
|
2729
|
+
* @returns Number of documents in the collection
|
|
2730
|
+
*/
|
|
2731
|
+
async countDocuments(collection: string): Promise<number> {
|
|
2732
|
+
const query = new QueryBuilder().limit(100000).build();
|
|
2733
|
+
const records = await this.find(collection, query);
|
|
2734
|
+
return records.length;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
/**
|
|
2738
|
+
* Create a WebSocket client
|
|
2739
|
+
*/
|
|
2740
|
+
websocket(wsURL: string): WebSocketClient {
|
|
2741
|
+
return new WebSocketClient(wsURL, this.token!);
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// ========== RAG Helper Methods ==========
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* Generate embeddings for a single text
|
|
2748
|
+
*
|
|
2749
|
+
* @param text - The text to generate embeddings for
|
|
2750
|
+
* @param model - The embedding model to use (e.g., "text-embedding-3-small")
|
|
2751
|
+
* @returns Array of floats representing the embedding vector
|
|
2752
|
+
*
|
|
2753
|
+
* @example
|
|
2754
|
+
* ```typescript
|
|
2755
|
+
* const embedding = await client.embed(
|
|
2756
|
+
* "Hello world",
|
|
2757
|
+
* "text-embedding-3-small"
|
|
2758
|
+
* );
|
|
2759
|
+
* console.log(`Generated ${embedding.length} dimensions`);
|
|
2760
|
+
* ```
|
|
2761
|
+
*/
|
|
2762
|
+
async embed(text: string, model: string): Promise<number[]> {
|
|
2763
|
+
const response = await this.embedRequest({ text, model });
|
|
2764
|
+
if (response.embeddings.length === 0) {
|
|
2765
|
+
throw new Error("No embedding returned");
|
|
2766
|
+
}
|
|
2767
|
+
return response.embeddings[0];
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
/**
|
|
2771
|
+
* Generate embeddings for multiple texts in a single batch request
|
|
2772
|
+
*
|
|
2773
|
+
* @param texts - Array of texts to generate embeddings for
|
|
2774
|
+
* @param model - The embedding model to use
|
|
2775
|
+
* @returns Array of embedding vectors
|
|
2776
|
+
*/
|
|
1805
2777
|
async embedBatch(texts: string[], model: string): Promise<number[][]> {
|
|
1806
2778
|
const response = await this.embedRequest({ texts, model });
|
|
1807
2779
|
return response.embeddings;
|
|
@@ -1916,26 +2888,132 @@ export class EkoDBClient {
|
|
|
1916
2888
|
}
|
|
1917
2889
|
}
|
|
1918
2890
|
|
|
2891
|
+
/** Mutation notification from a subscription. */
|
|
2892
|
+
export interface MutationNotification {
|
|
2893
|
+
collection: string;
|
|
2894
|
+
event: string;
|
|
2895
|
+
recordIds: string[];
|
|
2896
|
+
records?: any;
|
|
2897
|
+
timestamp: string;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
/** A chunk/event from a streaming chat response. */
|
|
2901
|
+
export type ChatStreamEvent =
|
|
2902
|
+
| { type: "chunk"; content: string }
|
|
2903
|
+
| {
|
|
2904
|
+
type: "end";
|
|
2905
|
+
messageId: string;
|
|
2906
|
+
tokenUsage?: any;
|
|
2907
|
+
toolCallHistory?: any;
|
|
2908
|
+
executionTimeMs: number;
|
|
2909
|
+
/** Model's context window size in tokens. */
|
|
2910
|
+
contextWindow?: number;
|
|
2911
|
+
}
|
|
2912
|
+
| {
|
|
2913
|
+
type: "toolCall";
|
|
2914
|
+
chatId: string;
|
|
2915
|
+
callId: string;
|
|
2916
|
+
toolName: string;
|
|
2917
|
+
arguments: any;
|
|
2918
|
+
}
|
|
2919
|
+
| { type: "error"; error: string };
|
|
2920
|
+
|
|
2921
|
+
/** Definition for a client-side tool the LLM can call. */
|
|
2922
|
+
export interface ClientToolDefinition {
|
|
2923
|
+
name: string;
|
|
2924
|
+
description: string;
|
|
2925
|
+
parameters: any;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/** Options for chatSend. */
|
|
2929
|
+
export interface ChatSendOptions {
|
|
2930
|
+
bypassRipple?: boolean;
|
|
2931
|
+
clientTools?: ClientToolDefinition[];
|
|
2932
|
+
maxIterations?: number;
|
|
2933
|
+
confirmTools?: string[];
|
|
2934
|
+
excludeTools?: string[];
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/** Options for subscribe. */
|
|
2938
|
+
export interface SubscribeOptions {
|
|
2939
|
+
filterField?: string;
|
|
2940
|
+
filterValue?: string;
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
/** EventEmitter-like interface for subscriptions and chat streams. */
|
|
2944
|
+
export class EventStream<_T = unknown> {
|
|
2945
|
+
private listeners: Map<string, Array<(data: any) => void>> = new Map();
|
|
2946
|
+
private _closed = false;
|
|
2947
|
+
|
|
2948
|
+
on(event: string, listener: (data: any) => void): this {
|
|
2949
|
+
if (!this.listeners.has(event)) {
|
|
2950
|
+
this.listeners.set(event, []);
|
|
2951
|
+
}
|
|
2952
|
+
this.listeners.get(event)!.push(listener);
|
|
2953
|
+
return this;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
/** @internal */
|
|
2957
|
+
emit(event: string, data?: any): void {
|
|
2958
|
+
const handlers = this.listeners.get(event);
|
|
2959
|
+
if (handlers) {
|
|
2960
|
+
for (const handler of handlers) {
|
|
2961
|
+
handler(data);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
get closed(): boolean {
|
|
2967
|
+
return this._closed;
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
/** @internal */
|
|
2971
|
+
close(): void {
|
|
2972
|
+
this._closed = true;
|
|
2973
|
+
this.emit("close");
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
|
|
1919
2977
|
/**
|
|
1920
|
-
* WebSocket client for real-time queries
|
|
2978
|
+
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1921
2979
|
*/
|
|
1922
2980
|
export class WebSocketClient {
|
|
1923
2981
|
private wsURL: string;
|
|
1924
2982
|
private token: string;
|
|
1925
2983
|
private ws: any = null;
|
|
2984
|
+
private dispatcherRunning = false;
|
|
2985
|
+
|
|
2986
|
+
// Dispatcher state
|
|
2987
|
+
private pendingRequests: Map<
|
|
2988
|
+
string,
|
|
2989
|
+
{ resolve: (value: any) => void; reject: (reason: any) => void }
|
|
2990
|
+
> = new Map();
|
|
2991
|
+
private subscriptions: Map<string, EventStream<MutationNotification>> =
|
|
2992
|
+
new Map();
|
|
2993
|
+
private chatStreams: Map<string, EventStream<ChatStreamEvent>> = new Map();
|
|
2994
|
+
private registerToolsAck: {
|
|
2995
|
+
resolve: (value: any) => void;
|
|
2996
|
+
reject: (reason: any) => void;
|
|
2997
|
+
} | null = null;
|
|
1926
2998
|
|
|
1927
2999
|
constructor(wsURL: string, token: string) {
|
|
1928
3000
|
this.wsURL = wsURL;
|
|
1929
3001
|
this.token = token;
|
|
1930
3002
|
}
|
|
1931
3003
|
|
|
3004
|
+
private messageCounter = 0;
|
|
3005
|
+
|
|
3006
|
+
private genMessageId(): string {
|
|
3007
|
+
const counter = this.messageCounter++;
|
|
3008
|
+
return `${Date.now()}-${counter}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
1932
3011
|
/**
|
|
1933
|
-
* Connect
|
|
3012
|
+
* Connect and start the dispatcher.
|
|
1934
3013
|
*/
|
|
1935
|
-
private async
|
|
1936
|
-
if (this.ws) return;
|
|
3014
|
+
private async ensureConnected(): Promise<void> {
|
|
3015
|
+
if (this.ws && this.dispatcherRunning) return;
|
|
1937
3016
|
|
|
1938
|
-
// Dynamic import for Node.js WebSocket
|
|
1939
3017
|
const WebSocket = (await import("ws")).default;
|
|
1940
3018
|
|
|
1941
3019
|
let url = this.wsURL;
|
|
@@ -1949,49 +3027,369 @@ export class WebSocketClient {
|
|
|
1949
3027
|
},
|
|
1950
3028
|
});
|
|
1951
3029
|
|
|
1952
|
-
|
|
3030
|
+
await new Promise<void>((resolve, reject) => {
|
|
1953
3031
|
this.ws.on("open", () => resolve());
|
|
1954
3032
|
this.ws.on("error", (err: Error) => reject(err));
|
|
1955
3033
|
});
|
|
3034
|
+
|
|
3035
|
+
this.spawnDispatcher();
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
private spawnDispatcher(): void {
|
|
3039
|
+
if (this.dispatcherRunning) return;
|
|
3040
|
+
this.dispatcherRunning = true;
|
|
3041
|
+
|
|
3042
|
+
this.ws.on("message", (data: Buffer) => {
|
|
3043
|
+
try {
|
|
3044
|
+
const msg = JSON.parse(data.toString());
|
|
3045
|
+
this.routeMessage(msg);
|
|
3046
|
+
} catch {
|
|
3047
|
+
// Ignore malformed messages
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
|
|
3051
|
+
this.ws.on("close", () => {
|
|
3052
|
+
this.dispatcherRunning = false;
|
|
3053
|
+
// Notify all pending requests
|
|
3054
|
+
for (const [, pending] of this.pendingRequests) {
|
|
3055
|
+
pending.reject(new Error("WebSocket connection closed"));
|
|
3056
|
+
}
|
|
3057
|
+
this.pendingRequests.clear();
|
|
3058
|
+
// Close all chat streams
|
|
3059
|
+
for (const [, stream] of this.chatStreams) {
|
|
3060
|
+
stream.emit("event", { type: "error", error: "Connection closed" });
|
|
3061
|
+
stream.close();
|
|
3062
|
+
}
|
|
3063
|
+
this.chatStreams.clear();
|
|
3064
|
+
// Close all subscriptions
|
|
3065
|
+
for (const [, stream] of this.subscriptions) {
|
|
3066
|
+
stream.close();
|
|
3067
|
+
}
|
|
3068
|
+
this.subscriptions.clear();
|
|
3069
|
+
this.ws = null;
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
private routeMessage(msg: any): void {
|
|
3074
|
+
switch (msg.type) {
|
|
3075
|
+
case "Success":
|
|
3076
|
+
case "Error": {
|
|
3077
|
+
// Try messageId from top-level, then from payload
|
|
3078
|
+
const messageId =
|
|
3079
|
+
msg.messageId ||
|
|
3080
|
+
msg.message_id ||
|
|
3081
|
+
msg.payload?.message_id ||
|
|
3082
|
+
msg.payload?.messageId;
|
|
3083
|
+
let matched = false;
|
|
3084
|
+
if (messageId && this.pendingRequests.has(messageId)) {
|
|
3085
|
+
const pending = this.pendingRequests.get(messageId)!;
|
|
3086
|
+
this.pendingRequests.delete(messageId);
|
|
3087
|
+
if (msg.type === "Error") {
|
|
3088
|
+
pending.reject(new Error(msg.message || "Unknown error"));
|
|
3089
|
+
} else {
|
|
3090
|
+
pending.resolve(msg.payload);
|
|
3091
|
+
}
|
|
3092
|
+
matched = true;
|
|
3093
|
+
}
|
|
3094
|
+
if (!matched && this.registerToolsAck) {
|
|
3095
|
+
const ack = this.registerToolsAck;
|
|
3096
|
+
this.registerToolsAck = null;
|
|
3097
|
+
if (msg.type === "Error") {
|
|
3098
|
+
ack.reject(new Error(msg.message || "Tool registration failed"));
|
|
3099
|
+
} else {
|
|
3100
|
+
ack.resolve(msg.payload);
|
|
3101
|
+
}
|
|
3102
|
+
matched = true;
|
|
3103
|
+
}
|
|
3104
|
+
// Server doesn't echo messageId — if there's exactly one pending
|
|
3105
|
+
// request, deliver the response to it (sequential request/response).
|
|
3106
|
+
if (!matched && this.pendingRequests.size === 1) {
|
|
3107
|
+
const entry = this.pendingRequests.entries().next().value!;
|
|
3108
|
+
const key = entry[0];
|
|
3109
|
+
const pending = entry[1];
|
|
3110
|
+
this.pendingRequests.delete(key);
|
|
3111
|
+
if (msg.type === "Error") {
|
|
3112
|
+
pending.reject(new Error(msg.message || "Unknown error"));
|
|
3113
|
+
} else {
|
|
3114
|
+
pending.resolve(msg.payload);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
break;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
case "MutationNotification": {
|
|
3121
|
+
const payload = msg.payload;
|
|
3122
|
+
const notification: MutationNotification = {
|
|
3123
|
+
collection: payload.collection,
|
|
3124
|
+
event: payload.event,
|
|
3125
|
+
recordIds: payload.record_ids || payload.recordIds || [],
|
|
3126
|
+
records: payload.records,
|
|
3127
|
+
timestamp: payload.timestamp,
|
|
3128
|
+
};
|
|
3129
|
+
for (const [collection, stream] of this.subscriptions) {
|
|
3130
|
+
if (collection === notification.collection) {
|
|
3131
|
+
stream.emit("mutation", notification);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
break;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
case "ChatStreamChunk": {
|
|
3138
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
3139
|
+
const stream = this.chatStreams.get(chatId);
|
|
3140
|
+
if (stream) {
|
|
3141
|
+
stream.emit("event", {
|
|
3142
|
+
type: "chunk",
|
|
3143
|
+
content: msg.payload.content,
|
|
3144
|
+
} as ChatStreamEvent);
|
|
3145
|
+
}
|
|
3146
|
+
break;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
case "ChatStreamEnd": {
|
|
3150
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
3151
|
+
const stream = this.chatStreams.get(chatId);
|
|
3152
|
+
if (stream) {
|
|
3153
|
+
stream.emit("event", {
|
|
3154
|
+
type: "end",
|
|
3155
|
+
messageId: msg.payload.message_id || msg.payload.messageId || "",
|
|
3156
|
+
tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
|
|
3157
|
+
toolCallHistory:
|
|
3158
|
+
msg.payload.tool_call_history || msg.payload.toolCallHistory,
|
|
3159
|
+
executionTimeMs:
|
|
3160
|
+
msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
|
|
3161
|
+
contextWindow:
|
|
3162
|
+
msg.payload.context_window || msg.payload.contextWindow,
|
|
3163
|
+
} as ChatStreamEvent);
|
|
3164
|
+
this.chatStreams.delete(chatId);
|
|
3165
|
+
stream.close();
|
|
3166
|
+
}
|
|
3167
|
+
break;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
case "ChatStreamError": {
|
|
3171
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
3172
|
+
const stream = this.chatStreams.get(chatId);
|
|
3173
|
+
if (stream) {
|
|
3174
|
+
stream.emit("event", {
|
|
3175
|
+
type: "error",
|
|
3176
|
+
error: msg.payload.error || msg.payload.message || "Unknown error",
|
|
3177
|
+
} as ChatStreamEvent);
|
|
3178
|
+
this.chatStreams.delete(chatId);
|
|
3179
|
+
stream.close();
|
|
3180
|
+
}
|
|
3181
|
+
break;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
case "ClientToolCall": {
|
|
3185
|
+
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
3186
|
+
const stream = this.chatStreams.get(chatId);
|
|
3187
|
+
if (stream) {
|
|
3188
|
+
stream.emit("event", {
|
|
3189
|
+
type: "toolCall",
|
|
3190
|
+
chatId,
|
|
3191
|
+
callId: msg.payload.call_id || msg.payload.callId,
|
|
3192
|
+
toolName: msg.payload.tool_name || msg.payload.toolName,
|
|
3193
|
+
arguments: msg.payload.arguments,
|
|
3194
|
+
} as ChatStreamEvent);
|
|
3195
|
+
}
|
|
3196
|
+
break;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
private async sendRequest(request: any): Promise<any> {
|
|
3202
|
+
await this.ensureConnected();
|
|
3203
|
+
const messageId = request.messageId || request.message_id;
|
|
3204
|
+
|
|
3205
|
+
return new Promise((resolve, reject) => {
|
|
3206
|
+
this.pendingRequests.set(messageId, { resolve, reject });
|
|
3207
|
+
try {
|
|
3208
|
+
this.ws.send(JSON.stringify(request));
|
|
3209
|
+
} catch (err) {
|
|
3210
|
+
this.pendingRequests.delete(messageId);
|
|
3211
|
+
reject(err);
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
1956
3214
|
}
|
|
1957
3215
|
|
|
1958
3216
|
/**
|
|
1959
|
-
* Find all records in a collection via WebSocket
|
|
3217
|
+
* Find all records in a collection via WebSocket.
|
|
1960
3218
|
*/
|
|
1961
3219
|
async findAll(collection: string): Promise<Record[]> {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
const messageId = Date.now().toString();
|
|
1965
|
-
const request = {
|
|
3220
|
+
const messageId = this.genMessageId();
|
|
3221
|
+
const payload = await this.sendRequest({
|
|
1966
3222
|
type: "FindAll",
|
|
1967
3223
|
messageId,
|
|
1968
3224
|
payload: { collection },
|
|
3225
|
+
});
|
|
3226
|
+
return payload?.data || [];
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
/**
|
|
3230
|
+
* Subscribe to mutation notifications on a collection.
|
|
3231
|
+
* Returns an EventStream that emits "mutation" events.
|
|
3232
|
+
*/
|
|
3233
|
+
async subscribe(
|
|
3234
|
+
collection: string,
|
|
3235
|
+
options?: SubscribeOptions,
|
|
3236
|
+
): Promise<EventStream<MutationNotification>> {
|
|
3237
|
+
await this.ensureConnected();
|
|
3238
|
+
|
|
3239
|
+
if (this.subscriptions.has(collection)) {
|
|
3240
|
+
throw new Error(`Already subscribed to collection "${collection}"`);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
const messageId = this.genMessageId();
|
|
3244
|
+
const stream = new EventStream<MutationNotification>();
|
|
3245
|
+
this.subscriptions.set(collection, stream);
|
|
3246
|
+
|
|
3247
|
+
const request: any = {
|
|
3248
|
+
type: "Subscribe",
|
|
3249
|
+
messageId,
|
|
3250
|
+
payload: {
|
|
3251
|
+
collection,
|
|
3252
|
+
...(options?.filterField && { filter_field: options.filterField }),
|
|
3253
|
+
...(options?.filterValue && { filter_value: options.filterValue }),
|
|
3254
|
+
},
|
|
1969
3255
|
};
|
|
1970
3256
|
|
|
1971
|
-
|
|
3257
|
+
// Send subscribe request and wait for ack
|
|
3258
|
+
try {
|
|
3259
|
+
await this.sendRequest(request);
|
|
3260
|
+
} catch (err) {
|
|
3261
|
+
this.subscriptions.delete(collection);
|
|
3262
|
+
throw err;
|
|
3263
|
+
}
|
|
3264
|
+
return stream;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/**
|
|
3268
|
+
* Send a chat message and receive a streaming response.
|
|
3269
|
+
* Returns an EventStream that emits "event" with ChatStreamEvent objects.
|
|
3270
|
+
*/
|
|
3271
|
+
async chatSend(
|
|
3272
|
+
chatId: string,
|
|
3273
|
+
message: string,
|
|
3274
|
+
options?: ChatSendOptions,
|
|
3275
|
+
): Promise<EventStream<ChatStreamEvent>> {
|
|
3276
|
+
await this.ensureConnected();
|
|
3277
|
+
|
|
3278
|
+
if (this.chatStreams.has(chatId)) {
|
|
3279
|
+
throw new Error(`Chat stream already active for chatId "${chatId}"`);
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
const stream = new EventStream<ChatStreamEvent>();
|
|
3283
|
+
this.chatStreams.set(chatId, stream);
|
|
3284
|
+
|
|
3285
|
+
const request: any = {
|
|
3286
|
+
type: "ChatSend",
|
|
3287
|
+
payload: {
|
|
3288
|
+
chat_id: chatId,
|
|
3289
|
+
message,
|
|
3290
|
+
...(options?.bypassRipple != null && {
|
|
3291
|
+
bypass_ripple: options.bypassRipple,
|
|
3292
|
+
}),
|
|
3293
|
+
...(options?.clientTools && { client_tools: options.clientTools }),
|
|
3294
|
+
...(options?.maxIterations != null && {
|
|
3295
|
+
max_iterations: options.maxIterations,
|
|
3296
|
+
}),
|
|
3297
|
+
...(options?.confirmTools && { confirm_tools: options.confirmTools }),
|
|
3298
|
+
...(options?.excludeTools && { exclude_tools: options.excludeTools }),
|
|
3299
|
+
},
|
|
3300
|
+
};
|
|
3301
|
+
|
|
3302
|
+
this.ws.send(JSON.stringify(request));
|
|
3303
|
+
return stream;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/**
|
|
3307
|
+
* Register client-side tools for a chat session.
|
|
3308
|
+
*/
|
|
3309
|
+
async registerClientTools(
|
|
3310
|
+
chatId: string,
|
|
3311
|
+
tools: ClientToolDefinition[],
|
|
3312
|
+
): Promise<void> {
|
|
3313
|
+
await this.ensureConnected();
|
|
3314
|
+
|
|
3315
|
+
const request = {
|
|
3316
|
+
type: "RegisterClientTools",
|
|
3317
|
+
payload: {
|
|
3318
|
+
chat_id: chatId,
|
|
3319
|
+
tools,
|
|
3320
|
+
},
|
|
3321
|
+
};
|
|
3322
|
+
|
|
3323
|
+
await new Promise<void>((resolve, reject) => {
|
|
3324
|
+
this.registerToolsAck = {
|
|
3325
|
+
resolve: () => resolve(),
|
|
3326
|
+
reject: (err) => reject(err),
|
|
3327
|
+
};
|
|
1972
3328
|
this.ws.send(JSON.stringify(request));
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
1973
3331
|
|
|
1974
|
-
|
|
1975
|
-
|
|
3332
|
+
/**
|
|
3333
|
+
* Send a tool result back to the server during a chat stream.
|
|
3334
|
+
*/
|
|
3335
|
+
async sendToolResult(
|
|
3336
|
+
chatId: string,
|
|
3337
|
+
callId: string,
|
|
3338
|
+
success: boolean,
|
|
3339
|
+
result?: any,
|
|
3340
|
+
error?: string,
|
|
3341
|
+
): Promise<void> {
|
|
3342
|
+
await this.ensureConnected();
|
|
1976
3343
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
3344
|
+
const request = {
|
|
3345
|
+
type: "ClientToolResult",
|
|
3346
|
+
payload: {
|
|
3347
|
+
chat_id: chatId,
|
|
3348
|
+
call_id: callId,
|
|
3349
|
+
success,
|
|
3350
|
+
...(result !== undefined && { result }),
|
|
3351
|
+
...(error !== undefined && { error }),
|
|
3352
|
+
},
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
this.ws.send(JSON.stringify(request));
|
|
3356
|
+
}
|
|
1983
3357
|
|
|
1984
|
-
|
|
3358
|
+
/**
|
|
3359
|
+
* Stateless raw LLM completion via WebSocket.
|
|
3360
|
+
*
|
|
3361
|
+
* Sends a RawComplete message and waits for the Success response.
|
|
3362
|
+
* Preferred over HTTP for deployed instances: the persistent WSS
|
|
3363
|
+
* connection is already authenticated and won't be killed by reverse
|
|
3364
|
+
* proxy timeouts.
|
|
3365
|
+
*/
|
|
3366
|
+
async rawCompletion(
|
|
3367
|
+
request: RawCompletionRequest,
|
|
3368
|
+
): Promise<RawCompletionResponse> {
|
|
3369
|
+
await this.ensureConnected();
|
|
3370
|
+
const messageId = this.genMessageId();
|
|
3371
|
+
const payload = await this.sendRequest({
|
|
3372
|
+
type: "RawComplete",
|
|
3373
|
+
messageId,
|
|
3374
|
+
payload: {
|
|
3375
|
+
system_prompt: request.system_prompt,
|
|
3376
|
+
message: request.message,
|
|
3377
|
+
...(request.provider && { provider: request.provider }),
|
|
3378
|
+
...(request.model && { model: request.model }),
|
|
3379
|
+
...(request.max_tokens != null && { max_tokens: request.max_tokens }),
|
|
3380
|
+
},
|
|
1985
3381
|
});
|
|
3382
|
+
return { content: payload?.data?.content || "" };
|
|
1986
3383
|
}
|
|
1987
3384
|
|
|
1988
3385
|
/**
|
|
1989
|
-
* Close the WebSocket connection
|
|
3386
|
+
* Close the WebSocket connection.
|
|
1990
3387
|
*/
|
|
1991
3388
|
close(): void {
|
|
1992
3389
|
if (this.ws) {
|
|
1993
3390
|
this.ws.close();
|
|
1994
3391
|
this.ws = null;
|
|
3392
|
+
this.dispatcherRunning = false;
|
|
1995
3393
|
}
|
|
1996
3394
|
}
|
|
1997
3395
|
}
|