Skip to content
Draft
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
"test:e2e:http:client": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/http/(client).test.ts' ]",
"test:e2e:http:server": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/http/(server).test.ts' ]",
"test:e2e:http:actors": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/http/actors.test.ts' ]",
"test:e2e:common": "npm run test:e2e:common:client && npm run test:e2e:common:server",
"test:e2e:common": "npm run test:e2e:common:client && npm run test:e2e:common:server && npm run test:e2e:common:conversation",
"test:e2e:common:client": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/common/client.test.ts' ]",
"test:e2e:common:server": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/common/server.test.ts' ]",
"test:e2e:common:conversation": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/common/conversation.test.ts' ]",
"test:e2e:workflow": "jest --runInBand --forceExit --testMatch [ '**/test/e2e/workflow/workflow.test.ts' ]",
"test:e2e:workflow:internal": "jest test/e2e/workflow --runInBand --forceExit",
"test:e2e:workflow:durabletask": "node scripts/test-e2e-workflow.js",
Expand Down
6 changes: 6 additions & 0 deletions src/implementation/Client/DaprClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import IClientSecret from "../../interfaces/Client/IClientSecret";
import IClientSidecar from "../../interfaces/Client/IClientSidecar";
import IClientState from "../../interfaces/Client/IClientState";
import IClientWorkflow from "../../interfaces/Client/IClientWorkflow";
import IClientConversation from "../../interfaces/Client/IClientConversation";

import GRPCClient from "./GRPCClient/GRPCClient";
import GRPCClientActor from "./GRPCClient/actor";
Expand All @@ -41,6 +42,7 @@ import GRPCClientSecret from "./GRPCClient/secret";
import GRPCClientSidecar from "./GRPCClient/sidecar";
import GRPCClientState from "./GRPCClient/state";
import GRPCClientWorkflow from "./GRPCClient/workflow";
import GRPCClientConversation from "./GRPCClient/conversation";

import HTTPClient from "./HTTPClient/HTTPClient";
import HTTPClientActor from "./HTTPClient/actor";
Expand All @@ -57,6 +59,7 @@ import HTTPClientSecret from "./HTTPClient/secret";
import HTTPClientSidecar from "./HTTPClient/sidecar";
import HTTPClientState from "./HTTPClient/state";
import HTTPClientWorkflow from "./HTTPClient/workflow";
import HTTPClientConversation from "./HTTPClient/conversation";

import CommunicationProtocolEnum from "../../enum/CommunicationProtocol.enum";
import { DaprClientOptions } from "../../types/DaprClientOptions";
Expand All @@ -83,6 +86,7 @@ export default class DaprClient {
readonly sidecar: IClientSidecar;
readonly state: IClientState;
readonly workflow: IClientWorkflow;
readonly conversation: IClientConversation;

private readonly logger: Logger;

Expand Down Expand Up @@ -119,6 +123,7 @@ export default class DaprClient {
this.crypto = new GRPCClientCrypto(client);
this.actor = new GRPCClientActor(client); // we use an abstractor here since we interface through a builder with the Actor Runtime
this.workflow = new GRPCClientWorkflow(client);
this.conversation = new GRPCClientConversation(client);
break;
}
case CommunicationProtocolEnum.HTTP:
Expand All @@ -140,6 +145,7 @@ export default class DaprClient {
this.sidecar = new HTTPClientSidecar(client);
this.state = new HTTPClientState(client);
this.workflow = new HTTPClientWorkflow(client);
this.conversation = new HTTPClientConversation(client);
break;
}
}
Expand Down
210 changes: 210 additions & 0 deletions src/implementation/Client/GRPCClient/conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { create } from "@bufbuild/protobuf";
import type { JsonObject } from "@bufbuild/protobuf";
import GRPCClient from "./GRPCClient";
import IClientConversation from "../../../interfaces/Client/IClientConversation";
import {
ConversationRequestAlpha2Schema,
ConversationInputAlpha2Schema,
ConversationMessageSchema,
ConversationMessageContentSchema,
ConversationMessageOfDeveloperSchema,
ConversationMessageOfSystemSchema,
ConversationMessageOfUserSchema,
ConversationMessageOfAssistantSchema,
ConversationMessageOfToolSchema,
ConversationToolsSchema,
ConversationToolsFunctionSchema,
ConversationToolCallsSchema,
ConversationToolCallsOfFunctionSchema,
} from "../../../proto/dapr/proto/runtime/v1/dapr_pb";
import type {
ConversationInputAlpha2,
ConversationMessage as ProtoConversationMessage,
ConversationTools as ProtoConversationTools,
ConversationResponseAlpha2,
} from "../../../proto/dapr/proto/runtime/v1/dapr_pb";
import {
ConversationOptions,
ConversationResponse,
ConversationInput,
ConversationMessage,
ConversationTool,
ConversationResultChoice,
ConversationResult,
ConversationResultToolCall,
} from "../../../types/conversation/Conversation.type";

// https://docs.dapr.io/reference/api/conversation_api/
export default class GRPCClientConversation implements IClientConversation {
client: GRPCClient;

constructor(client: GRPCClient) {
this.client = client;
}

async converse(
conversationComponentName: string,
inputs: ConversationInput[],
options?: ConversationOptions,
): Promise<ConversationResponse> {
const client = await this.client.getClient();

const protoInputs = inputs.map((input) => this.buildInput(input));
const protoTools = (options?.tools ?? []).map((tool) => this.buildTool(tool));

const protoRequest = create(ConversationRequestAlpha2Schema, {
name: conversationComponentName,
contextId: options?.contextId,
inputs: protoInputs,
metadata: options?.metadata ?? {},
scrubPii: options?.scrubPii,
temperature: options?.temperature,
tools: protoTools,
toolChoice: options?.toolChoice,
});

const res: ConversationResponseAlpha2 = await client.converseAlpha2(protoRequest);

return this.mapResponse(res);
}

private buildInput(input: ConversationInput): ConversationInputAlpha2 {
const messages = input.messages.map((msg) => this.buildMessage(msg));
return create(ConversationInputAlpha2Schema, {
messages,
scrubPii: input.scrubPii,
});
}

private buildMessage(msg: ConversationMessage): ProtoConversationMessage {
const contentItems = msg.content.map((c) => create(ConversationMessageContentSchema, { text: c.text }));

switch (msg.role) {
case "developer":
return create(ConversationMessageSchema, {
messageTypes: {
case: "ofDeveloper",
value: create(ConversationMessageOfDeveloperSchema, {
name: msg.name,
content: contentItems,
}),
},
});
case "system":
return create(ConversationMessageSchema, {
messageTypes: {
case: "ofSystem",
value: create(ConversationMessageOfSystemSchema, {
name: msg.name,
content: contentItems,
}),
},
});
case "user":
return create(ConversationMessageSchema, {
messageTypes: {
case: "ofUser",
value: create(ConversationMessageOfUserSchema, {
name: msg.name,
content: contentItems,
}),
},
});
case "assistant": {
const toolCalls = (msg.toolCalls ?? []).map((tc) =>
create(ConversationToolCallsSchema, {
id: tc.id,
toolTypes: tc.function
? {
case: "function",
value: create(ConversationToolCallsOfFunctionSchema, {
name: tc.function.name,
arguments: tc.function.arguments,
}),
}
: { case: undefined, value: undefined },
}),
);
return create(ConversationMessageSchema, {
messageTypes: {
case: "ofAssistant",
value: create(ConversationMessageOfAssistantSchema, {
name: msg.name,
content: contentItems,
toolCalls,
}),
},
});
}
case "tool":
return create(ConversationMessageSchema, {
messageTypes: {
case: "ofTool",
value: create(ConversationMessageOfToolSchema, {
toolId: msg.toolId,
name: msg.name,
content: contentItems,
}),
},
});
}
}

private buildTool(tool: ConversationTool): ProtoConversationTools {
return create(ConversationToolsSchema, {
toolTypes: {
case: "function",
value: create(ConversationToolsFunctionSchema, {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters as JsonObject | undefined,
}),
},
});
}

private mapResponse(res: ConversationResponseAlpha2): ConversationResponse {
const outputs: ConversationResult[] = (res.outputs ?? []).map((output) => {
const choices: ConversationResultChoice[] = (output.choices ?? []).map((choice) => {
const toolCalls: ConversationResultToolCall[] | undefined = choice.message?.toolCalls?.map((tc) => ({
id: tc.id,
function:
tc.toolTypes.case === "function"
? { name: tc.toolTypes.value.name, arguments: tc.toolTypes.value.arguments }
: undefined,
}));

return {
finishReason: choice.finishReason,
index: Number(choice.index),
message: choice.message
? {
content: choice.message.content,
toolCalls: toolCalls?.length ? toolCalls : undefined,
}
: undefined,
};
});

return { choices };
});

return {
contextId: res.contextId,
outputs,
};
}
}
Loading
Loading