From d0b6490d9c42dba0fa38a492aa7a948ca42be9ae Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Thu, 7 Nov 2024 11:54:00 +0000 Subject: [PATCH 1/4] feat: conversation api initial implementation Signed-off-by: Mike Nguyen --- .github/workflows/validate-examples.yml | 4 +- dapr/src/client.rs | 98 ++++++++++++ dapr/src/dapr/dapr.proto.runtime.v1.rs | 144 +++++++++++++++++- dapr/src/dapr/types.bin | Bin 120969 -> 124094 bytes examples/Cargo.toml | 4 + examples/src/conversation/README.md | 55 +++++++ .../config/conversation-echo.yaml | 7 + examples/src/conversation/main.rs | 30 ++++ proto/dapr/proto/runtime/v1/appcallback.proto | 16 +- proto/dapr/proto/runtime/v1/dapr.proto | 57 ++++++- 10 files changed, 403 insertions(+), 12 deletions(-) create mode 100644 examples/src/conversation/README.md create mode 100644 examples/src/conversation/config/conversation-echo.yaml create mode 100644 examples/src/conversation/main.rs diff --git a/.github/workflows/validate-examples.yml b/.github/workflows/validate-examples.yml index 9304f54..f3bd27c 100644 --- a/.github/workflows/validate-examples.yml +++ b/.github/workflows/validate-examples.yml @@ -46,7 +46,7 @@ jobs: DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/master/install/install.sh DAPR_CLI_REF: ${{ github.event.inputs_daprcli_commit }} DAPR_CLI_VERSION: ${{ github.event.inputs_daprcli_version }} - DAPR_REF: ${{ github.event.inputs.dapr_commit }} + DAPR_REF: 334ae9eea43d487a7b29a0e4aef904e3eba57a10 DAPR_RUNTIME_VERSION: ${{ github.event.inputs.dapr_version }} CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_REF: ${{ github.ref }} @@ -219,7 +219,7 @@ jobs: fail-fast: false matrix: examples: - [ "actors", "bindings", "client", "configuration", "crypto", "invoke/grpc", "invoke/grpc-proxying", "jobs", "pubsub", "query_state", "secrets-bulk" ] + [ "actors", "bindings", "client", "configuration", "conversation", "crypto", "invoke/grpc", "invoke/grpc-proxying", "jobs", "pubsub", "query_state", "secrets-bulk" ] steps: - name: Check out code uses: actions/checkout@v4 diff --git a/dapr/src/client.rs b/dapr/src/client.rs index 15bd2d0..e274603 100644 --- a/dapr/src/client.rs +++ b/dapr/src/client.rs @@ -533,6 +533,18 @@ impl Client { }; self.0.delete_job_alpha1(request).await } + + /// Converse with an LLM + /// + /// # Arguments + /// + /// * ConversationRequest - The request containing inputs to send to the LLM + pub async fn converse_alpha1( + &mut self, + request: ConversationRequest, + ) -> Result { + self.0.converse_alpha1(request).await + } } #[async_trait] @@ -595,6 +607,11 @@ pub trait DaprInterface: Sized { &mut self, request: DeleteJobRequest, ) -> Result; + + async fn converse_alpha1( + &mut self, + request: ConversationRequest, + ) -> Result; } #[async_trait] @@ -789,6 +806,16 @@ impl DaprInterface for dapr_v1::dapr_client::DaprClient { .await? .into_inner()) } + + async fn converse_alpha1( + &mut self, + request: ConversationRequest, + ) -> Result { + Ok(self + .converse_alpha1(Request::new(request)) + .await? + .into_inner()) + } } /// A request from invoking a service @@ -907,6 +934,18 @@ pub type DeleteJobRequest = crate::dapr::proto::runtime::v1::DeleteJobRequest; /// A response from a delete job request pub type DeleteJobResponse = crate::dapr::proto::runtime::v1::DeleteJobResponse; +/// A request to conversate with an LLM +pub type ConversationRequest = crate::dapr::proto::runtime::v1::ConversationRequest; + +/// A response from conversating with an LLM +pub type ConversationResponse = crate::dapr::proto::runtime::v1::ConversationResponse; + +/// A result from an interacting with a LLM +pub type ConversationResult = crate::dapr::proto::runtime::v1::ConversationResult; + +/// An input to the conversation +pub type ConversationInput = crate::dapr::proto::runtime::v1::ConversationInput; + type StreamPayload = crate::dapr::proto::common::v1::StreamPayload; impl From<(K, Vec)> for common_v1::StateItem where @@ -987,3 +1026,62 @@ impl JobBuilder { } } } + +pub struct ConversationInputBuilder { + message: String, + role: Option, + scrub_pii: Option, +} + +impl ConversationInputBuilder { + pub fn new(message: &str) -> Self { + ConversationInputBuilder { + message: message.to_string(), + role: None, + scrub_pii: None, + } + } + + pub fn build(self) -> ConversationInput { + ConversationInput { + message: self.message, + role: self.role, + scrub_pii: self.scrub_pii, + } + } +} + +pub struct ConversationRequestBuilder { + name: String, + context_id: Option, + inputs: Vec, + parameters: HashMap, + metadata: HashMap, + scrub_pii: Option, + temperature: Option, +} +impl ConversationRequestBuilder { + pub fn new(name: &str, inputs: Vec) -> Self { + ConversationRequestBuilder { + name: name.to_string(), + context_id: None, + inputs, + parameters: Default::default(), + metadata: Default::default(), + scrub_pii: None, + temperature: None, + } + } + + pub fn build(self) -> ConversationRequest { + ConversationRequest { + name: self.name, + context_id: self.context_id, + inputs: self.inputs, + parameters: self.parameters, + metadata: self.metadata, + scrub_pii: self.scrub_pii, + temperature: self.temperature, + } + } +} diff --git a/dapr/src/dapr/dapr.proto.runtime.v1.rs b/dapr/src/dapr/dapr.proto.runtime.v1.rs index abe99fa..1f53f52 100644 --- a/dapr/src/dapr/dapr.proto.runtime.v1.rs +++ b/dapr/src/dapr/dapr.proto.runtime.v1.rs @@ -3154,7 +3154,7 @@ pub struct Job { /// /// Systemd timer style cron accepts 6 fields: /// seconds | minutes | hours | day of month | month | day of week - /// 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + /// 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat /// /// "0 30 * * * *" - every hour on the half hour /// "0 15 3 * * *" - every day at 03:15 @@ -3228,6 +3228,72 @@ pub struct DeleteJobRequest { /// Empty #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct DeleteJobResponse {} +/// ConversationRequest is the request object for Conversation. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConversationRequest { + /// The name of Conversation component + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + /// The ID of an existing chat (like in ChatGPT) + #[prost(string, optional, tag = "2")] + pub context_id: ::core::option::Option<::prost::alloc::string::String>, + /// Inputs for the conversation, support multiple input in one time. + #[prost(message, repeated, tag = "3")] + pub inputs: ::prost::alloc::vec::Vec, + /// Parameters for all custom fields. + #[prost(map = "string, message", tag = "4")] + pub parameters: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost_types::Any, + >, + /// The metadata passing to conversation components. + #[prost(map = "string, string", tag = "5")] + pub metadata: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, + /// Scrub PII data that comes back from the LLM + #[prost(bool, optional, tag = "6")] + pub scrub_pii: ::core::option::Option, + /// Temperature for the LLM to optimize for creativity or predictability + #[prost(double, optional, tag = "7")] + pub temperature: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConversationInput { + /// The message to send to the llm + #[prost(string, tag = "1")] + pub message: ::prost::alloc::string::String, + /// The role to set for the message + #[prost(string, optional, tag = "2")] + pub role: ::core::option::Option<::prost::alloc::string::String>, + /// Scrub PII data that goes into the LLM + #[prost(bool, optional, tag = "3")] + pub scrub_pii: ::core::option::Option, +} +/// ConversationResult is the result for one input. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConversationResult { + /// Result for the one conversation input. + #[prost(string, tag = "1")] + pub result: ::prost::alloc::string::String, + /// Parameters for all custom fields. + #[prost(map = "string, message", tag = "2")] + pub parameters: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost_types::Any, + >, +} +/// ConversationResponse is the response for Conversation. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConversationResponse { + /// The ID of an existing chat (like in ChatGPT) + #[prost(string, optional, tag = "1")] + pub context_id: ::core::option::Option<::prost::alloc::string::String>, + /// An array of results. + #[prost(message, repeated, tag = "2")] + pub outputs: ::prost::alloc::vec::Vec, +} /// PubsubSubscriptionType indicates the type of subscription #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] @@ -4871,6 +4937,31 @@ pub mod dapr_client { ); self.inner.unary(req, path, codec).await } + /// Converse with a LLM service + pub async fn converse_alpha1( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/dapr.proto.runtime.v1.Dapr/ConverseAlpha1", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("dapr.proto.runtime.v1.Dapr", "ConverseAlpha1")); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -5318,6 +5409,14 @@ pub mod dapr_server { tonic::Response, tonic::Status, >; + /// Converse with a LLM service + async fn converse_alpha1( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } /// Dapr service provides APIs to user application to access Dapr building blocks. #[derive(Debug)] @@ -8000,6 +8099,49 @@ pub mod dapr_server { }; Box::pin(fut) } + "/dapr.proto.runtime.v1.Dapr/ConverseAlpha1" => { + #[allow(non_camel_case_types)] + struct ConverseAlpha1Svc(pub Arc); + impl tonic::server::UnaryService + for ConverseAlpha1Svc { + type Response = super::ConversationResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::converse_alpha1(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ConverseAlpha1Svc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { let mut response = http::Response::new(empty_body()); diff --git a/dapr/src/dapr/types.bin b/dapr/src/dapr/types.bin index ca7a444a0641b5125f4abcab05e6fcae26cc42bc..a9ff37410cc4b7e89cd42c1f18a3f2f3ac4a8aed 100644 GIT binary patch delta 27152 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

3*H1N_y7L?3Z%DfS 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 diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 242d10f..ccdbd8a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -47,6 +47,10 @@ path = "src/client/client.rs" name = "configuration" path = "src/configuration/main.rs" +[[example]] +name = "conversation" +path = "src/conversation/main.rs" + [[example]] name = "crypto" path = "src/crypto/main.rs" diff --git a/examples/src/conversation/README.md b/examples/src/conversation/README.md new file mode 100644 index 0000000..a95440e --- /dev/null +++ b/examples/src/conversation/README.md @@ -0,0 +1,55 @@ +# Dapr Conversation Example with the Rust-SDK + +This example uses the echo component to send a request and the component response will be the exact message received. + +## Step + +### Prepare + +- Dapr installed + +### Run Conversation Example + +1. To run the example we need to first build the examples using the following command: + + + +```bash +cargo build --examples +``` + +2. Run the example using the Dapr CLI + + + +```bash +dapr run --app-id conversation \ + --dapr-grpc-port 50001 \ + --log-level debug \ + --resources-path ./config \ + -- go run ./main.go +``` + + + +## Result + +``` + - 'conversation input: hello world' + - 'conversation output: hello world' +``` diff --git a/examples/src/conversation/config/conversation-echo.yaml b/examples/src/conversation/config/conversation-echo.yaml new file mode 100644 index 0000000..9a8b307 --- /dev/null +++ b/examples/src/conversation/config/conversation-echo.yaml @@ -0,0 +1,7 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: echo +spec: + type: conversation.echo + version: v1 \ No newline at end of file diff --git a/examples/src/conversation/main.rs b/examples/src/conversation/main.rs new file mode 100644 index 0000000..6ca4a89 --- /dev/null +++ b/examples/src/conversation/main.rs @@ -0,0 +1,30 @@ +use dapr::client::{ConversationInputBuilder, ConversationRequestBuilder}; +use std::thread; +use std::time::Duration; + +type DaprClient = dapr::Client; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Sleep to allow for the server to become available + thread::sleep(Duration::from_secs(5)); + + // Set the Dapr address + let address = "https://127.0.0.1".to_string(); + + let mut client = DaprClient::connect(address).await?; + + let input = ConversationInputBuilder::new("hello world").build(); + + let conversation_component = "echo"; + + let request = + ConversationRequestBuilder::new(conversation_component, vec![input.clone()]).build(); + + println!("conversation input: {:?}", input.message); + + let response = client.converse_alpha1(request).await?; + + println!("conversation output: {:?}", response.outputs[0].result); + Ok(()) +} diff --git a/proto/dapr/proto/runtime/v1/appcallback.proto b/proto/dapr/proto/runtime/v1/appcallback.proto index 3e98b53..9997414 100644 --- a/proto/dapr/proto/runtime/v1/appcallback.proto +++ b/proto/dapr/proto/runtime/v1/appcallback.proto @@ -58,7 +58,7 @@ service AppCallbackHealthCheck { // AppCallbackAlpha V1 is an optional extension to AppCallback V1 to opt // for Alpha RPCs. service AppCallbackAlpha { - // Subscribes bulk events from Pubsub + // Subscribes bulk events from Pubsub rpc OnBulkTopicEventAlpha1(TopicEventBulkRequest) returns (TopicEventBulkResponse) {} // Sends job back to the app's endpoint at trigger time. @@ -185,14 +185,14 @@ message TopicEventBulkRequestEntry { // content type of the event contained. string content_type = 4; - + // The metadata associated with the event. map metadata = 5; } // TopicEventBulkRequest represents request for bulk message message TopicEventBulkRequest { - // Unique identifier for the bulk request. + // Unique identifier for the bulk request. string id = 1; // The list of items inside this bulk request. @@ -203,10 +203,10 @@ message TopicEventBulkRequest { // The pubsub topic which publisher sent to. string topic = 4; - + // The name of the pubsub the publisher sent to. string pubsub_name = 5; - + // The type of event related to the originating occurrence. string type = 6; @@ -310,8 +310,8 @@ message TopicRoutes { message TopicRule { // The optional CEL expression used to match the event. - // If the match is not specified, then the route is considered - // the default. + // If the match is not specified, then the route is considered + // the default. string match = 1; // The path used to identify matches for this subscription. @@ -340,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} +message HealthCheckResponse {} \ No newline at end of file diff --git a/proto/dapr/proto/runtime/v1/dapr.proto b/proto/dapr/proto/runtime/v1/dapr.proto index 8061643..df483bf 100644 --- a/proto/dapr/proto/runtime/v1/dapr.proto +++ b/proto/dapr/proto/runtime/v1/dapr.proto @@ -202,6 +202,9 @@ service Dapr { // Delete a job rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} + + // Converse with a LLM service + rpc ConverseAlpha1(ConversationRequest) returns (ConversationResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -1206,7 +1209,7 @@ message Job { // // Systemd timer style cron accepts 6 fields: // seconds | minutes | hours | day of month | month | day of week - // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat // // "0 30 * * * *" - every hour on the half hour // "0 15 3 * * *" - every day at 03:15 @@ -1274,4 +1277,56 @@ message DeleteJobRequest { // DeleteJobResponse is the message response to delete the job by name. message DeleteJobResponse { // Empty +} + +// ConversationRequest is the request object for Conversation. +message ConversationRequest { + // The name of Conversation component + string name = 1; + + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 2; + + // Inputs for the conversation, support multiple input in one time. + repeated ConversationInput inputs = 3; + + // Parameters for all custom fields. + map parameters = 4; + + // The metadata passing to conversation components. + map metadata = 5; + + // Scrub PII data that comes back from the LLM + optional bool scrubPII = 6; + + // Temperature for the LLM to optimize for creativity or predictability + optional double temperature = 7; +} + +message ConversationInput { + // The message to send to the llm + string message = 1; + + // The role to set for the message + optional string role = 2; + + // Scrub PII data that goes into the LLM + optional bool scrubPII = 3; +} + +// ConversationResult is the result for one input. +message ConversationResult { + // Result for the one conversation input. + string result = 1; + // Parameters for all custom fields. + map parameters = 2; +} + +// ConversationResponse is the response for Conversation. +message ConversationResponse { + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 1; + + // An array of results. + repeated ConversationResult outputs = 2; } \ No newline at end of file From dbc7a80ef1331b73989090c9ff1dddcd974ed448 Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Thu, 7 Nov 2024 13:56:28 +0000 Subject: [PATCH 2/4] fix(test): close step token Signed-off-by: Mike Nguyen --- examples/src/conversation/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/src/conversation/README.md b/examples/src/conversation/README.md index a95440e..b42b1d5 100644 --- a/examples/src/conversation/README.md +++ b/examples/src/conversation/README.md @@ -23,6 +23,8 @@ timeout: 60 cargo build --examples ``` + + 2. Run the example using the Dapr CLI ```bash -dapr run --app-id conversation \ - --dapr-grpc-port 50001 \ - --log-level debug \ - --resources-path ./config \ - -- go run ./main.go +dapr run --app-id=conversation --resources-path ./config --dapr-grpc-port 3500 -- cargo run --example conversation ``` From 80a3a90f820bd78a0a7a4e8ae7c8c369825ea42c Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Thu, 7 Nov 2024 14:34:40 +0000 Subject: [PATCH 4/4] fix(test): example validation Signed-off-by: Mike Nguyen --- examples/src/conversation/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/src/conversation/README.md b/examples/src/conversation/README.md index 540f086..51578d3 100644 --- a/examples/src/conversation/README.md +++ b/examples/src/conversation/README.md @@ -31,8 +31,8 @@ cargo build --examples name: Run Conversation output_match_mode: substring expected_stdout_lines: - - 'conversation input: hello world' - - 'conversation output: hello world' + - 'conversation input: "hello world"' + - 'conversation output: "hello world"' background: true sleep: 15