Skip to content

Commit f639c4a

Browse files
authored
Merge pull request #496 from M4xymm/RDBC-944
RDBC-944 add streaming to nodejs and ai connection strings
2 parents d61b5bf + fa256ac commit f639c4a

33 files changed

+2928
-811
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,37 @@ console.log("Agent response:", llmResponse);
883883
if (llmResponse.status === "Done") console.log("Conversation finished.")
884884
```
885885

886+
#### Stream a Conversation Response
887+
Stream the agent's response in real-time to provide immediate feedback to users.
888+
889+
```javascript
890+
const chat = store.ai.conversation(agent.identifier, "Performers/", {
891+
parameters: {country: "France"}
892+
});
893+
894+
// Register action handler
895+
chat.handle("store-performer-details", async (req, performer) => {
896+
const session = store.openSession();
897+
await session.store(performer);
898+
await session.saveChanges();
899+
session.dispose();
900+
return {success: true};
901+
});
902+
903+
chat.setUserPrompt("Find the employee with largest profit and suggest rewards");
904+
905+
// Stream the "suggestedReward" property
906+
let chunkedText = "";
907+
const answer = await chat.stream("suggestedReward", async (chunk) => {
908+
// Called for each streamed chunk
909+
chunkedText += chunk;
910+
});
911+
912+
console.log("chunkedText", chunkedText);
913+
914+
console.log("Final answer:", answer);
915+
```
916+
886917
## Attachments
887918

888919
#### Store attachments
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import type { AiAgentToolQueryOptions } from "./AiAgentToolQueryOptions.js";
2+
13
export interface AiAgentToolQuery {
24
name: string;
35
description: string;
46
query: string;
57
parametersSampleObject?: string; // JSON example of parameters
68
parametersSchema?: string; // JSON schema for parameters
9+
options?: AiAgentToolQueryOptions;
710
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Options for controlling when and how query tools are executed in AI agent conversations.
3+
*/
4+
export interface AiAgentToolQueryOptions {
5+
/**
6+
* When true, the model is allowed to execute this query on demand based on its own judgment.
7+
* When false, the model cannot call this query (unless executed as part of initial context).
8+
* When null/undefined, server-side defaults apply.
9+
*/
10+
allowModelQueries?: boolean;
11+
12+
/**
13+
* When true, the query will be executed during the initial context build and its results provided to the model.
14+
* When false, the query will not be executed for the initial context.
15+
* When null/undefined, server-side defaults apply.
16+
*
17+
* Notes:
18+
* - The query must not require model-supplied parameters (it may use agent-scope parameters).
19+
* - If only addToInitialContext is true and allowModelQueries is false, the query runs only
20+
* during the initial context and is not callable by the model afterward.
21+
* - If both addToInitialContext and allowModelQueries are true, the query will run during
22+
* the initial context and may also be invoked later by the model (e.g., to fetch fresh data).
23+
*/
24+
addToInitialContext?: boolean;
25+
}

src/Documents/Operations/AI/Agents/RunConversationOperation.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { IMaintenanceOperation, OperationResultType } from "../../OperationAbstractions.js";
2-
import { Stream } from "node:stream";
2+
import { Readable, Stream } from "node:stream";
3+
import { createInterface } from "node:readline";
34
import type { AiAgentActionResponse } from "./AiAgentActionResponse.js";
45
import type { AiConversationCreationOptions } from "./AiConversationCreationOptions.js";
56
import type { ConversationResult } from "./ConversationResult.js";
7+
import type { AiStreamCallback } from "../AiStreamCallback.js";
68
import { RavenCommand } from "../../../../Http/RavenCommand.js";
79
import { DocumentConventions } from "../../../Conventions/DocumentConventions.js";
810
import { IRaftCommand } from "../../../../Http/IRaftCommand.js";
@@ -21,27 +23,39 @@ export class RunConversationOperation<TAnswer> implements IMaintenanceOperation<
2123
private readonly _actionResponses?: AiAgentActionResponse[];
2224
private readonly _options?: AiConversationCreationOptions;
2325
private readonly _changeVector?: string;
26+
private readonly _streamPropertyPath?: string;
27+
private readonly _streamCallback?: AiStreamCallback;
2428

2529
public constructor(
2630
agentId: string,
2731
conversationId: string,
2832
userPrompt?: string,
2933
actionResponses?: AiAgentActionResponse[],
3034
options?: AiConversationCreationOptions,
31-
changeVector?: string
35+
changeVector?: string,
36+
streamPropertyPath?: string,
37+
streamCallback?: AiStreamCallback
3238
) {
3339
if (StringUtil.isNullOrEmpty(agentId)) {
3440
throwError("InvalidArgumentException", "agentId cannot be null or empty.");
3541
}
3642
if (StringUtil.isNullOrEmpty(conversationId)) {
3743
throwError("InvalidArgumentException", "conversationId cannot be null or empty.");
3844
}
45+
46+
// Both streamPropertyPath and streamCallback must be specified together or neither
47+
if ((streamPropertyPath != null) !== (streamCallback != null)) {
48+
throwError("InvalidOperationException", "Both streamPropertyPath and streamCallback must be specified together or neither.");
49+
}
50+
3951
this._agentId = agentId;
4052
this._conversationId = conversationId;
4153
this._userPrompt = userPrompt;
4254
this._actionResponses = actionResponses;
4355
this._options = options;
4456
this._changeVector = changeVector;
57+
this._streamPropertyPath = streamPropertyPath;
58+
this._streamCallback = streamCallback;
4559
}
4660

4761
public get resultType(): OperationResultType {
@@ -56,7 +70,9 @@ export class RunConversationOperation<TAnswer> implements IMaintenanceOperation<
5670
this._actionResponses,
5771
this._options,
5872
this._changeVector,
59-
conventions
73+
conventions,
74+
this._streamPropertyPath,
75+
this._streamCallback
6076
);
6177
}
6278
}
@@ -70,6 +86,8 @@ class RunConversationCommand<TAnswer>
7086
private readonly _actionResponses?: AiAgentActionResponse[];
7187
private readonly _options?: AiConversationCreationOptions;
7288
private readonly _changeVector?: string;
89+
private readonly _streamPropertyPath?: string;
90+
private readonly _streamCallback?: AiStreamCallback;
7391
private _raftId: string;
7492

7593
public constructor(
@@ -79,7 +97,9 @@ class RunConversationCommand<TAnswer>
7997
actionResponses: AiAgentActionResponse[] | undefined,
8098
options: AiConversationCreationOptions | undefined,
8199
changeVector: string | undefined,
82-
conventions: DocumentConventions
100+
conventions: DocumentConventions,
101+
streamPropertyPath?: string,
102+
streamCallback?: AiStreamCallback
83103
) {
84104
super();
85105
this._conversationId = conversationId;
@@ -88,6 +108,13 @@ class RunConversationCommand<TAnswer>
88108
this._actionResponses = actionResponses;
89109
this._options = options;
90110
this._changeVector = changeVector;
111+
this._streamPropertyPath = streamPropertyPath;
112+
this._streamCallback = streamCallback;
113+
114+
// When streaming is enabled, we need to handle raw response
115+
if (this._streamPropertyPath && this._streamCallback) {
116+
this._responseType = "Raw";
117+
}
91118

92119
if (this._conversationId && this._conversationId.endsWith("|")) {
93120
this._raftId = RaftIdGenerator.newId();
@@ -112,12 +139,17 @@ class RunConversationCommand<TAnswer>
112139
uriParams.append("changeVector", this._changeVector);
113140
}
114141

142+
if (this._streamPropertyPath) {
143+
uriParams.append("streaming", "true");
144+
uriParams.append("streamPropertyPath", this._streamPropertyPath);
145+
}
146+
115147
const uri = `${node.url}/databases/${node.database}/ai/agent?${uriParams}`;
116148

117149
const bodyObj = {
118150
ActionResponses: this._actionResponses,
119151
UserPrompt: this._prompt,
120-
CreationOptions: this._options
152+
CreationOptions: this._options ?? {}
121153
};
122154

123155
const headers = this._headers().typeAppJson().build();
@@ -144,6 +176,43 @@ class RunConversationCommand<TAnswer>
144176
this._throwInvalidResponse();
145177
}
146178

147-
return this._parseResponseDefaultAsync(bodyStream)
179+
if (this._streamPropertyPath && this._streamCallback) {
180+
return await this._processStreamingResponse(bodyStream as Readable);
181+
}
182+
183+
return await this._parseResponseDefaultAsync(bodyStream);
184+
}
185+
186+
private async _processStreamingResponse(bodyStream: Readable): Promise<string> {
187+
const rl = createInterface({
188+
input: bodyStream,
189+
crlfDelay: Infinity
190+
});
191+
192+
for await (const line of rl) {
193+
if (!line || line.trim().length === 0) {
194+
continue;
195+
}
196+
197+
if (line.startsWith("{")) {
198+
const jsonStream = Readable.from([line]);
199+
let body: string = null;
200+
this.result = await this._defaultPipeline(_ => body = _).process(jsonStream);
201+
return body;
202+
}
203+
204+
try {
205+
const unescaped = JSON.parse(line);
206+
await this._streamCallback!(unescaped);
207+
} catch (err) {
208+
await this._streamCallback!(line);
209+
}
210+
}
211+
212+
if (!this.result) {
213+
throwError("InvalidOperationException", "No final result received in streaming response");
214+
}
215+
216+
return null;
148217
}
149218
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./GetAiAgentsOperation.js";
22
export * from "./AddOrUpdateAiAgentOperation.js";
33
export * from "./DeleteAiAgentOperation.js";
4-
export * from "./RunConversationOperation.js";
4+
export * from "./RunConversationOperation.js";
5+
export * from "./AiAgentToolQueryOptions.js";

0 commit comments

Comments
 (0)