@chaoslabs/ai-sdk 0.0.2 → 0.0.4
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 +46 -1
- package/dist/__tests__/http-streaming.test.d.ts +15 -0
- package/dist/__tests__/http-streaming.test.js +1401 -0
- package/dist/__tests__/stream.test.d.ts +1 -0
- package/dist/__tests__/stream.test.js +345 -0
- package/dist/__tests__/trivial.test.d.ts +1 -0
- package/dist/__tests__/trivial.test.js +6 -0
- package/dist/blocks.d.ts +108 -0
- package/dist/blocks.js +246 -0
- package/dist/client.d.ts +21 -5
- package/dist/client.js +75 -29
- package/dist/conversation.d.ts +136 -0
- package/dist/conversation.js +230 -0
- package/dist/http-streaming.d.ts +55 -0
- package/dist/http-streaming.js +359 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +157 -0
- package/dist/primitives.js +220 -0
- package/dist/schemas.d.ts +6 -6
- package/dist/stream.d.ts +32 -0
- package/dist/stream.js +127 -0
- package/dist/types.d.ts +11 -0
- package/package.json +2 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Chaos AI SDK - Conversation Management
|
|
2
|
+
//
|
|
3
|
+
// A class for managing multi-turn conversations with history tracking.
|
|
4
|
+
import { extractText } from './types.js';
|
|
5
|
+
import { WALLET_MODEL } from './request.js';
|
|
6
|
+
/**
|
|
7
|
+
* Manages a multi-turn conversation with history tracking.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { Chaos, Conversation } from '@chaoslabs/ai-sdk';
|
|
12
|
+
*
|
|
13
|
+
* const chaos = new Chaos({ apiKey: 'ck-...' });
|
|
14
|
+
* const conversation = new Conversation(chaos, {
|
|
15
|
+
* userId: 'user-123',
|
|
16
|
+
* walletId: '0x...',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Send messages - history is tracked automatically
|
|
20
|
+
* const response1 = await conversation.send("What's my portfolio value?");
|
|
21
|
+
* const response2 = await conversation.send("Which asset has the highest allocation?");
|
|
22
|
+
*
|
|
23
|
+
* // Get conversation stats
|
|
24
|
+
* console.log(conversation.stats);
|
|
25
|
+
*
|
|
26
|
+
* // Reset for a new conversation
|
|
27
|
+
* conversation.reset();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class Conversation {
|
|
31
|
+
client;
|
|
32
|
+
model;
|
|
33
|
+
maxHistoryLength;
|
|
34
|
+
history = [];
|
|
35
|
+
metadata;
|
|
36
|
+
userTurns = 0;
|
|
37
|
+
assistantTurns = 0;
|
|
38
|
+
startedAt;
|
|
39
|
+
lastMessageAt = null;
|
|
40
|
+
constructor(client, options) {
|
|
41
|
+
this.client = client;
|
|
42
|
+
this.model = options.model || WALLET_MODEL;
|
|
43
|
+
this.maxHistoryLength = options.maxHistoryLength || 50;
|
|
44
|
+
this.startedAt = new Date();
|
|
45
|
+
this.metadata = {
|
|
46
|
+
user_id: options.userId,
|
|
47
|
+
wallet_id: options.walletId,
|
|
48
|
+
session_id: options.sessionId || `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get the current session ID.
|
|
53
|
+
*/
|
|
54
|
+
get sessionId() {
|
|
55
|
+
return this.metadata.session_id;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the conversation history.
|
|
59
|
+
*/
|
|
60
|
+
get messages() {
|
|
61
|
+
return [...this.history];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get conversation statistics.
|
|
65
|
+
*/
|
|
66
|
+
get stats() {
|
|
67
|
+
return {
|
|
68
|
+
userTurns: this.userTurns,
|
|
69
|
+
assistantTurns: this.assistantTurns,
|
|
70
|
+
totalMessages: this.history.length,
|
|
71
|
+
sessionId: this.metadata.session_id,
|
|
72
|
+
startedAt: this.startedAt,
|
|
73
|
+
lastMessageAt: this.lastMessageAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Send a message and get a response.
|
|
78
|
+
* Automatically manages conversation history.
|
|
79
|
+
*/
|
|
80
|
+
async send(message) {
|
|
81
|
+
// Add user message to history
|
|
82
|
+
this.addUserMessage(message);
|
|
83
|
+
// Send request with full history
|
|
84
|
+
const response = await this.client.chat.responses.create({
|
|
85
|
+
model: this.model,
|
|
86
|
+
input: [...this.history],
|
|
87
|
+
metadata: this.metadata,
|
|
88
|
+
});
|
|
89
|
+
// Add assistant response to history if successful
|
|
90
|
+
if (response.status === 'completed') {
|
|
91
|
+
const text = extractText(response);
|
|
92
|
+
if (text) {
|
|
93
|
+
this.addAssistantMessage(text);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.lastMessageAt = new Date();
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Add a user message to the history without sending.
|
|
101
|
+
* Useful for restoring conversation state.
|
|
102
|
+
*/
|
|
103
|
+
addUserMessage(content) {
|
|
104
|
+
this.history.push({
|
|
105
|
+
type: 'message',
|
|
106
|
+
role: 'user',
|
|
107
|
+
content,
|
|
108
|
+
});
|
|
109
|
+
this.userTurns++;
|
|
110
|
+
this.trimHistory();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Add an assistant message to the history.
|
|
114
|
+
* Useful for restoring conversation state.
|
|
115
|
+
*/
|
|
116
|
+
addAssistantMessage(content) {
|
|
117
|
+
// Store assistant responses as user messages with prefix for context
|
|
118
|
+
// This matches the pattern used in the multi-turn example
|
|
119
|
+
this.history.push({
|
|
120
|
+
type: 'message',
|
|
121
|
+
role: 'user',
|
|
122
|
+
content: `[Assistant]: ${content}`,
|
|
123
|
+
});
|
|
124
|
+
this.assistantTurns++;
|
|
125
|
+
this.trimHistory();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Add a system message to the history.
|
|
129
|
+
*/
|
|
130
|
+
addSystemMessage(content) {
|
|
131
|
+
this.history.push({
|
|
132
|
+
type: 'message',
|
|
133
|
+
role: 'system',
|
|
134
|
+
content,
|
|
135
|
+
});
|
|
136
|
+
this.trimHistory();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reset the conversation to start fresh.
|
|
140
|
+
* Generates a new session ID and clears history.
|
|
141
|
+
*/
|
|
142
|
+
reset() {
|
|
143
|
+
this.history = [];
|
|
144
|
+
this.userTurns = 0;
|
|
145
|
+
this.assistantTurns = 0;
|
|
146
|
+
this.startedAt = new Date();
|
|
147
|
+
this.lastMessageAt = null;
|
|
148
|
+
this.metadata.session_id = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Clear the history but keep the same session ID.
|
|
152
|
+
*/
|
|
153
|
+
clearHistory() {
|
|
154
|
+
this.history = [];
|
|
155
|
+
this.userTurns = 0;
|
|
156
|
+
this.assistantTurns = 0;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Fork this conversation to create a branch.
|
|
160
|
+
* The new conversation has the same history but a new session ID.
|
|
161
|
+
*/
|
|
162
|
+
fork() {
|
|
163
|
+
const forked = new Conversation(this.client, {
|
|
164
|
+
model: this.model,
|
|
165
|
+
maxHistoryLength: this.maxHistoryLength,
|
|
166
|
+
userId: this.metadata.user_id,
|
|
167
|
+
walletId: this.metadata.wallet_id,
|
|
168
|
+
});
|
|
169
|
+
// Copy history
|
|
170
|
+
for (const msg of this.history) {
|
|
171
|
+
if (msg.role === 'user') {
|
|
172
|
+
if (msg.content.startsWith('[Assistant]: ')) {
|
|
173
|
+
forked.history.push(msg);
|
|
174
|
+
forked.assistantTurns++;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
forked.history.push(msg);
|
|
178
|
+
forked.userTurns++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
forked.history.push(msg);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return forked;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Serialize the conversation to JSON for persistence.
|
|
189
|
+
*/
|
|
190
|
+
toJSON() {
|
|
191
|
+
return {
|
|
192
|
+
sessionId: this.metadata.session_id,
|
|
193
|
+
history: [...this.history],
|
|
194
|
+
metadata: { ...this.metadata },
|
|
195
|
+
stats: this.stats,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Restore conversation state from JSON.
|
|
200
|
+
*/
|
|
201
|
+
static fromJSON(client, data) {
|
|
202
|
+
const conversation = new Conversation(client, {
|
|
203
|
+
userId: data.metadata.user_id,
|
|
204
|
+
walletId: data.metadata.wallet_id,
|
|
205
|
+
sessionId: data.sessionId,
|
|
206
|
+
});
|
|
207
|
+
// Restore history
|
|
208
|
+
for (const msg of data.history) {
|
|
209
|
+
conversation.history.push(msg);
|
|
210
|
+
if (msg.role === 'user') {
|
|
211
|
+
if (msg.content.startsWith('[Assistant]: ')) {
|
|
212
|
+
conversation.assistantTurns++;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
conversation.userTurns++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return conversation;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Trim history to stay within maxHistoryLength.
|
|
223
|
+
*/
|
|
224
|
+
trimHistory() {
|
|
225
|
+
if (this.history.length > this.maxHistoryLength) {
|
|
226
|
+
const excess = this.history.length - this.maxHistoryLength;
|
|
227
|
+
this.history = this.history.slice(excess);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface HttpStreamOptions {
|
|
2
|
+
url: string;
|
|
3
|
+
method: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
body: string;
|
|
6
|
+
timeout: number;
|
|
7
|
+
}
|
|
8
|
+
export interface StreamingHttpClientOptions {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
timeout: number;
|
|
11
|
+
}
|
|
12
|
+
export interface StreamRequestOptions {
|
|
13
|
+
method: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
body: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Makes an HTTP request using Node's native http/https modules and returns
|
|
19
|
+
* an async iterator that yields chunks as they arrive (true streaming).
|
|
20
|
+
*
|
|
21
|
+
* @param options - Request options
|
|
22
|
+
* @returns AsyncIterable that yields string chunks
|
|
23
|
+
*/
|
|
24
|
+
export declare function httpStreamRequest(options: HttpStreamOptions): AsyncIterable<string>;
|
|
25
|
+
/**
|
|
26
|
+
* High-level HTTP streaming client that provides methods for streaming
|
|
27
|
+
* requests with proper chunk and line parsing.
|
|
28
|
+
*/
|
|
29
|
+
export declare class StreamingHttpClient {
|
|
30
|
+
private baseUrl;
|
|
31
|
+
private timeout;
|
|
32
|
+
private aborted;
|
|
33
|
+
private currentIterator;
|
|
34
|
+
constructor(options: StreamingHttpClientOptions);
|
|
35
|
+
/**
|
|
36
|
+
* Stream raw chunks from a request.
|
|
37
|
+
*
|
|
38
|
+
* @param path - URL path (will be appended to baseUrl)
|
|
39
|
+
* @param options - Request options
|
|
40
|
+
* @returns AsyncIterable of raw string chunks
|
|
41
|
+
*/
|
|
42
|
+
stream(path: string, options: StreamRequestOptions): AsyncIterable<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Stream parsed NDJSON lines from a request.
|
|
45
|
+
*
|
|
46
|
+
* @param path - URL path (will be appended to baseUrl)
|
|
47
|
+
* @param options - Request options
|
|
48
|
+
* @returns AsyncIterable of complete JSON lines (without newline)
|
|
49
|
+
*/
|
|
50
|
+
streamLines(path: string, options: StreamRequestOptions): AsyncIterable<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Abort the current in-progress request.
|
|
53
|
+
*/
|
|
54
|
+
abort(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// HTTP Streaming Module
|
|
2
|
+
//
|
|
3
|
+
// This module provides low-level HTTP streaming using Node's native http/https modules.
|
|
4
|
+
// It yields data incrementally as it arrives, unlike fetch which buffers the response.
|
|
5
|
+
import * as http from 'node:http';
|
|
6
|
+
import * as https from 'node:https';
|
|
7
|
+
import { ChaosError, ChaosTimeoutError } from './types.js';
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// httpStreamRequest - Low-level streaming function
|
|
10
|
+
// ============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Makes an HTTP request using Node's native http/https modules and returns
|
|
13
|
+
* an async iterator that yields chunks as they arrive (true streaming).
|
|
14
|
+
*
|
|
15
|
+
* @param options - Request options
|
|
16
|
+
* @returns AsyncIterable that yields string chunks
|
|
17
|
+
*/
|
|
18
|
+
export function httpStreamRequest(options) {
|
|
19
|
+
const { url, method, headers, body, timeout } = options;
|
|
20
|
+
return {
|
|
21
|
+
[Symbol.asyncIterator]() {
|
|
22
|
+
const parsedUrl = new URL(url);
|
|
23
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
24
|
+
const httpModule = isHttps ? https : http;
|
|
25
|
+
let request = null;
|
|
26
|
+
let response = null;
|
|
27
|
+
let destroyed = false;
|
|
28
|
+
let pendingChunks = [];
|
|
29
|
+
let resolveNext = null;
|
|
30
|
+
let rejectNext = null;
|
|
31
|
+
let ended = false;
|
|
32
|
+
let error = null;
|
|
33
|
+
let requestStarted = false;
|
|
34
|
+
let requestPromise = null;
|
|
35
|
+
// Store timeout reference so we can restart it
|
|
36
|
+
let timeoutId = null;
|
|
37
|
+
// Store the startRequest reject function so timeout can use it
|
|
38
|
+
let startRequestReject = null;
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
if (timeoutId) {
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
timeoutId = null;
|
|
43
|
+
}
|
|
44
|
+
if (request && !destroyed) {
|
|
45
|
+
destroyed = true;
|
|
46
|
+
request.destroy();
|
|
47
|
+
}
|
|
48
|
+
if (response) {
|
|
49
|
+
response.removeAllListeners();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const resetTimeout = () => {
|
|
53
|
+
if (timeoutId) {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
}
|
|
56
|
+
if (!ended && !destroyed && !error) {
|
|
57
|
+
timeoutId = setTimeout(() => {
|
|
58
|
+
const err = new ChaosTimeoutError(timeout);
|
|
59
|
+
// Set error BEFORE cleanup so that handleEnd doesn't resolve
|
|
60
|
+
error = err;
|
|
61
|
+
// Reject the appropriate promise
|
|
62
|
+
if (rejectNext) {
|
|
63
|
+
// We're waiting for data during iteration
|
|
64
|
+
const reject = rejectNext;
|
|
65
|
+
rejectNext = null;
|
|
66
|
+
resolveNext = null;
|
|
67
|
+
reject(err);
|
|
68
|
+
}
|
|
69
|
+
else if (startRequestReject) {
|
|
70
|
+
// We're still waiting for the response headers
|
|
71
|
+
const reject = startRequestReject;
|
|
72
|
+
startRequestReject = null;
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
cleanup();
|
|
76
|
+
}, timeout);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const handleError = (err) => {
|
|
80
|
+
if (error)
|
|
81
|
+
return; // Already have an error
|
|
82
|
+
error = err;
|
|
83
|
+
cleanup();
|
|
84
|
+
if (rejectNext) {
|
|
85
|
+
rejectNext(err);
|
|
86
|
+
rejectNext = null;
|
|
87
|
+
resolveNext = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const handleData = (chunk) => {
|
|
91
|
+
// Reset timeout on each data chunk
|
|
92
|
+
resetTimeout();
|
|
93
|
+
const str = chunk.toString('utf8');
|
|
94
|
+
if (resolveNext) {
|
|
95
|
+
resolveNext({ value: str, done: false });
|
|
96
|
+
resolveNext = null;
|
|
97
|
+
rejectNext = null;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
pendingChunks.push(str);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const handleEnd = () => {
|
|
104
|
+
ended = true;
|
|
105
|
+
// Don't cleanup yet - let the caller handle it
|
|
106
|
+
if (timeoutId) {
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
timeoutId = null;
|
|
109
|
+
}
|
|
110
|
+
if (resolveNext && !error) {
|
|
111
|
+
resolveNext({ value: undefined, done: true });
|
|
112
|
+
resolveNext = null;
|
|
113
|
+
rejectNext = null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// Start the request
|
|
117
|
+
const requestOptions = {
|
|
118
|
+
hostname: parsedUrl.hostname,
|
|
119
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
120
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
121
|
+
method,
|
|
122
|
+
headers: {
|
|
123
|
+
...headers,
|
|
124
|
+
'Content-Length': Buffer.byteLength(body, 'utf8').toString(),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const startRequest = () => {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
// Store reject so timeout can use it
|
|
130
|
+
startRequestReject = reject;
|
|
131
|
+
// Start initial timeout
|
|
132
|
+
resetTimeout();
|
|
133
|
+
request = httpModule.request(requestOptions, (res) => {
|
|
134
|
+
response = res;
|
|
135
|
+
// Reset timeout when we get response
|
|
136
|
+
resetTimeout();
|
|
137
|
+
// Check for HTTP errors
|
|
138
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
139
|
+
let errorBody = '';
|
|
140
|
+
res.on('data', (chunk) => {
|
|
141
|
+
errorBody += chunk.toString();
|
|
142
|
+
});
|
|
143
|
+
res.on('end', () => {
|
|
144
|
+
const err = new ChaosError(`HTTP error: ${res.statusCode} ${res.statusMessage || ''}`, res.statusCode);
|
|
145
|
+
handleError(err);
|
|
146
|
+
reject(err);
|
|
147
|
+
});
|
|
148
|
+
res.on('error', () => {
|
|
149
|
+
// Ignore errors during error body collection
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Set up data handlers
|
|
154
|
+
res.on('data', handleData);
|
|
155
|
+
res.on('end', handleEnd);
|
|
156
|
+
res.on('error', (err) => {
|
|
157
|
+
handleError(new ChaosError(`Stream error: ${err.message}`));
|
|
158
|
+
});
|
|
159
|
+
res.on('close', () => {
|
|
160
|
+
// If closed without ending properly, it's an error
|
|
161
|
+
if (!ended && !error) {
|
|
162
|
+
handleError(new ChaosError('Connection closed unexpectedly'));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Clear startRequestReject since we've resolved
|
|
166
|
+
startRequestReject = null;
|
|
167
|
+
resolve();
|
|
168
|
+
});
|
|
169
|
+
request.on('error', (err) => {
|
|
170
|
+
const chaosErr = new ChaosError(`Connection error: ${err.message}`);
|
|
171
|
+
handleError(chaosErr);
|
|
172
|
+
reject(chaosErr);
|
|
173
|
+
});
|
|
174
|
+
request.on('timeout', () => {
|
|
175
|
+
const err = new ChaosTimeoutError(timeout);
|
|
176
|
+
handleError(err);
|
|
177
|
+
reject(err);
|
|
178
|
+
});
|
|
179
|
+
// Send the request body
|
|
180
|
+
request.write(body);
|
|
181
|
+
request.end();
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
async next() {
|
|
186
|
+
// Start request on first next() call
|
|
187
|
+
if (!requestStarted) {
|
|
188
|
+
requestStarted = true;
|
|
189
|
+
requestPromise = startRequest();
|
|
190
|
+
try {
|
|
191
|
+
await requestPromise;
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Check for error
|
|
198
|
+
if (error) {
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
// Check for pending chunks
|
|
202
|
+
if (pendingChunks.length > 0) {
|
|
203
|
+
return { value: pendingChunks.shift(), done: false };
|
|
204
|
+
}
|
|
205
|
+
// Check if ended
|
|
206
|
+
if (ended) {
|
|
207
|
+
return { value: undefined, done: true };
|
|
208
|
+
}
|
|
209
|
+
// Wait for next chunk (timeout will fire if nothing comes)
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
resolveNext = resolve;
|
|
212
|
+
rejectNext = reject;
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
async return() {
|
|
216
|
+
cleanup();
|
|
217
|
+
return { value: undefined, done: true };
|
|
218
|
+
},
|
|
219
|
+
async throw(err) {
|
|
220
|
+
cleanup();
|
|
221
|
+
throw err;
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// StreamingHttpClient - High-level streaming client
|
|
229
|
+
// ============================================================================
|
|
230
|
+
/**
|
|
231
|
+
* High-level HTTP streaming client that provides methods for streaming
|
|
232
|
+
* requests with proper chunk and line parsing.
|
|
233
|
+
*/
|
|
234
|
+
export class StreamingHttpClient {
|
|
235
|
+
baseUrl;
|
|
236
|
+
timeout;
|
|
237
|
+
aborted = false;
|
|
238
|
+
currentIterator = null;
|
|
239
|
+
constructor(options) {
|
|
240
|
+
this.baseUrl = options.baseUrl;
|
|
241
|
+
this.timeout = options.timeout;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Stream raw chunks from a request.
|
|
245
|
+
*
|
|
246
|
+
* @param path - URL path (will be appended to baseUrl)
|
|
247
|
+
* @param options - Request options
|
|
248
|
+
* @returns AsyncIterable of raw string chunks
|
|
249
|
+
*/
|
|
250
|
+
stream(path, options) {
|
|
251
|
+
const fullUrl = this.baseUrl + path;
|
|
252
|
+
this.aborted = false;
|
|
253
|
+
const self = this;
|
|
254
|
+
const innerIterable = httpStreamRequest({
|
|
255
|
+
url: fullUrl,
|
|
256
|
+
method: options.method,
|
|
257
|
+
headers: options.headers || {},
|
|
258
|
+
body: options.body,
|
|
259
|
+
timeout: this.timeout,
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
[Symbol.asyncIterator]() {
|
|
263
|
+
const iterator = innerIterable[Symbol.asyncIterator]();
|
|
264
|
+
self.currentIterator = iterator;
|
|
265
|
+
return {
|
|
266
|
+
async next() {
|
|
267
|
+
if (self.aborted) {
|
|
268
|
+
// Call return to cleanup the underlying iterator
|
|
269
|
+
await iterator.return?.();
|
|
270
|
+
throw new ChaosError('Request aborted');
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const result = await iterator.next();
|
|
274
|
+
if (self.aborted) {
|
|
275
|
+
await iterator.return?.();
|
|
276
|
+
throw new ChaosError('Request aborted');
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
if (self.aborted) {
|
|
282
|
+
throw new ChaosError('Request aborted');
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
async return() {
|
|
288
|
+
self.currentIterator = null;
|
|
289
|
+
return iterator.return?.() || { value: undefined, done: true };
|
|
290
|
+
},
|
|
291
|
+
async throw(err) {
|
|
292
|
+
self.currentIterator = null;
|
|
293
|
+
return iterator.throw?.(err) || Promise.reject(err);
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Stream parsed NDJSON lines from a request.
|
|
301
|
+
*
|
|
302
|
+
* @param path - URL path (will be appended to baseUrl)
|
|
303
|
+
* @param options - Request options
|
|
304
|
+
* @returns AsyncIterable of complete JSON lines (without newline)
|
|
305
|
+
*/
|
|
306
|
+
streamLines(path, options) {
|
|
307
|
+
const rawStream = this.stream(path, options);
|
|
308
|
+
return {
|
|
309
|
+
[Symbol.asyncIterator]() {
|
|
310
|
+
const rawIterator = rawStream[Symbol.asyncIterator]();
|
|
311
|
+
let buffer = '';
|
|
312
|
+
return {
|
|
313
|
+
async next() {
|
|
314
|
+
while (true) {
|
|
315
|
+
// Check for complete lines in buffer
|
|
316
|
+
const newlineIndex = buffer.indexOf('\n');
|
|
317
|
+
if (newlineIndex !== -1) {
|
|
318
|
+
const line = buffer.substring(0, newlineIndex);
|
|
319
|
+
buffer = buffer.substring(newlineIndex + 1);
|
|
320
|
+
if (line.trim()) {
|
|
321
|
+
return { value: line, done: false };
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
// Get more data
|
|
326
|
+
const result = await rawIterator.next();
|
|
327
|
+
if (result.done) {
|
|
328
|
+
// Handle remaining buffer
|
|
329
|
+
if (buffer.trim()) {
|
|
330
|
+
const remaining = buffer.trim();
|
|
331
|
+
buffer = '';
|
|
332
|
+
return { value: remaining, done: false };
|
|
333
|
+
}
|
|
334
|
+
return { value: undefined, done: true };
|
|
335
|
+
}
|
|
336
|
+
buffer += result.value;
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
async return() {
|
|
340
|
+
return rawIterator.return?.() || { value: undefined, done: true };
|
|
341
|
+
},
|
|
342
|
+
async throw(err) {
|
|
343
|
+
return rawIterator.throw?.(err) || Promise.reject(err);
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Abort the current in-progress request.
|
|
351
|
+
*/
|
|
352
|
+
abort() {
|
|
353
|
+
this.aborted = true;
|
|
354
|
+
if (this.currentIterator) {
|
|
355
|
+
this.currentIterator.return?.();
|
|
356
|
+
this.currentIterator = null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { Chaos as default, Chaos, WALLET_MODEL, ASK_MODEL } from './client.js';
|
|
2
|
+
export { httpStreamRequest, StreamingHttpClient } from './http-streaming.js';
|
|
3
|
+
export type { HttpStreamOptions, StreamingHttpClientOptions, StreamRequestOptions } from './http-streaming.js';
|
|
2
4
|
export type { V1WalletRequest, V1AskRequest } from './request.js';
|
|
3
5
|
export type { V1StreamEvent, V1FinalState } from './response.js';
|
|
4
6
|
export type { ChaosConfig, CreateResponseParams, RequestMetadata, InputItem, Response, OutputItem, ContentPart, Block, OutputText, ChaosBlock, MarkdownBlock, TableBlock, TableColumnType, TableSortDirection, TableColumnConfig, ChartBlock, ChartSeries, ChartSegment, ChartAxis, ChartDataPoint, TransactionActionBlock, Primitive, PrimitiveIcon, PrimitiveLineItem, PrimitiveDisplay, RawTransaction, TransactionGroup, Risks, RiskInfoItem, InteractiveBlock, InteractiveOption, ResponseError, } from './types.js';
|
|
@@ -6,3 +8,5 @@ export { ChaosError, ChaosTimeoutError } from './types.js';
|
|
|
6
8
|
export { extractText, extractBlocks, hasRisks, hasBlockers, isTableBlock, isChartBlock, isTransactionActionBlock, isInteractiveBlock, isMarkdownBlock, isOutputText, isChaosBlock, } from './types.js';
|
|
7
9
|
export { BlockSchema, parseRawBlock, detectBlockType } from './schemas.js';
|
|
8
10
|
export type { BlockParsed } from './schemas.js';
|
|
11
|
+
export { isAgentStatusMessage, isAgentMessage, isReportMessage, isFollowUpSuggestions, isUserInputMessage, parseAgentStatus, isTerminalStatus, extractAgentMessageText, extractSuggestions, extractReportBlock, parseStreamLine, parseStreamLines, } from './stream.js';
|
|
12
|
+
export type { MessageType, AgentStatus, StreamMessage, StreamMessageContext, } from './stream.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Chaos AI SDK V1 - Public API
|
|
2
2
|
export { Chaos as default, Chaos, WALLET_MODEL, ASK_MODEL } from './client.js';
|
|
3
|
+
export { httpStreamRequest, StreamingHttpClient } from './http-streaming.js';
|
|
3
4
|
// Export error classes
|
|
4
5
|
export { ChaosError, ChaosTimeoutError } from './types.js';
|
|
5
6
|
// Export helper functions
|
|
6
7
|
export { extractText, extractBlocks, hasRisks, hasBlockers, isTableBlock, isChartBlock, isTransactionActionBlock, isInteractiveBlock, isMarkdownBlock, isOutputText, isChaosBlock, } from './types.js';
|
|
7
8
|
// Export schemas for validation
|
|
8
9
|
export { BlockSchema, parseRawBlock, detectBlockType } from './schemas.js';
|
|
10
|
+
// Export stream message utilities
|
|
11
|
+
export { isAgentStatusMessage, isAgentMessage, isReportMessage, isFollowUpSuggestions, isUserInputMessage, parseAgentStatus, isTerminalStatus, extractAgentMessageText, extractSuggestions, extractReportBlock, parseStreamLine, parseStreamLines, } from './stream.js';
|