Skip to content

Implemented AI agent feature for node.js api #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: v7.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
995 changes: 625 additions & 370 deletions README.md

Large diffs are not rendered by default.

142 changes: 142 additions & 0 deletions examples/ai-agent-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { DocumentStore } from "../src/Documents/DocumentStore.js";
import {
AiAgentConfiguration,
AiAgentToolQuery,
AiAgentToolAction,
AiAgentPersistenceConfiguration
} from "../src/Documents/Operations/AI/Agents/index.js";

// Example schema for AI agent responses
interface CustomerSupportResponse {
answer: string;
relevant: boolean;
relatedOrderIds: string[];
suggestedActions: string[];
}

async function demonstrateAiAgentFeature() {
// Initialize document store
const store = new DocumentStore(["http://localhost:8080"], "TestDB");
store.initialize();

try {
// Create an AI agent configuration
const agentConfig = new AiAgentConfiguration(
"CustomerSupportAgent",
"OpenAI-GPT4",
"You are a helpful customer support assistant for an e-commerce platform. " +
"Answer customer questions using the available tools to query order and product information."
);

// Add query tools (database-side)
agentConfig.queries.push(
new AiAgentToolQuery(
"get_customer_orders",
"Retrieves orders for a specific customer",
"from Orders where CustomerId = $customerId"
),
new AiAgentToolQuery(
"get_product_info",
"Gets detailed product information",
"from Products where Id = $productId"
)
);

// Add action tools (client-side)
agentConfig.actions.push(
new AiAgentToolAction(
"send_email",
"Sends an email to the customer"
),
new AiAgentToolAction(
"create_support_ticket",
"Creates a support ticket for escalation"
)
);

// Configure persistence
agentConfig.persistence = new AiAgentPersistenceConfiguration("chats/", 86400); // 1 day expiration

// Add required parameters
agentConfig.parameters.add("customerId");

// Set sample schema for the AI response format
agentConfig.sampleObject = JSON.stringify({
answer: "Answer to the customer question",
relevant: true,
relatedOrderIds: ["orders/1", "orders/2"],
suggestedActions: ["check_shipping", "contact_support"]
});

// Create the agent
const result = await store.ai.createAgent<CustomerSupportResponse>(agentConfig);
console.log(`Agent created with ID: ${result.identifier}`);

// Start a conversation
const conversation = store.ai.startConversation<CustomerSupportResponse>(
result.identifier,
builder => builder.addParameter("customerId", "customers/123")
);

// Set initial user prompt
conversation.setUserPrompt("I want to check the status of my recent orders and see if any have shipping delays.");

// Run the conversation
let conversationResult = await conversation.run();

while (conversationResult === "ActionRequired") {
// Handle required actions
const actions = conversation.requiredActions();
console.log(`AI requested ${actions.length} actions:`);

for (const action of actions) {
console.log(`- ${action.name}: ${action.arguments}`);

// Simulate handling the action
if (action.name === "send_email") {
conversation.addActionResponse(action.toolId, "Email sent successfully");
} else if (action.name === "create_support_ticket") {
conversation.addActionResponse(action.toolId, JSON.stringify({
ticketId: "TICKET-123",
status: "created"
}));
}
}

// Continue the conversation
conversationResult = await conversation.run();
}

// Get the final answer
const answer = conversation.answer;
console.log("AI Response:", answer);

// Continue conversation with follow-up
conversation.setUserPrompt("Can you also help me track my latest order?");
conversationResult = await conversation.run();

if (conversationResult === "Done") {
console.log("Follow-up response:", conversation.answer);
}

// Example of resuming an existing conversation
const existingConversation = store.ai.resumeConversation<CustomerSupportResponse>(
conversation.id,
conversation.changeVector
);

existingConversation.setUserPrompt("Thank you for your help!");
await existingConversation.run();
console.log("Final response:", existingConversation.answer);

} finally {
store.dispose();
}
}

// Example usage
if (require.main === module) {
demonstrateAiAgentFeature().catch(console.error);
}

export { demonstrateAiAgentFeature };
16 changes: 16 additions & 0 deletions src/Documents/AI/AiAgentParametersBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface IAiAgentParametersBuilder {
addParameter(key: string, value: any): IAiAgentParametersBuilder;
}

export class AiAgentParametersBuilder implements IAiAgentParametersBuilder {
private readonly _parameters = new Map<string, any>();

public addParameter(key: string, value: any): IAiAgentParametersBuilder {
this._parameters.set(key, value);
return this;
}

public getParameters(): Record<string, any> | null {
return this._parameters.size === 0 ? null : Object.fromEntries(this._parameters);
}
}
161 changes: 161 additions & 0 deletions src/Documents/AI/AiConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { IAiConversationOperations } from "./IAiConversationOperations.js";
import { AiConversationResult } from "./AiConversationResult.js";
import { AiAgentActionRequest, AiAgentActionResponse, RunConversationOperation } from "../Operations/AI/Agents/RunConversationOperation.js";
import { StringUtil } from "../../Utility/StringUtil.js";
import { throwError } from "../../Exceptions/index.js";
import { MaintenanceOperationExecutor } from "../Operations/MaintenanceOperationExecutor.js";

interface NewConversationOptions {
type: "new";
agentId: string;
parameters?: Record<string, any>;
}

interface ExistingConversationOptions {
type: "existing";
conversationId: string;
changeVector: string;
}

type ConversationOptions = NewConversationOptions | ExistingConversationOptions;

export class AiConversation<T> implements IAiConversationOperations<T> {
private readonly _maintenanceExecutor: MaintenanceOperationExecutor;
private readonly _agentId?: string;
private readonly _parameters?: Record<string, any>;

private _conversationId?: string;
private _actionRequests?: AiAgentActionRequest[];
private _actionResponses: AiAgentActionResponse[] = [];
private _userPrompt?: string;
private _changeVector?: string;
private _answer?: T;

private constructor(maintenanceExecutor: MaintenanceOperationExecutor, options: ConversationOptions) {
this._maintenanceExecutor = maintenanceExecutor;

if (options.type === "new") {
if (StringUtil.isNullOrEmpty(options.agentId)) {
throwError("InvalidArgumentException", "agentId cannot be null or empty");
}
this._agentId = options.agentId;
this._parameters = options.parameters;
} else {
if (StringUtil.isNullOrEmpty(options.conversationId)) {
throwError("InvalidArgumentException", "conversationId cannot be null or empty");
}
this._conversationId = options.conversationId;
this._changeVector = options.changeVector;
}
}

public static start<T>(maintenanceExecutor: MaintenanceOperationExecutor, agentId: string, parameters?: Record<string, any>): AiConversation<T> {
return new AiConversation<T>(maintenanceExecutor, {
type: "new",
agentId,
parameters
});
}

public static resume<T>(maintenanceExecutor: MaintenanceOperationExecutor, conversationId: string, changeVector: string): AiConversation<T> {
return new AiConversation<T>(maintenanceExecutor, {
type: "existing",
conversationId,
changeVector
});
}

public get id(): string {
if (!this._conversationId) {
throwError("InvalidOperationException", "This is a new conversation, the ID wasn't set yet, you have to call run");
}
return this._conversationId;
}

public get answer(): T {
if (!this._answer) {
throwError("InvalidOperationException", "You have to call run first");
}
return this._answer;
}

public get changeVector(): string {
return this._changeVector ?? "";
}

public requiredActions(): AiAgentActionRequest[] {
if (!this._actionRequests) {
throwError("InvalidOperationException", "You have to call run first");
}
return this._actionRequests;
}

public addActionResponse(actionId: string, actionResponse: string): void;
public addActionResponse<TResponse extends object>(actionId: string, actionResponse: TResponse): void;
public addActionResponse<TResponse extends object>(actionId: string, actionResponse: string | TResponse): void {
let content: string;

if (typeof actionResponse === "string") {
content = actionResponse;
} else {
// For object responses, we need to serialize them
content = JSON.stringify(actionResponse);
}

this._actionResponses.push({
toolId: actionId,
content: content
});
}

public setUserPrompt(userPrompt: string): void {
if (StringUtil.isNullOrEmpty(userPrompt)) {
throwError("InvalidArgumentException", "userPrompt cannot be null or empty");
}
this._userPrompt = userPrompt;
}

public async run(token?: AbortSignal): Promise<AiConversationResult> {
let operation: RunConversationOperation<T>;

if (!this._conversationId) {
operation = new RunConversationOperation<T>(this._agentId!, this._userPrompt!, this._parameters);
} else {
// we allow to run the conversation only if it is the first run with no user prompt or tool requests
// this way we can fetch the pending actions
if (this._actionRequests && !this._userPrompt && this._actionResponses.length === 0) {
return AiConversationResult.Done;
}

operation = new RunConversationOperation<T>(
this._conversationId,
this._userPrompt,
this._actionResponses,
this._changeVector
);
}

try {
const result = await this._maintenanceExecutor.send(operation);

this._conversationId = result.conversationId;
this._changeVector = result.changeVector;
this._answer = result.response;
this._actionRequests = result.actionRequests || [];

// Reset for next turn
this._actionResponses = [];
this._userPrompt = undefined;

return (this._actionRequests && this._actionRequests.length > 0)
? AiConversationResult.ActionRequired
: AiConversationResult.Done;

} catch (e: any) {
if (e.name === "ConcurrencyException") {
throwError("ConcurrencyException", `The conversation was modified by another operation. ChangeVector: ${this._changeVector}`);
}
throw e;
}
}
}
14 changes: 14 additions & 0 deletions src/Documents/AI/AiConversationResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Represents the result of a single conversation turn.
*/
export enum AiConversationResult {
/**
* The conversation has completed and a final answer is available.
*/
Done = "Done",

/**
* Further interaction is required, such as responding to tool requests.
*/
ActionRequired = "ActionRequired"
}
Loading