ravendb
Version:
RavenDB client for Node.js
228 lines • 9.95 kB
JavaScript
import { RunConversationOperation } from "./Agents/RunConversationOperation.js";
import { throwError } from "../../../Exceptions/index.js";
import { StringUtil } from "../../../Utility/StringUtil.js";
export var AiHandleErrorStrategy;
(function (AiHandleErrorStrategy) {
AiHandleErrorStrategy["SendErrorsToModel"] = "SendErrorsToModel";
AiHandleErrorStrategy["RaiseImmediately"] = "RaiseImmediately";
})(AiHandleErrorStrategy || (AiHandleErrorStrategy = {}));
export class AiConversation {
_store;
_databaseName;
_agentId;
_conversationId;
_options;
_actionRequests = null;
_actionResponses = [];
_userPrompt;
_invocations = new Map();
onUnhandledAction;
constructor(store, databaseName, agentId, conversationId, options, changeVector) {
if (!store)
throwError("InvalidArgumentException", "store is required");
if (StringUtil.isNullOrEmpty(databaseName))
throwError("InvalidArgumentException", "databaseName is required");
if (StringUtil.isNullOrEmpty(agentId))
throwError("InvalidArgumentException", "agentId is required");
if (StringUtil.isNullOrEmpty(conversationId))
throwError("InvalidArgumentException", "conversationId is required");
this._store = store;
this._databaseName = databaseName;
this._agentId = agentId;
this._conversationId = conversationId;
this._options = options;
this._changeVector = changeVector;
}
_changeVector;
get changeVector() {
return this._changeVector;
}
get id() {
if (!this._conversationId || this._conversationId.endsWith("/") || this._conversationId.endsWith("|")) {
throwError("InvalidOperationException", "This is a new conversation, the ID wasn't set yet, you have to call run() first");
}
return this._conversationId;
}
requiredActions() {
if (!this._actionRequests) {
throwError("InvalidOperationException", "You must call run() first.");
}
return this._actionRequests;
}
addActionResponse(toolId, actionResponse) {
if (!toolId)
throwError("InvalidArgumentException", "toolId cannot be empty");
if (actionResponse == null)
throwError("InvalidArgumentException", `Action response for '${toolId}' cannot be null.`);
if (typeof actionResponse === "string") {
this._actionResponses.push({ toolId, content: actionResponse });
return;
}
this._actionResponses.push({ toolId, content: JSON.stringify(actionResponse) });
}
setUserPrompt(userPrompt) {
if (!userPrompt)
throwError("InvalidArgumentException", "userPrompt cannot be empty");
this._userPrompt = userPrompt;
}
handle(actionName, action, aiHandleError = AiHandleErrorStrategy.SendErrorsToModel) {
const wrappedAction = action.length === 1
? (_request, args) => action(args)
: action;
this.receive(actionName, async (req, args) => {
const result = await wrappedAction(req, args);
this.addActionResponse(req.toolId, result);
}, aiHandleError);
}
receive(actionName, action, aiHandleError = AiHandleErrorStrategy.SendErrorsToModel) {
if (this._invocations.has(actionName)) {
throwError("InvalidOperationException", `Action '${actionName}' already exists.`);
}
const inv = async (request) => {
try {
const args = this._parseArgs(request.arguments);
await action(request, args);
}
catch (e) {
if (aiHandleError === AiHandleErrorStrategy.SendErrorsToModel) {
this.addActionResponse(request.toolId, this._createErrorMessageForLlm(e));
}
else {
throw e;
}
}
};
this._invocations.set(actionName, inv);
}
async run() {
// eslint-disable-next-line no-constant-condition
while (true) {
const r = await this._runInternal();
if (r.status === "Done") {
return r;
}
if (!this._actionRequests || this._actionRequests.length === 0) {
throwError("InvalidOperationException", `There are no action requests to process, but Status was ${r.status}, should not be possible.`);
}
for (const action of this._actionRequests) {
const invocation = this._invocations.get(action.name);
if (invocation) {
await invocation(action);
}
else if (this.onUnhandledAction) {
await this.onUnhandledAction({
sender: this,
action: action
});
}
else {
throwError("InvalidOperationException", `There is no action defined for action '${action.name}' on agent '${this._agentId}' (${this._conversationId}), ` +
`but it was invoked by the model with: ${action.arguments}. ` +
`Did you forget to call receive() or handle()? You can also handle unexpected action invocations using the onUnhandledAction event.`);
}
}
if (this._actionResponses.length === 0) {
return r; // ActionsRequired, nothing to send back yet
}
}
}
/**
* Executes one "turn" of the conversation with streaming enabled.
* Streams the specified property's value in real-time by invoking the callback with each chunk.
*
* @param streamPropertyPath - The property path of the answer to stream (e.g., "suggestedReward")
* @param streamCallback - Callback invoked with each streamed chunk
* @returns A promise that resolves to the full answer after streaming completes
*
* @example
* ```typescript
* const answer = await chat.stream<TAnswer>("propertyName", async (chunk) => {
* console.log("Received chunk:", chunk);
* });
* ```
*/
async stream(streamPropertyPath, streamCallback) {
if (StringUtil.isNullOrEmpty(streamPropertyPath)) {
throwError("InvalidArgumentException", "streamPropertyPath cannot be empty");
}
if (!streamCallback) {
throwError("InvalidArgumentException", "streamCallback cannot be null");
}
// eslint-disable-next-line no-constant-condition
while (true) {
const r = await this._runInternal(streamPropertyPath, streamCallback);
if (r.status === "Done") {
return r;
}
if (!this._actionRequests || this._actionRequests.length === 0) {
throwError("InvalidOperationException", `There are no action requests to process, but Status was ${r.status}, should not be possible.`);
}
for (const action of this._actionRequests) {
const invocation = this._invocations.get(action.name);
if (invocation) {
await invocation(action);
}
else if (this.onUnhandledAction) {
await this.onUnhandledAction({
sender: this,
action: action
});
}
else {
throwError("InvalidOperationException", `There is no action defined for action '${action.name}' on agent '${this._agentId}' (${this._conversationId}), ` +
`but it was invoked by the model with: ${action.arguments}. ` +
`Did you forget to call receive() or handle()? You can also handle unexpected action invocations using the onUnhandledAction event.`);
}
}
if (this._actionResponses.length === 0) {
return r; // ActionsRequired, nothing to send back yet
}
}
}
async _runInternal(streamPropertyPath, streamCallback) {
if (this._actionRequests != null && !this._userPrompt && this._actionResponses.length === 0) {
return { status: "Done" };
}
const op = new RunConversationOperation(this._agentId, this._conversationId, this._userPrompt, this._actionResponses, this._options, this._changeVector, streamPropertyPath, streamCallback);
try {
const res = await this._store.maintenance.send(op);
this._changeVector = res.changeVector;
this._conversationId = res.conversationId;
this._actionRequests = res.actionRequests;
return {
answer: res.response,
status: (this._actionRequests.length > 0) ? "ActionRequired" : "Done"
};
}
finally {
// clear prompt and responses after running the conversation
this._userPrompt = undefined;
this._actionResponses.length = 0;
}
}
_parseArgs(argsJson) {
// If TArgs is string, return as-is
try {
return JSON.parse(argsJson);
}
catch {
// fall back to raw string when not a JSON
return argsJson;
}
}
_createErrorMessageForLlm(e) {
const lines = [];
let curr = e;
let indent = 0;
while (curr) {
const pad = " ".repeat(indent);
const name = curr?.name || curr?.constructor?.name || "Error";
const msg = curr?.message || String(curr);
lines.push(`${pad}${name}: ${msg}`);
curr = curr?.cause; // Node 20 supports error cause
indent++;
}
return lines.join("\n");
}
}
//# sourceMappingURL=AiConversation.js.map