From fdff6cf41a10d1d1bcccd79809e58314f41446ac Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:32:08 -0500 Subject: [PATCH 01/29] initial Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 177 ++- dapr/clients/grpc/_helpers.py | 87 +- dapr/clients/grpc/_request.py | 102 +- dapr/clients/grpc/_response.py | 33 + dapr/clients/grpc/client.py | 196 ++- dapr/proto/common/v1/common_pb2.py | 20 +- dapr/proto/common/v1/common_pb2_grpc.py | 20 + dapr/proto/runtime/v1/appcallback_pb2.py | 24 +- dapr/proto/runtime/v1/appcallback_pb2_grpc.py | 167 ++- dapr/proto/runtime/v1/dapr_pb2.py | 176 ++- dapr/proto/runtime/v1/dapr_pb2.pyi | 613 +++++++++ dapr/proto/runtime/v1/dapr_pb2_grpc.py | 1127 +++++++++++++---- dev-requirements.txt | 2 + pyproject.toml | 9 +- tests/clients/fake_dapr_server.py | 77 ++ tests/clients/test_conversation.py | 783 ++++++++++++ tests/clients/test_dapr_grpc_client.py | 304 +++++ 17 files changed, 3570 insertions(+), 347 deletions(-) create mode 100644 tests/clients/test_conversation.py diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 8daa3cb0..473c27d2 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -63,6 +63,7 @@ to_bytes, validateNotNone, validateNotBlankString, + convert_parameters, ) from dapr.aio.clients.grpc._request import ( EncryptRequestIterator, @@ -77,12 +78,28 @@ BindingRequest, TransactionalStateOperation, ConversationInput, + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageContent, + ConversationMessageOfUser, + ConversationMessageOfSystem, + ConversationMessageOfAssistant, + ConversationMessageOfDeveloper, + ConversationMessageOfTool, + ConversationTools, + ConversationToolsFunction, + ConversationToolCalls, + ConversationToolCallsOfFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( BindingResponse, ConversationResponse, ConversationResult, + ConversationResponseAlpha2, + ConversationResultAlpha2, + ConversationResultChoices, + ConversationResultMessage, DaprResponse, GetSecretResponse, GetBulkSecretResponse, @@ -1722,7 +1739,7 @@ async def converse_alpha1( inputs: List[ConversationInput], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, GrpcAny]] = None, + parameters: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, @@ -1733,7 +1750,7 @@ async def converse_alpha1( name: Name of the LLM component to invoke inputs: List of conversation inputs context_id: Optional ID for continuing an existing chat - parameters: Optional custom parameters for the request + parameters: Optional custom parameters for the request (raw Python values or GrpcAny objects) metadata: Optional metadata for the component scrub_pii: Optional flag to scrub PII from inputs and outputs temperature: Optional temperature setting for the LLM to optimize for creativity or predictability @@ -1749,11 +1766,14 @@ async def converse_alpha1( for inp in inputs ] + # Convert raw Python parameters to GrpcAny objects + converted_parameters = convert_parameters(parameters) + request = api_v1.ConversationRequest( name=name, inputs=inputs_pb, contextID=context_id, - parameters=parameters or {}, + parameters=converted_parameters, metadata=metadata or {}, scrubPII=scrub_pii, temperature=temperature, @@ -1772,6 +1792,157 @@ async def converse_alpha1( except grpc.aio.AioRpcError as err: raise DaprGrpcError(err) from err + async def converse_alpha2( + self, + name: str, + inputs: List[ConversationInputAlpha2], + *, + context_id: Optional[str] = None, + parameters: Optional[Dict[str, Any]] = None, + metadata: Optional[Dict[str, str]] = None, + scrub_pii: Optional[bool] = None, + temperature: Optional[float] = None, + tools: Optional[List[ConversationTools]] = None, + tool_choice: Optional[str] = None, + ) -> ConversationResponseAlpha2: + """Invoke an LLM using the conversation API (Alpha2) with tool calling support. + + Args: + name: Name of the LLM component to invoke + inputs: List of Alpha2 conversation inputs with sophisticated message types + context_id: Optional ID for continuing an existing chat + parameters: Optional custom parameters for the request (raw Python values or GrpcAny objects) + metadata: Optional metadata for the component + scrub_pii: Optional flag to scrub PII from inputs and outputs + temperature: Optional temperature setting for the LLM to optimize for creativity or predictability + tools: Optional list of tools available for the LLM to call + tool_choice: Optional control over which tools can be called ('none', 'auto', 'required', or specific tool name) + + Returns: + ConversationResponseAlpha2 containing the conversation results with choices and tool calls + + Raises: + DaprGrpcError: If the Dapr runtime returns an error + """ + + def _convert_message_content(content_list: List[ConversationMessageContent]): + """Convert message content list to proto format.""" + if not content_list: + return [] + return [api_v1.ConversationMessageContent(text=content.text) for content in content_list] + + def _convert_tool_calls(tool_calls: List[ConversationToolCalls]): + """Convert tool calls to proto format.""" + if not tool_calls: + return [] + proto_calls = [] + for call in tool_calls: + proto_call = api_v1.ConversationToolCalls() + if call.id: + proto_call.id = call.id + if call.function: + proto_call.function.name = call.function.name + proto_call.function.arguments = call.function.arguments + proto_calls.append(proto_call) + return proto_calls + + def _convert_message(message: ConversationMessage): + """Convert a conversation message to proto format.""" + proto_message = api_v1.ConversationMessage() + + if message.of_developer: + proto_message.of_developer.name = message.of_developer.name or '' + proto_message.of_developer.content.extend(_convert_message_content(message.of_developer.content or [])) + elif message.of_system: + proto_message.of_system.name = message.of_system.name or '' + proto_message.of_system.content.extend(_convert_message_content(message.of_system.content or [])) + elif message.of_user: + proto_message.of_user.name = message.of_user.name or '' + proto_message.of_user.content.extend(_convert_message_content(message.of_user.content or [])) + elif message.of_assistant: + proto_message.of_assistant.name = message.of_assistant.name or '' + proto_message.of_assistant.content.extend(_convert_message_content(message.of_assistant.content or [])) + proto_message.of_assistant.tool_calls.extend(_convert_tool_calls(message.of_assistant.tool_calls or [])) + elif message.of_tool: + if message.of_tool.tool_id: + proto_message.of_tool.tool_id = message.of_tool.tool_id + proto_message.of_tool.name = message.of_tool.name + proto_message.of_tool.content.extend(_convert_message_content(message.of_tool.content or [])) + + return proto_message + + # Convert inputs to proto format + inputs_pb = [] + for inp in inputs: + proto_input = api_v1.ConversationInputAlpha2() + if inp.scrub_pii is not None: + proto_input.scrub_pii = inp.scrub_pii + + for message in inp.messages: + proto_input.messages.append(_convert_message(message)) + + inputs_pb.append(proto_input) + + # Convert tools to proto format + tools_pb = [] + if tools: + for tool in tools: + proto_tool = api_v1.ConversationTools() + if tool.function: + proto_tool.function.name = tool.function.name + if tool.function.description: + proto_tool.function.description = tool.function.description + if tool.function.parameters: + for key, value in tool.function.parameters.items(): + proto_tool.function.parameters[key].CopyFrom(value) + tools_pb.append(proto_tool) + + # Convert raw Python parameters to GrpcAny objects + converted_parameters = convert_parameters(parameters) + + # Build the request + request = api_v1.ConversationRequestAlpha2( + name=name, + inputs=inputs_pb, + parameters=converted_parameters, + metadata=metadata or {}, + tools=tools_pb, + ) + + if context_id is not None: + request.context_id = context_id + if scrub_pii is not None: + request.scrub_pii = scrub_pii + if temperature is not None: + request.temperature = temperature + if tool_choice is not None: + request.tool_choice = tool_choice + + try: + response = await self._stub.ConverseAlpha2(request) + + outputs = [] + for output in response.outputs: + choices = [] + for choice in output.choices: + choices.append(ConversationResultChoices( + finish_reason=choice.finish_reason, + index=choice.index, + message=ConversationResultMessage( + content=choice.message.content, + tool_calls=choice.message.tool_calls + ) + )) + outputs.append(ConversationResultAlpha2(choices=choices)) + + return ConversationResponseAlpha2( + context_id=response.context_id, + outputs=outputs + ) + + except grpc.aio.AioRpcError as err: + raise DaprGrpcError(err) from err + async def wait(self, timeout_s: float): """Waits for sidecar to be available within the timeout. diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index 7da35dc2..d5879650 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -12,10 +12,18 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import Dict, List, Union, Tuple, Optional +from typing import Dict, List, Union, Tuple, Optional, Any from enum import Enum from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage +from google.protobuf.wrappers_pb2 import ( + BoolValue, + StringValue, + Int32Value, + Int64Value, + DoubleValue, + BytesValue, +) MetadataDict = Dict[str, List[Union[bytes, str]]] MetadataTuple = Tuple[Tuple[str, Union[bytes, str]], ...] @@ -105,3 +113,80 @@ def getWorkflowRuntimeStatus(inputString): return WorkflowRuntimeStatus[inputString].value except KeyError: return WorkflowRuntimeStatus.UNKNOWN + + +def convert_parameter_value(value: Any) -> GrpcAny: + """Convert a raw Python value to a GrpcAny protobuf message. + + This function automatically detects the type of the input value and wraps it + in the appropriate protobuf wrapper type before packing it into GrpcAny. + + Args: + value: Raw Python value (str, int, float, bool, bytes, or already GrpcAny) + + Returns: + GrpcAny: The value wrapped in a GrpcAny protobuf message + + Raises: + ValueError: If the value type is not supported + + Examples: + >>> convert_parameter_value("hello") # -> GrpcAny containing StringValue + >>> convert_parameter_value(42) # -> GrpcAny containing Int64Value + >>> convert_parameter_value(3.14) # -> GrpcAny containing DoubleValue + >>> convert_parameter_value(True) # -> GrpcAny containing BoolValue + """ + # If it's already a GrpcAny, return as-is (backward compatibility) + if isinstance(value, GrpcAny): + return value + + # Create the GrpcAny wrapper + any_pb = GrpcAny() + + # Convert based on Python type + if isinstance(value, bool): + # Note: bool check must come before int since bool is a subclass of int in Python + any_pb.Pack(BoolValue(value=value)) + elif isinstance(value, str): + any_pb.Pack(StringValue(value=value)) + elif isinstance(value, int): + # Use Int64Value to handle larger integers, but Int32Value for smaller ones + if -2147483648 <= value <= 2147483647: + any_pb.Pack(Int32Value(value=value)) + else: + any_pb.Pack(Int64Value(value=value)) + elif isinstance(value, float): + any_pb.Pack(DoubleValue(value=value)) + elif isinstance(value, bytes): + any_pb.Pack(BytesValue(value=value)) + else: + raise ValueError(f"Unsupported parameter type: {type(value)}. " + f"Supported types: str, int, float, bool, bytes, GrpcAny") + + return any_pb + + +def convert_parameters(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAny]: + """Convert a dictionary of raw Python values to GrpcAny parameters. + + This function takes a dictionary with raw Python values and converts each + value to the appropriate GrpcAny protobuf message for use in Dapr API calls. + + Args: + parameters: Optional dictionary of parameter names to raw Python values + + Returns: + Dictionary of parameter names to GrpcAny values + + Examples: + >>> convert_parameters({"temperature": 0.7, "max_tokens": 1000, "stream": False}) + >>> # Returns: {"temperature": GrpcAny, "max_tokens": GrpcAny, "stream": GrpcAny} + """ + if not parameters: + return {} + + converted = {} + for key, value in parameters.items(): + converted[key] = convert_parameter_value(value) + + return converted diff --git a/dapr/clients/grpc/_request.py b/dapr/clients/grpc/_request.py index c914a9d5..f6fe3854 100644 --- a/dapr/clients/grpc/_request.py +++ b/dapr/clients/grpc/_request.py @@ -16,7 +16,7 @@ import io from enum import Enum from dataclasses import dataclass -from typing import Dict, Optional, Union +from typing import Dict, Optional, Union, List from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage @@ -437,6 +437,106 @@ class ConversationInput: scrub_pii: Optional[bool] = None +@dataclass +class ConversationMessageContent: + """Content for conversation messages.""" + + text: str + + +@dataclass +class ConversationMessageOfDeveloper: + """Developer message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = None + + +@dataclass +class ConversationMessageOfSystem: + """System message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = None + + +@dataclass +class ConversationMessageOfUser: + """User message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = None + + +@dataclass +class ConversationToolCallsOfFunction: + """Function call details within a tool call.""" + + name: str + arguments: str + + +@dataclass +class ConversationToolCalls: + """Tool calls generated by the model.""" + + id: Optional[str] = None + function: Optional[ConversationToolCallsOfFunction] = None + + +@dataclass +class ConversationMessageOfAssistant: + """Assistant message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = None + tool_calls: List[ConversationToolCalls] = None + + +@dataclass +class ConversationMessageOfTool: + """Tool message content.""" + + tool_id: Optional[str] = None + name: str = '' + content: List[ConversationMessageContent] = None + + +@dataclass +class ConversationMessage: + """Conversation message with different role types.""" + + of_developer: Optional[ConversationMessageOfDeveloper] = None + of_system: Optional[ConversationMessageOfSystem] = None + of_user: Optional[ConversationMessageOfUser] = None + of_assistant: Optional[ConversationMessageOfAssistant] = None + of_tool: Optional[ConversationMessageOfTool] = None + + +@dataclass +class ConversationInputAlpha2: + """Alpha2 input message for conversation API.""" + + messages: List[ConversationMessage] + scrub_pii: Optional[bool] = None + + +@dataclass +class ConversationToolsFunction: + """Function definition for conversation tools.""" + + name: str + description: Optional[str] = None + parameters: Optional[Dict[str, GrpcAny]] = None + + +@dataclass +class ConversationTools: + """Tools available for conversation.""" + + function: Optional[ConversationToolsFunction] = None + + class JobEvent: """Represents a job event received from Dapr runtime. diff --git a/dapr/clients/grpc/_response.py b/dapr/clients/grpc/_response.py index 6d6ee92a..cbaf16b3 100644 --- a/dapr/clients/grpc/_response.py +++ b/dapr/clients/grpc/_response.py @@ -56,6 +56,7 @@ # for type checking if TYPE_CHECKING: from dapr.clients.grpc.client import DaprGrpcClient + from dapr.clients.grpc._request import ConversationToolCalls TCryptoResponse = TypeVar( 'TCryptoResponse', bound=Union[api_v1.EncryptResponse, api_v1.DecryptResponse] @@ -1081,9 +1082,41 @@ class ConversationResult: parameters: Dict[str, GrpcAny] = field(default_factory=dict) +@dataclass +class ConversationResultMessage: + """Message content in conversation result.""" + + content: str + tool_calls: List['ConversationToolCalls'] = field(default_factory=list) + + +@dataclass +class ConversationResultChoices: + """Choice in Alpha2 conversation result.""" + + finish_reason: str + index: int + message: ConversationResultMessage + + +@dataclass +class ConversationResultAlpha2: + """Alpha2 result from conversation input.""" + + choices: List[ConversationResultChoices] = field(default_factory=list) + + @dataclass class ConversationResponse: """Response from the conversation API.""" context_id: Optional[str] outputs: List[ConversationResult] + + +@dataclass +class ConversationResponseAlpha2: + """Alpha2 response from the conversation API.""" + + context_id: Optional[str] + outputs: List[ConversationResultAlpha2] diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index ac0def79..a389c910 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -53,10 +53,14 @@ from dapr.version import __version__ from dapr.clients.grpc._helpers import ( + MetadataDict, MetadataTuple, to_bytes, - validateNotNone, - validateNotBlankString, + to_str, + tuple_to_dict, + unpack, + WorkflowRuntimeStatus, + convert_parameters, ) from dapr.conf.helpers import GrpcEndpoint from dapr.clients.grpc._request import ( @@ -66,6 +70,18 @@ EncryptRequestIterator, DecryptRequestIterator, ConversationInput, + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageContent, + ConversationMessageOfDeveloper, + ConversationMessageOfSystem, + ConversationMessageOfUser, + ConversationMessageOfAssistant, + ConversationMessageOfTool, + ConversationTools, + ConversationToolsFunction, + ConversationToolCalls, + ConversationToolCallsOfFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( @@ -93,6 +109,10 @@ TopicEventResponse, ConversationResponse, ConversationResult, + ConversationResponseAlpha2, + ConversationResultAlpha2, + ConversationResultChoices, + ConversationResultMessage, ) @@ -1725,7 +1745,7 @@ def converse_alpha1( inputs: List[ConversationInput], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, GrpcAny]] = None, + parameters: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, @@ -1736,7 +1756,7 @@ def converse_alpha1( name: Name of the LLM component to invoke inputs: List of conversation inputs context_id: Optional ID for continuing an existing chat - parameters: Optional custom parameters for the request + parameters: Optional custom parameters for the request (raw Python values or GrpcAny objects) metadata: Optional metadata for the component scrub_pii: Optional flag to scrub PII from inputs and outputs temperature: Optional temperature setting for the LLM to optimize for creativity or predictability @@ -1753,11 +1773,14 @@ def converse_alpha1( for inp in inputs ] + # Convert raw Python parameters to GrpcAny objects + converted_parameters = convert_parameters(parameters) + request = api_v1.ConversationRequest( name=name, inputs=inputs_pb, contextID=context_id, - parameters=parameters or {}, + parameters=converted_parameters, metadata=metadata or {}, scrubPII=scrub_pii, temperature=temperature, @@ -1775,6 +1798,169 @@ def converse_alpha1( except RpcError as err: raise DaprGrpcError(err) from err + def converse_alpha2( + self, + name: str, + inputs: List[ConversationInputAlpha2], + *, + context_id: Optional[str] = None, + parameters: Optional[Dict[str, Any]] = None, + metadata: Optional[Dict[str, str]] = None, + scrub_pii: Optional[bool] = None, + temperature: Optional[float] = None, + tools: Optional[List[ConversationTools]] = None, + tool_choice: Optional[str] = None, + ) -> ConversationResponseAlpha2: + """Invoke an LLM using the conversation API (Alpha2) with tool calling support. + + Args: + name: Name of the LLM component to invoke + inputs: List of Alpha2 conversation inputs with sophisticated message types + context_id: Optional ID for continuing an existing chat + parameters: Optional custom parameters for the request (raw Python values or GrpcAny objects) + metadata: Optional metadata for the component + scrub_pii: Optional flag to scrub PII from inputs and outputs + temperature: Optional temperature setting for the LLM to optimize for creativity or predictability + tools: Optional list of tools available for the LLM to call + tool_choice: Optional control over which tools can be called ('none', 'auto', 'required', or specific tool name) + + Returns: + ConversationResponseAlpha2 containing the conversation results with choices and tool calls + + Raises: + DaprGrpcError: If the Dapr runtime returns an error + """ + + def _convert_message_content(content_list: List[ConversationMessageContent]): + """Convert message content list to proto format.""" + if not content_list: + return [] + return [api_v1.ConversationMessageContent(text=content.text) for content in content_list] + + def _convert_tool_calls(tool_calls: List[ConversationToolCalls]): + """Convert tool calls to proto format.""" + if not tool_calls: + return [] + proto_calls = [] + for call in tool_calls: + proto_call = api_v1.ConversationToolCalls() + if call.id: + proto_call.id = call.id + if call.function: + proto_call.function.name = call.function.name + proto_call.function.arguments = call.function.arguments + proto_calls.append(proto_call) + return proto_calls + + def _convert_message(message: ConversationMessage): + """Convert a conversation message to proto format.""" + proto_message = api_v1.ConversationMessage() + + if message.of_developer: + proto_message.of_developer.name = message.of_developer.name or '' + proto_message.of_developer.content.extend(_convert_message_content(message.of_developer.content or [])) + elif message.of_system: + proto_message.of_system.name = message.of_system.name or '' + proto_message.of_system.content.extend(_convert_message_content(message.of_system.content or [])) + elif message.of_user: + proto_message.of_user.name = message.of_user.name or '' + proto_message.of_user.content.extend(_convert_message_content(message.of_user.content or [])) + elif message.of_assistant: + proto_message.of_assistant.name = message.of_assistant.name or '' + proto_message.of_assistant.content.extend(_convert_message_content(message.of_assistant.content or [])) + proto_message.of_assistant.tool_calls.extend(_convert_tool_calls(message.of_assistant.tool_calls or [])) + elif message.of_tool: + if message.of_tool.tool_id: + proto_message.of_tool.tool_id = message.of_tool.tool_id + proto_message.of_tool.name = message.of_tool.name + proto_message.of_tool.content.extend(_convert_message_content(message.of_tool.content or [])) + + return proto_message + + # Convert inputs to proto format + inputs_pb = [] + for inp in inputs: + proto_input = api_v1.ConversationInputAlpha2() + if inp.scrub_pii is not None: + proto_input.scrub_pii = inp.scrub_pii + + for message in inp.messages: + proto_input.messages.append(_convert_message(message)) + + inputs_pb.append(proto_input) + + # Convert tools to proto format + tools_pb = [] + if tools: + for tool in tools: + proto_tool = api_v1.ConversationTools() + if tool.function: + proto_tool.function.name = tool.function.name + if tool.function.description: + proto_tool.function.description = tool.function.description + if tool.function.parameters: + for key, value in tool.function.parameters.items(): + proto_tool.function.parameters[key].CopyFrom(value) + tools_pb.append(proto_tool) + + # Convert raw Python parameters to GrpcAny objects + converted_parameters = convert_parameters(parameters) + + # Build the request + request = api_v1.ConversationRequestAlpha2( + name=name, + inputs=inputs_pb, + parameters=converted_parameters, + metadata=metadata or {}, + tools=tools_pb, + ) + + if context_id is not None: + request.context_id = context_id + if scrub_pii is not None: + request.scrub_pii = scrub_pii + if temperature is not None: + request.temperature = temperature + if tool_choice is not None: + request.tool_choice = tool_choice + + try: + response, call = self.retry_policy.run_rpc(self._stub.ConverseAlpha2.with_call, request) + + # Convert response to our format + outputs = [] + for output in response.outputs: + choices = [] + for choice in output.choices: + # Convert tool calls from response + tool_calls = [] + for tool_call in choice.message.tool_calls: + function_call = ConversationToolCallsOfFunction( + name=tool_call.function.name, + arguments=tool_call.function.arguments + ) + tool_calls.append(ConversationToolCalls( + id=tool_call.id if tool_call.id else None, + function=function_call + )) + + result_message = ConversationResultMessage( + content=choice.message.content, + tool_calls=tool_calls + ) + + choices.append(ConversationResultChoices( + finish_reason=choice.finish_reason, + index=choice.index, + message=result_message + )) + + outputs.append(ConversationResultAlpha2(choices=choices)) + + return ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) + except RpcError as err: + raise DaprGrpcError(err) from err + def schedule_job_alpha1(self, job: Job) -> DaprResponse: """Schedules a job to be triggered at a specified time or interval. diff --git a/dapr/proto/common/v1/common_pb2.py b/dapr/proto/common/v1/common_pb2.py index 7f8feb46..21ef9de3 100644 --- a/dapr/proto/common/v1/common_pb2.py +++ b/dapr/proto/common/v1/common_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/common/v1/common.proto -# Protobuf Python Version: 4.25.1 +# Protobuf Python Version: 6.31.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'dapr/proto/common/v1/common.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -21,12 +31,12 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dapr.proto.common.v1.common_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - _globals['DESCRIPTOR']._options = None +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\nio.dapr.v1B\014CommonProtosZ/github.com/dapr/dapr/pkg/proto/common/v1;common\252\002\033Dapr.Client.Autogen.Grpc.v1' - _globals['_STATEITEM_METADATAENTRY']._options = None + _globals['_STATEITEM_METADATAENTRY']._loaded_options = None _globals['_STATEITEM_METADATAENTRY']._serialized_options = b'8\001' - _globals['_CONFIGURATIONITEM_METADATAENTRY']._options = None + _globals['_CONFIGURATIONITEM_METADATAENTRY']._loaded_options = None _globals['_CONFIGURATIONITEM_METADATAENTRY']._serialized_options = b'8\001' _globals['_HTTPEXTENSION']._serialized_start=119 _globals['_HTTPEXTENSION']._serialized_end=327 diff --git a/dapr/proto/common/v1/common_pb2_grpc.py b/dapr/proto/common/v1/common_pb2_grpc.py index 2daafffe..5d08ae56 100644 --- a/dapr/proto/common/v1/common_pb2_grpc.py +++ b/dapr/proto/common/v1/common_pb2_grpc.py @@ -1,4 +1,24 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings + +GRPC_GENERATED_VERSION = '1.74.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in dapr/proto/common/v1/common_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/dapr/proto/runtime/v1/appcallback_pb2.py b/dapr/proto/runtime/v1/appcallback_pb2.py index 6773f762..eb8fdea4 100644 --- a/dapr/proto/runtime/v1/appcallback_pb2.py +++ b/dapr/proto/runtime/v1/appcallback_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/runtime/v1/appcallback.proto -# Protobuf Python Version: 4.25.1 +# Protobuf Python Version: 6.31.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'dapr/proto/runtime/v1/appcallback.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -23,16 +33,16 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dapr.proto.runtime.v1.appcallback_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - _globals['DESCRIPTOR']._options = None +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\nio.dapr.v1B\025DaprAppCallbackProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\252\002 Dapr.AppCallback.Autogen.Grpc.v1' - _globals['_TOPICEVENTBULKREQUESTENTRY_METADATAENTRY']._options = None + _globals['_TOPICEVENTBULKREQUESTENTRY_METADATAENTRY']._loaded_options = None _globals['_TOPICEVENTBULKREQUESTENTRY_METADATAENTRY']._serialized_options = b'8\001' - _globals['_TOPICEVENTBULKREQUEST_METADATAENTRY']._options = None + _globals['_TOPICEVENTBULKREQUEST_METADATAENTRY']._loaded_options = None _globals['_TOPICEVENTBULKREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BINDINGEVENTREQUEST_METADATAENTRY']._options = None + _globals['_BINDINGEVENTREQUEST_METADATAENTRY']._loaded_options = None _globals['_BINDINGEVENTREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_TOPICSUBSCRIPTION_METADATAENTRY']._options = None + _globals['_TOPICSUBSCRIPTION_METADATAENTRY']._loaded_options = None _globals['_TOPICSUBSCRIPTION_METADATAENTRY']._serialized_options = b'8\001' _globals['_JOBEVENTREQUEST']._serialized_start=188 _globals['_JOBEVENTREQUEST']._serialized_end=354 diff --git a/dapr/proto/runtime/v1/appcallback_pb2_grpc.py b/dapr/proto/runtime/v1/appcallback_pb2_grpc.py index b203f7db..3d8afd6c 100644 --- a/dapr/proto/runtime/v1/appcallback_pb2_grpc.py +++ b/dapr/proto/runtime/v1/appcallback_pb2_grpc.py @@ -1,11 +1,31 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from dapr.proto.common.v1 import common_pb2 as dapr_dot_proto_dot_common_dot_v1_dot_common__pb2 from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +GRPC_GENERATED_VERSION = '1.74.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in dapr/proto/runtime/v1/appcallback_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + class AppCallbackStub(object): """AppCallback V1 allows user application to interact with Dapr runtime. @@ -23,27 +43,27 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.AppCallback/OnInvoke', request_serializer=dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - ) + _registered_method=True) self.ListTopicSubscriptions = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/ListTopicSubscriptions', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListTopicSubscriptionsResponse.FromString, - ) + _registered_method=True) self.OnTopicEvent = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/OnTopicEvent', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventResponse.FromString, - ) + _registered_method=True) self.ListInputBindings = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/ListInputBindings', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListInputBindingsResponse.FromString, - ) + _registered_method=True) self.OnBindingEvent = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/OnBindingEvent', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventResponse.FromString, - ) + _registered_method=True) class AppCallbackServicer(object): @@ -122,6 +142,7 @@ def add_AppCallbackServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallback', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallback', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -142,11 +163,21 @@ def OnInvoke(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/OnInvoke', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallback/OnInvoke', dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeRequest.SerializeToString, dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ListTopicSubscriptions(request, @@ -159,11 +190,21 @@ def ListTopicSubscriptions(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/ListTopicSubscriptions', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallback/ListTopicSubscriptions', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListTopicSubscriptionsResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def OnTopicEvent(request, @@ -176,11 +217,21 @@ def OnTopicEvent(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/OnTopicEvent', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallback/OnTopicEvent', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ListInputBindings(request, @@ -193,11 +244,21 @@ def ListInputBindings(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/ListInputBindings', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallback/ListInputBindings', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListInputBindingsResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def OnBindingEvent(request, @@ -210,11 +271,21 @@ def OnBindingEvent(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/OnBindingEvent', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallback/OnBindingEvent', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) class AppCallbackHealthCheckStub(object): @@ -232,7 +303,7 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.AppCallbackHealthCheck/HealthCheck', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.HealthCheckResponse.FromString, - ) + _registered_method=True) class AppCallbackHealthCheckServicer(object): @@ -259,6 +330,7 @@ def add_AppCallbackHealthCheckServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallbackHealthCheck', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallbackHealthCheck', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -278,11 +350,21 @@ def HealthCheck(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallbackHealthCheck/HealthCheck', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallbackHealthCheck/HealthCheck', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.HealthCheckResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) class AppCallbackAlphaStub(object): @@ -300,12 +382,12 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.AppCallbackAlpha/OnBulkTopicEventAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkResponse.FromString, - ) + _registered_method=True) self.OnJobEventAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallbackAlpha/OnJobEventAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventResponse.FromString, - ) + _registered_method=True) class AppCallbackAlphaServicer(object): @@ -344,6 +426,7 @@ def add_AppCallbackAlphaServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallbackAlpha', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallbackAlpha', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -363,11 +446,21 @@ def OnBulkTopicEventAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallbackAlpha/OnBulkTopicEventAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallbackAlpha/OnBulkTopicEventAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def OnJobEventAlpha1(request, @@ -380,8 +473,18 @@ def OnJobEventAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallbackAlpha/OnJobEventAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.AppCallbackAlpha/OnJobEventAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/dapr/proto/runtime/v1/dapr_pb2.py b/dapr/proto/runtime/v1/dapr_pb2.py index 76c3b50b..ec2d728b 100644 --- a/dapr/proto/runtime/v1/dapr_pb2.py +++ b/dapr/proto/runtime/v1/dapr_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/runtime/v1/dapr.proto -# Protobuf Python Version: 4.25.1 +# Protobuf Python Version: 6.31.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'dapr/proto/runtime/v1/dapr.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -19,98 +29,112 @@ from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n dapr/proto/runtime/v1/dapr.proto\x12\x15\x64\x61pr.proto.runtime.v1\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!dapr/proto/common/v1/common.proto\x1a\'dapr/proto/runtime/v1/appcallback.proto\"X\n\x14InvokeServiceRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x34\n\x07message\x18\x03 \x01(\x0b\x32#.dapr.proto.common.v1.InvokeRequest\"\xf5\x01\n\x0fGetStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12H\n\x0b\x63onsistency\x18\x03 \x01(\x0e\x32\x33.dapr.proto.common.v1.StateOptions.StateConsistency\x12\x46\n\x08metadata\x18\x04 \x03(\x0b\x32\x34.dapr.proto.runtime.v1.GetStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc9\x01\n\x13GetBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12\x13\n\x0bparallelism\x18\x03 \x01(\x05\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.GetBulkStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x14GetBulkStateResponse\x12\x33\n\x05items\x18\x01 \x03(\x0b\x32$.dapr.proto.runtime.v1.BulkStateItem\"\xbe\x01\n\rBulkStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x44\n\x08metadata\x18\x05 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.BulkStateItem.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa8\x01\n\x10GetStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\x12\x44\x65leteStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12(\n\x04\x65tag\x18\x03 \x01(\x0b\x32\x1a.dapr.proto.common.v1.Etag\x12\x33\n\x07options\x18\x04 \x01(\x0b\x32\".dapr.proto.common.v1.StateOptions\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.DeleteStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x16\x44\x65leteBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"W\n\x10SaveStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\xbc\x01\n\x11QueryStateRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\r\n\x05query\x18\x02 \x01(\t\x12H\n\x08metadata\x18\x03 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.QueryStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"H\n\x0eQueryStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd7\x01\n\x12QueryStateResponse\x12\x36\n\x07results\x18\x01 \x03(\x0b\x32%.dapr.proto.runtime.v1.QueryStateItem\x12\r\n\x05token\x18\x02 \x01(\t\x12I\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.QueryStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xdf\x01\n\x13PublishEventRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x11\x64\x61ta_content_type\x18\x04 \x01(\t\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.PublishEventRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf5\x01\n\x12\x42ulkPublishRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12?\n\x07\x65ntries\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.BulkPublishRequestEntry\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.BulkPublishRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd1\x01\n\x17\x42ulkPublishRequestEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12N\n\x08metadata\x18\x04 \x03(\x0b\x32<.dapr.proto.runtime.v1.BulkPublishRequestEntry.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"c\n\x13\x42ulkPublishResponse\x12L\n\rfailedEntries\x18\x01 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.BulkPublishResponseFailedEntry\"A\n\x1e\x42ulkPublishResponseFailedEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x84\x02\n!SubscribeTopicEventsRequestAlpha1\x12Z\n\x0finitial_request\x18\x01 \x01(\x0b\x32?.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1H\x00\x12\\\n\x0f\x65vent_processed\x18\x02 \x01(\x0b\x32\x41.dapr.proto.runtime.v1.SubscribeTopicEventsRequestProcessedAlpha1H\x00\x42%\n#subscribe_topic_events_request_type\"\x96\x02\n(SubscribeTopicEventsRequestInitialAlpha1\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12_\n\x08metadata\x18\x03 \x03(\x0b\x32M.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1.MetadataEntry\x12\x1e\n\x11\x64\x65\x61\x64_letter_topic\x18\x04 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x14\n\x12_dead_letter_topic\"s\n*SubscribeTopicEventsRequestProcessedAlpha1\x12\n\n\x02id\x18\x01 \x01(\t\x12\x39\n\x06status\x18\x02 \x01(\x0b\x32).dapr.proto.runtime.v1.TopicEventResponse\"\xed\x01\n\"SubscribeTopicEventsResponseAlpha1\x12\\\n\x10initial_response\x18\x01 \x01(\x0b\x32@.dapr.proto.runtime.v1.SubscribeTopicEventsResponseInitialAlpha1H\x00\x12\x41\n\revent_message\x18\x02 \x01(\x0b\x32(.dapr.proto.runtime.v1.TopicEventRequestH\x00\x42&\n$subscribe_topic_events_response_type\"+\n)SubscribeTopicEventsResponseInitialAlpha1\"\xc3\x01\n\x14InvokeBindingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12K\n\x08metadata\x18\x03 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.InvokeBindingRequest.MetadataEntry\x12\x11\n\toperation\x18\x04 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa4\x01\n\x15InvokeBindingResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.InvokeBindingResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb8\x01\n\x10GetSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x0b\n\x03key\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x11GetSecretResponse\x12@\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.GetSecretResponse.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb3\x01\n\x14GetBulkSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12K\n\x08metadata\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.GetBulkSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x0eSecretResponse\x12\x43\n\x07secrets\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.SecretResponse.SecretsEntry\x1a.\n\x0cSecretsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb1\x01\n\x15GetBulkSecretResponse\x12\x44\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.GetBulkSecretResponse.DataEntry\x1aR\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x34\n\x05value\x18\x02 \x01(\x0b\x32%.dapr.proto.runtime.v1.SecretResponse:\x02\x38\x01\"f\n\x1bTransactionalStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x30\n\x07request\x18\x02 \x01(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\x83\x02\n\x1e\x45xecuteStateTransactionRequest\x12\x11\n\tstoreName\x18\x01 \x01(\t\x12\x46\n\noperations\x18\x02 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.TransactionalStateOperation\x12U\n\x08metadata\x18\x03 \x03(\x0b\x32\x43.dapr.proto.runtime.v1.ExecuteStateTransactionRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x01\n\x19RegisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x06 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\x0b\n\x03ttl\x18\x08 \x01(\t\"e\n\x1bUnregisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xac\x01\n\x1cRegisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x0b\n\x03ttl\x18\x07 \x01(\t\"h\n\x1eUnregisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"]\n\x14GetActorStateRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0b\n\x03key\x18\x03 \x01(\t\"\xa4\x01\n\x15GetActorStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetActorStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n#ExecuteActorStateTransactionRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12K\n\noperations\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.TransactionalActorStateOperation\"\xf5\x01\n TransactionalActorStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12W\n\x08metadata\x18\x04 \x03(\x0b\x32\x45.dapr.proto.runtime.v1.TransactionalActorStateOperation.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe8\x01\n\x12InvokeActorRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.InvokeActorRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"#\n\x13InvokeActorResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x14\n\x12GetMetadataRequest\"\xf6\x06\n\x13GetMetadataResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12Q\n\x13\x61\x63tive_actors_count\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountB\x02\x18\x01R\x06\x61\x63tors\x12V\n\x15registered_components\x18\x03 \x03(\x0b\x32+.dapr.proto.runtime.v1.RegisteredComponentsR\ncomponents\x12\x65\n\x11\x65xtended_metadata\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.GetMetadataResponse.ExtendedMetadataEntryR\x08\x65xtended\x12O\n\rsubscriptions\x18\x05 \x03(\x0b\x32).dapr.proto.runtime.v1.PubsubSubscriptionR\rsubscriptions\x12R\n\x0ehttp_endpoints\x18\x06 \x03(\x0b\x32+.dapr.proto.runtime.v1.MetadataHTTPEndpointR\rhttpEndpoints\x12j\n\x19\x61pp_connection_properties\x18\x07 \x01(\x0b\x32..dapr.proto.runtime.v1.AppConnectionPropertiesR\x17\x61ppConnectionProperties\x12\'\n\x0fruntime_version\x18\x08 \x01(\tR\x0eruntimeVersion\x12)\n\x10\x65nabled_features\x18\t \x03(\tR\x0f\x65nabledFeatures\x12H\n\ractor_runtime\x18\n \x01(\x0b\x32#.dapr.proto.runtime.v1.ActorRuntimeR\x0c\x61\x63torRuntime\x12K\n\tscheduler\x18\x0b \x01(\x0b\x32(.dapr.proto.runtime.v1.MetadataSchedulerH\x00R\tscheduler\x88\x01\x01\x1a\x37\n\x15\x45xtendedMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_scheduler\"0\n\x11MetadataScheduler\x12\x1b\n\x13\x63onnected_addresses\x18\x01 \x03(\t\"\xbc\x02\n\x0c\x41\x63torRuntime\x12]\n\x0eruntime_status\x18\x01 \x01(\x0e\x32\x36.dapr.proto.runtime.v1.ActorRuntime.ActorRuntimeStatusR\rruntimeStatus\x12M\n\ractive_actors\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountR\x0c\x61\x63tiveActors\x12\x1d\n\nhost_ready\x18\x03 \x01(\x08R\thostReady\x12\x1c\n\tplacement\x18\x04 \x01(\tR\tplacement\"A\n\x12\x41\x63torRuntimeStatus\x12\x10\n\x0cINITIALIZING\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x0b\n\x07RUNNING\x10\x02\"0\n\x11\x41\x63tiveActorsCount\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"Y\n\x14RegisteredComponents\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\"*\n\x14MetadataHTTPEndpoint\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xd1\x01\n\x17\x41ppConnectionProperties\x12\x0c\n\x04port\x18\x01 \x01(\x05\x12\x10\n\x08protocol\x18\x02 \x01(\t\x12\'\n\x0f\x63hannel_address\x18\x03 \x01(\tR\x0e\x63hannelAddress\x12\'\n\x0fmax_concurrency\x18\x04 \x01(\x05R\x0emaxConcurrency\x12\x44\n\x06health\x18\x05 \x01(\x0b\x32\x34.dapr.proto.runtime.v1.AppConnectionHealthProperties\"\xdc\x01\n\x1d\x41ppConnectionHealthProperties\x12*\n\x11health_check_path\x18\x01 \x01(\tR\x0fhealthCheckPath\x12\x32\n\x15health_probe_interval\x18\x02 \x01(\tR\x13healthProbeInterval\x12\x30\n\x14health_probe_timeout\x18\x03 \x01(\tR\x12healthProbeTimeout\x12)\n\x10health_threshold\x18\x04 \x01(\x05R\x0fhealthThreshold\"\x86\x03\n\x12PubsubSubscription\x12\x1f\n\x0bpubsub_name\x18\x01 \x01(\tR\npubsubname\x12\x14\n\x05topic\x18\x02 \x01(\tR\x05topic\x12S\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.PubsubSubscription.MetadataEntryR\x08metadata\x12\x44\n\x05rules\x18\x04 \x01(\x0b\x32..dapr.proto.runtime.v1.PubsubSubscriptionRulesR\x05rules\x12*\n\x11\x64\x65\x61\x64_letter_topic\x18\x05 \x01(\tR\x0f\x64\x65\x61\x64LetterTopic\x12\x41\n\x04type\x18\x06 \x01(\x0e\x32-.dapr.proto.runtime.v1.PubsubSubscriptionTypeR\x04type\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"W\n\x17PubsubSubscriptionRules\x12<\n\x05rules\x18\x01 \x03(\x0b\x32-.dapr.proto.runtime.v1.PubsubSubscriptionRule\"5\n\x16PubsubSubscriptionRule\x12\r\n\x05match\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"0\n\x12SetMetadataRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xbc\x01\n\x17GetConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12N\n\x08metadata\x18\x03 \x03(\x0b\x32<.dapr.proto.runtime.v1.GetConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\x18GetConfigurationResponse\x12I\n\x05items\x18\x01 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"\xc8\x01\n\x1dSubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12T\n\x08metadata\x18\x03 \x03(\x0b\x32\x42.dapr.proto.runtime.v1.SubscribeConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"A\n\x1fUnsubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\"\xd4\x01\n\x1eSubscribeConfigurationResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12O\n\x05items\x18\x02 \x03(\x0b\x32@.dapr.proto.runtime.v1.SubscribeConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"?\n UnsubscribeConfigurationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x9b\x01\n\x0eTryLockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\x12*\n\x11\x65xpiry_in_seconds\x18\x04 \x01(\x05R\x0f\x65xpiryInSeconds\"\"\n\x0fTryLockResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"n\n\rUnlockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\"\xae\x01\n\x0eUnlockResponse\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.dapr.proto.runtime.v1.UnlockResponse.Status\"^\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\x17\n\x13LOCK_DOES_NOT_EXIST\x10\x01\x12\x1a\n\x16LOCK_BELONGS_TO_OTHERS\x10\x02\x12\x12\n\x0eINTERNAL_ERROR\x10\x03\"\xb0\x01\n\x13SubtleGetKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x34.dapr.proto.runtime.v1.SubtleGetKeyRequest.KeyFormat\"\x1e\n\tKeyFormat\x12\x07\n\x03PEM\x10\x00\x12\x08\n\x04JSON\x10\x01\"C\n\x14SubtleGetKeyResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\npublic_key\x18\x02 \x01(\tR\tpublicKey\"\xb6\x01\n\x14SubtleEncryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x11\n\tplaintext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"8\n\x15SubtleEncryptResponse\x12\x12\n\nciphertext\x18\x01 \x01(\x0c\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xc4\x01\n\x14SubtleDecryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\"*\n\x15SubtleDecryptResponse\x12\x11\n\tplaintext\x18\x01 \x01(\x0c\"\xc8\x01\n\x14SubtleWrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12#\n\rplaintext_key\x18\x02 \x01(\x0cR\x0cplaintextKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"E\n\x15SubtleWrapKeyResponse\x12\x1f\n\x0bwrapped_key\x18\x01 \x01(\x0cR\nwrappedKey\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xd3\x01\n\x16SubtleUnwrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x1f\n\x0bwrapped_key\x18\x02 \x01(\x0cR\nwrappedKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\">\n\x17SubtleUnwrapKeyResponse\x12#\n\rplaintext_key\x18\x01 \x01(\x0cR\x0cplaintextKey\"x\n\x11SubtleSignRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\"\'\n\x12SubtleSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x8d\x01\n\x13SubtleVerifyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"%\n\x14SubtleVerifyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x85\x01\n\x0e\x45ncryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.EncryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\xfe\x01\n\x15\x45ncryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x02 \x01(\tR\x07keyName\x12\x1a\n\x12key_wrap_algorithm\x18\x03 \x01(\t\x12\x1e\n\x16\x64\x61ta_encryption_cipher\x18\n \x01(\t\x12\x37\n\x18omit_decryption_key_name\x18\x0b \x01(\x08R\x15omitDecryptionKeyName\x12.\n\x13\x64\x65\x63ryption_key_name\x18\x0c \x01(\tR\x11\x64\x65\x63ryptionKeyName\"G\n\x0f\x45ncryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\x85\x01\n\x0e\x44\x65\x63ryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.DecryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"Y\n\x15\x44\x65\x63ryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x0c \x01(\tR\x07keyName\"G\n\x0f\x44\x65\x63ryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"d\n\x12GetWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x84\x03\n\x13GetWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12#\n\rworkflow_name\x18\x02 \x01(\tR\x0cworkflowName\x12\x39\n\ncreated_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x42\n\x0flast_updated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\rlastUpdatedAt\x12%\n\x0eruntime_status\x18\x05 \x01(\tR\rruntimeStatus\x12N\n\nproperties\x18\x06 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetWorkflowResponse.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x02\n\x14StartWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12#\n\rworkflow_name\x18\x03 \x01(\tR\x0cworkflowName\x12I\n\x07options\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.StartWorkflowRequest.OptionsEntry\x12\r\n\x05input\x18\x05 \x01(\x0c\x1a.\n\x0cOptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x15StartWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\"j\n\x18TerminateWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"f\n\x14PauseWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"g\n\x15ResumeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x9e\x01\n\x19RaiseEventWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12\x1d\n\nevent_name\x18\x03 \x01(\tR\teventName\x12\x12\n\nevent_data\x18\x04 \x01(\x0c\"f\n\x14PurgeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x11\n\x0fShutdownRequest\"\xed\x02\n\x03Job\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n\x08schedule\x18\x02 \x01(\tH\x00R\x08schedule\x88\x01\x01\x12\x1d\n\x07repeats\x18\x03 \x01(\rH\x01R\x07repeats\x88\x01\x01\x12\x1e\n\x08\x64ue_time\x18\x04 \x01(\tH\x02R\x07\x64ueTime\x88\x01\x01\x12\x15\n\x03ttl\x18\x05 \x01(\tH\x03R\x03ttl\x88\x01\x01\x12(\n\x04\x64\x61ta\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyR\x04\x64\x61ta\x12\x1c\n\toverwrite\x18\x07 \x01(\x08R\toverwrite\x12R\n\x0e\x66\x61ilure_policy\x18\x08 \x01(\x0b\x32&.dapr.proto.common.v1.JobFailurePolicyH\x04R\rfailurePolicy\x88\x01\x01\x42\x0b\n\t_scheduleB\n\n\x08_repeatsB\x0b\n\t_due_timeB\x06\n\x04_ttlB\x11\n\x0f_failure_policy\"=\n\x12ScheduleJobRequest\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\"\x15\n\x13ScheduleJobResponse\"\x1d\n\rGetJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x0eGetJobResponse\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\" \n\x10\x44\x65leteJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteJobResponse\"\xe7\x03\n\x13\x43onversationRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\tcontextID\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x38\n\x06inputs\x18\x03 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationInput\x12N\n\nparameters\x18\x04 \x03(\x0b\x32:.dapr.proto.runtime.v1.ConversationRequest.ParametersEntry\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.ConversationRequest.MetadataEntry\x12\x15\n\x08scrubPII\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_contextIDB\x0b\n\t_scrubPIIB\x0e\n\x0c_temperature\"d\n\x11\x43onversationInput\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x11\n\x04role\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08scrubPII\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x07\n\x05_roleB\x0b\n\t_scrubPII\"\xbc\x01\n\x12\x43onversationResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12M\n\nparameters\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.ConversationResult.ParametersEntry\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\"x\n\x14\x43onversationResponse\x12\x16\n\tcontextID\x18\x01 \x01(\tH\x00\x88\x01\x01\x12:\n\x07outputs\x18\x02 \x03(\x0b\x32).dapr.proto.runtime.v1.ConversationResultB\x0c\n\n_contextID*W\n\x16PubsubSubscriptionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x44\x45\x43LARATIVE\x10\x01\x12\x10\n\x0cPROGRAMMATIC\x10\x02\x12\r\n\tSTREAMING\x10\x03\x32\xbe\x31\n\x04\x44\x61pr\x12\x64\n\rInvokeService\x12+.dapr.proto.runtime.v1.InvokeServiceRequest\x1a$.dapr.proto.common.v1.InvokeResponse\"\x00\x12]\n\x08GetState\x12&.dapr.proto.runtime.v1.GetStateRequest\x1a\'.dapr.proto.runtime.v1.GetStateResponse\"\x00\x12i\n\x0cGetBulkState\x12*.dapr.proto.runtime.v1.GetBulkStateRequest\x1a+.dapr.proto.runtime.v1.GetBulkStateResponse\"\x00\x12N\n\tSaveState\x12\'.dapr.proto.runtime.v1.SaveStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12i\n\x10QueryStateAlpha1\x12(.dapr.proto.runtime.v1.QueryStateRequest\x1a).dapr.proto.runtime.v1.QueryStateResponse\"\x00\x12R\n\x0b\x44\x65leteState\x12).dapr.proto.runtime.v1.DeleteStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x0f\x44\x65leteBulkState\x12-.dapr.proto.runtime.v1.DeleteBulkStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17\x45xecuteStateTransaction\x12\x35.dapr.proto.runtime.v1.ExecuteStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n\x0cPublishEvent\x12*.dapr.proto.runtime.v1.PublishEventRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x16\x42ulkPublishEventAlpha1\x12).dapr.proto.runtime.v1.BulkPublishRequest\x1a*.dapr.proto.runtime.v1.BulkPublishResponse\"\x00\x12\x97\x01\n\x1aSubscribeTopicEventsAlpha1\x12\x38.dapr.proto.runtime.v1.SubscribeTopicEventsRequestAlpha1\x1a\x39.dapr.proto.runtime.v1.SubscribeTopicEventsResponseAlpha1\"\x00(\x01\x30\x01\x12l\n\rInvokeBinding\x12+.dapr.proto.runtime.v1.InvokeBindingRequest\x1a,.dapr.proto.runtime.v1.InvokeBindingResponse\"\x00\x12`\n\tGetSecret\x12\'.dapr.proto.runtime.v1.GetSecretRequest\x1a(.dapr.proto.runtime.v1.GetSecretResponse\"\x00\x12l\n\rGetBulkSecret\x12+.dapr.proto.runtime.v1.GetBulkSecretRequest\x1a,.dapr.proto.runtime.v1.GetBulkSecretResponse\"\x00\x12`\n\x12RegisterActorTimer\x12\x30.dapr.proto.runtime.v1.RegisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x14UnregisterActorTimer\x12\x32.dapr.proto.runtime.v1.UnregisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x15RegisterActorReminder\x12\x33.dapr.proto.runtime.v1.RegisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17UnregisterActorReminder\x12\x35.dapr.proto.runtime.v1.UnregisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\rGetActorState\x12+.dapr.proto.runtime.v1.GetActorStateRequest\x1a,.dapr.proto.runtime.v1.GetActorStateResponse\"\x00\x12t\n\x1c\x45xecuteActorStateTransaction\x12:.dapr.proto.runtime.v1.ExecuteActorStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bInvokeActor\x12).dapr.proto.runtime.v1.InvokeActorRequest\x1a*.dapr.proto.runtime.v1.InvokeActorResponse\"\x00\x12{\n\x16GetConfigurationAlpha1\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12u\n\x10GetConfiguration\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12\x8f\x01\n\x1cSubscribeConfigurationAlpha1\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x89\x01\n\x16SubscribeConfiguration\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x93\x01\n\x1eUnsubscribeConfigurationAlpha1\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12\x8d\x01\n\x18UnsubscribeConfiguration\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12`\n\rTryLockAlpha1\x12%.dapr.proto.runtime.v1.TryLockRequest\x1a&.dapr.proto.runtime.v1.TryLockResponse\"\x00\x12]\n\x0cUnlockAlpha1\x12$.dapr.proto.runtime.v1.UnlockRequest\x1a%.dapr.proto.runtime.v1.UnlockResponse\"\x00\x12\x62\n\rEncryptAlpha1\x12%.dapr.proto.runtime.v1.EncryptRequest\x1a&.dapr.proto.runtime.v1.EncryptResponse(\x01\x30\x01\x12\x62\n\rDecryptAlpha1\x12%.dapr.proto.runtime.v1.DecryptRequest\x1a&.dapr.proto.runtime.v1.DecryptResponse(\x01\x30\x01\x12\x66\n\x0bGetMetadata\x12).dapr.proto.runtime.v1.GetMetadataRequest\x1a*.dapr.proto.runtime.v1.GetMetadataResponse\"\x00\x12R\n\x0bSetMetadata\x12).dapr.proto.runtime.v1.SetMetadataRequest\x1a\x16.google.protobuf.Empty\"\x00\x12m\n\x12SubtleGetKeyAlpha1\x12*.dapr.proto.runtime.v1.SubtleGetKeyRequest\x1a+.dapr.proto.runtime.v1.SubtleGetKeyResponse\x12p\n\x13SubtleEncryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleEncryptRequest\x1a,.dapr.proto.runtime.v1.SubtleEncryptResponse\x12p\n\x13SubtleDecryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleDecryptRequest\x1a,.dapr.proto.runtime.v1.SubtleDecryptResponse\x12p\n\x13SubtleWrapKeyAlpha1\x12+.dapr.proto.runtime.v1.SubtleWrapKeyRequest\x1a,.dapr.proto.runtime.v1.SubtleWrapKeyResponse\x12v\n\x15SubtleUnwrapKeyAlpha1\x12-.dapr.proto.runtime.v1.SubtleUnwrapKeyRequest\x1a..dapr.proto.runtime.v1.SubtleUnwrapKeyResponse\x12g\n\x10SubtleSignAlpha1\x12(.dapr.proto.runtime.v1.SubtleSignRequest\x1a).dapr.proto.runtime.v1.SubtleSignResponse\x12m\n\x12SubtleVerifyAlpha1\x12*.dapr.proto.runtime.v1.SubtleVerifyRequest\x1a+.dapr.proto.runtime.v1.SubtleVerifyResponse\x12u\n\x13StartWorkflowAlpha1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x03\x88\x02\x01\x12o\n\x11GetWorkflowAlpha1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x03\x88\x02\x01\x12_\n\x13PurgeWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12g\n\x17TerminateWorkflowAlpha1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12_\n\x13PauseWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12\x61\n\x14ResumeWorkflowAlpha1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12i\n\x18RaiseEventWorkflowAlpha1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12q\n\x12StartWorkflowBeta1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12k\n\x10GetWorkflowBeta1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12[\n\x12PurgeWorkflowBeta1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x63\n\x16TerminateWorkflowBeta1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x12PauseWorkflowBeta1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x13ResumeWorkflowBeta1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x17RaiseEventWorkflowBeta1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12L\n\x08Shutdown\x12&.dapr.proto.runtime.v1.ShutdownRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\x11ScheduleJobAlpha1\x12).dapr.proto.runtime.v1.ScheduleJobRequest\x1a*.dapr.proto.runtime.v1.ScheduleJobResponse\"\x00\x12]\n\x0cGetJobAlpha1\x12$.dapr.proto.runtime.v1.GetJobRequest\x1a%.dapr.proto.runtime.v1.GetJobResponse\"\x00\x12\x66\n\x0f\x44\x65leteJobAlpha1\x12\'.dapr.proto.runtime.v1.DeleteJobRequest\x1a(.dapr.proto.runtime.v1.DeleteJobResponse\"\x00\x12k\n\x0e\x43onverseAlpha1\x12*.dapr.proto.runtime.v1.ConversationRequest\x1a+.dapr.proto.runtime.v1.ConversationResponse\"\x00\x42i\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\xaa\x02\x1b\x44\x61pr.Client.Autogen.Grpc.v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n dapr/proto/runtime/v1/dapr.proto\x12\x15\x64\x61pr.proto.runtime.v1\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!dapr/proto/common/v1/common.proto\x1a\'dapr/proto/runtime/v1/appcallback.proto\"X\n\x14InvokeServiceRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x34\n\x07message\x18\x03 \x01(\x0b\x32#.dapr.proto.common.v1.InvokeRequest\"\xf5\x01\n\x0fGetStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12H\n\x0b\x63onsistency\x18\x03 \x01(\x0e\x32\x33.dapr.proto.common.v1.StateOptions.StateConsistency\x12\x46\n\x08metadata\x18\x04 \x03(\x0b\x32\x34.dapr.proto.runtime.v1.GetStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc9\x01\n\x13GetBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12\x13\n\x0bparallelism\x18\x03 \x01(\x05\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.GetBulkStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x14GetBulkStateResponse\x12\x33\n\x05items\x18\x01 \x03(\x0b\x32$.dapr.proto.runtime.v1.BulkStateItem\"\xbe\x01\n\rBulkStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x44\n\x08metadata\x18\x05 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.BulkStateItem.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa8\x01\n\x10GetStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\x12\x44\x65leteStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12(\n\x04\x65tag\x18\x03 \x01(\x0b\x32\x1a.dapr.proto.common.v1.Etag\x12\x33\n\x07options\x18\x04 \x01(\x0b\x32\".dapr.proto.common.v1.StateOptions\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.DeleteStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x16\x44\x65leteBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"W\n\x10SaveStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\xbc\x01\n\x11QueryStateRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\r\n\x05query\x18\x02 \x01(\t\x12H\n\x08metadata\x18\x03 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.QueryStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"H\n\x0eQueryStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd7\x01\n\x12QueryStateResponse\x12\x36\n\x07results\x18\x01 \x03(\x0b\x32%.dapr.proto.runtime.v1.QueryStateItem\x12\r\n\x05token\x18\x02 \x01(\t\x12I\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.QueryStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xdf\x01\n\x13PublishEventRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x11\x64\x61ta_content_type\x18\x04 \x01(\t\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.PublishEventRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf5\x01\n\x12\x42ulkPublishRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12?\n\x07\x65ntries\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.BulkPublishRequestEntry\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.BulkPublishRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd1\x01\n\x17\x42ulkPublishRequestEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12N\n\x08metadata\x18\x04 \x03(\x0b\x32<.dapr.proto.runtime.v1.BulkPublishRequestEntry.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"c\n\x13\x42ulkPublishResponse\x12L\n\rfailedEntries\x18\x01 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.BulkPublishResponseFailedEntry\"A\n\x1e\x42ulkPublishResponseFailedEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x84\x02\n!SubscribeTopicEventsRequestAlpha1\x12Z\n\x0finitial_request\x18\x01 \x01(\x0b\x32?.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1H\x00\x12\\\n\x0f\x65vent_processed\x18\x02 \x01(\x0b\x32\x41.dapr.proto.runtime.v1.SubscribeTopicEventsRequestProcessedAlpha1H\x00\x42%\n#subscribe_topic_events_request_type\"\x96\x02\n(SubscribeTopicEventsRequestInitialAlpha1\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12_\n\x08metadata\x18\x03 \x03(\x0b\x32M.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1.MetadataEntry\x12\x1e\n\x11\x64\x65\x61\x64_letter_topic\x18\x04 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x14\n\x12_dead_letter_topic\"s\n*SubscribeTopicEventsRequestProcessedAlpha1\x12\n\n\x02id\x18\x01 \x01(\t\x12\x39\n\x06status\x18\x02 \x01(\x0b\x32).dapr.proto.runtime.v1.TopicEventResponse\"\xed\x01\n\"SubscribeTopicEventsResponseAlpha1\x12\\\n\x10initial_response\x18\x01 \x01(\x0b\x32@.dapr.proto.runtime.v1.SubscribeTopicEventsResponseInitialAlpha1H\x00\x12\x41\n\revent_message\x18\x02 \x01(\x0b\x32(.dapr.proto.runtime.v1.TopicEventRequestH\x00\x42&\n$subscribe_topic_events_response_type\"+\n)SubscribeTopicEventsResponseInitialAlpha1\"\xc3\x01\n\x14InvokeBindingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12K\n\x08metadata\x18\x03 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.InvokeBindingRequest.MetadataEntry\x12\x11\n\toperation\x18\x04 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa4\x01\n\x15InvokeBindingResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.InvokeBindingResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb8\x01\n\x10GetSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x0b\n\x03key\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x11GetSecretResponse\x12@\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.GetSecretResponse.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb3\x01\n\x14GetBulkSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12K\n\x08metadata\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.GetBulkSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x0eSecretResponse\x12\x43\n\x07secrets\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.SecretResponse.SecretsEntry\x1a.\n\x0cSecretsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb1\x01\n\x15GetBulkSecretResponse\x12\x44\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.GetBulkSecretResponse.DataEntry\x1aR\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x34\n\x05value\x18\x02 \x01(\x0b\x32%.dapr.proto.runtime.v1.SecretResponse:\x02\x38\x01\"f\n\x1bTransactionalStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x30\n\x07request\x18\x02 \x01(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\x83\x02\n\x1e\x45xecuteStateTransactionRequest\x12\x11\n\tstoreName\x18\x01 \x01(\t\x12\x46\n\noperations\x18\x02 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.TransactionalStateOperation\x12U\n\x08metadata\x18\x03 \x03(\x0b\x32\x43.dapr.proto.runtime.v1.ExecuteStateTransactionRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x01\n\x19RegisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x06 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\x0b\n\x03ttl\x18\x08 \x01(\t\"e\n\x1bUnregisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xac\x01\n\x1cRegisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x0b\n\x03ttl\x18\x07 \x01(\t\"h\n\x1eUnregisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"]\n\x14GetActorStateRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0b\n\x03key\x18\x03 \x01(\t\"\xa4\x01\n\x15GetActorStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetActorStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n#ExecuteActorStateTransactionRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12K\n\noperations\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.TransactionalActorStateOperation\"\xf5\x01\n TransactionalActorStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12W\n\x08metadata\x18\x04 \x03(\x0b\x32\x45.dapr.proto.runtime.v1.TransactionalActorStateOperation.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe8\x01\n\x12InvokeActorRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.InvokeActorRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"#\n\x13InvokeActorResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x14\n\x12GetMetadataRequest\"\xf6\x06\n\x13GetMetadataResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12Q\n\x13\x61\x63tive_actors_count\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountB\x02\x18\x01R\x06\x61\x63tors\x12V\n\x15registered_components\x18\x03 \x03(\x0b\x32+.dapr.proto.runtime.v1.RegisteredComponentsR\ncomponents\x12\x65\n\x11\x65xtended_metadata\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.GetMetadataResponse.ExtendedMetadataEntryR\x08\x65xtended\x12O\n\rsubscriptions\x18\x05 \x03(\x0b\x32).dapr.proto.runtime.v1.PubsubSubscriptionR\rsubscriptions\x12R\n\x0ehttp_endpoints\x18\x06 \x03(\x0b\x32+.dapr.proto.runtime.v1.MetadataHTTPEndpointR\rhttpEndpoints\x12j\n\x19\x61pp_connection_properties\x18\x07 \x01(\x0b\x32..dapr.proto.runtime.v1.AppConnectionPropertiesR\x17\x61ppConnectionProperties\x12\'\n\x0fruntime_version\x18\x08 \x01(\tR\x0eruntimeVersion\x12)\n\x10\x65nabled_features\x18\t \x03(\tR\x0f\x65nabledFeatures\x12H\n\ractor_runtime\x18\n \x01(\x0b\x32#.dapr.proto.runtime.v1.ActorRuntimeR\x0c\x61\x63torRuntime\x12K\n\tscheduler\x18\x0b \x01(\x0b\x32(.dapr.proto.runtime.v1.MetadataSchedulerH\x00R\tscheduler\x88\x01\x01\x1a\x37\n\x15\x45xtendedMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_scheduler\"0\n\x11MetadataScheduler\x12\x1b\n\x13\x63onnected_addresses\x18\x01 \x03(\t\"\xbc\x02\n\x0c\x41\x63torRuntime\x12]\n\x0eruntime_status\x18\x01 \x01(\x0e\x32\x36.dapr.proto.runtime.v1.ActorRuntime.ActorRuntimeStatusR\rruntimeStatus\x12M\n\ractive_actors\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountR\x0c\x61\x63tiveActors\x12\x1d\n\nhost_ready\x18\x03 \x01(\x08R\thostReady\x12\x1c\n\tplacement\x18\x04 \x01(\tR\tplacement\"A\n\x12\x41\x63torRuntimeStatus\x12\x10\n\x0cINITIALIZING\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x0b\n\x07RUNNING\x10\x02\"0\n\x11\x41\x63tiveActorsCount\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"Y\n\x14RegisteredComponents\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\"*\n\x14MetadataHTTPEndpoint\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xd1\x01\n\x17\x41ppConnectionProperties\x12\x0c\n\x04port\x18\x01 \x01(\x05\x12\x10\n\x08protocol\x18\x02 \x01(\t\x12\'\n\x0f\x63hannel_address\x18\x03 \x01(\tR\x0e\x63hannelAddress\x12\'\n\x0fmax_concurrency\x18\x04 \x01(\x05R\x0emaxConcurrency\x12\x44\n\x06health\x18\x05 \x01(\x0b\x32\x34.dapr.proto.runtime.v1.AppConnectionHealthProperties\"\xdc\x01\n\x1d\x41ppConnectionHealthProperties\x12*\n\x11health_check_path\x18\x01 \x01(\tR\x0fhealthCheckPath\x12\x32\n\x15health_probe_interval\x18\x02 \x01(\tR\x13healthProbeInterval\x12\x30\n\x14health_probe_timeout\x18\x03 \x01(\tR\x12healthProbeTimeout\x12)\n\x10health_threshold\x18\x04 \x01(\x05R\x0fhealthThreshold\"\x86\x03\n\x12PubsubSubscription\x12\x1f\n\x0bpubsub_name\x18\x01 \x01(\tR\npubsubname\x12\x14\n\x05topic\x18\x02 \x01(\tR\x05topic\x12S\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.PubsubSubscription.MetadataEntryR\x08metadata\x12\x44\n\x05rules\x18\x04 \x01(\x0b\x32..dapr.proto.runtime.v1.PubsubSubscriptionRulesR\x05rules\x12*\n\x11\x64\x65\x61\x64_letter_topic\x18\x05 \x01(\tR\x0f\x64\x65\x61\x64LetterTopic\x12\x41\n\x04type\x18\x06 \x01(\x0e\x32-.dapr.proto.runtime.v1.PubsubSubscriptionTypeR\x04type\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"W\n\x17PubsubSubscriptionRules\x12<\n\x05rules\x18\x01 \x03(\x0b\x32-.dapr.proto.runtime.v1.PubsubSubscriptionRule\"5\n\x16PubsubSubscriptionRule\x12\r\n\x05match\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"0\n\x12SetMetadataRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xbc\x01\n\x17GetConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12N\n\x08metadata\x18\x03 \x03(\x0b\x32<.dapr.proto.runtime.v1.GetConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\x18GetConfigurationResponse\x12I\n\x05items\x18\x01 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"\xc8\x01\n\x1dSubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12T\n\x08metadata\x18\x03 \x03(\x0b\x32\x42.dapr.proto.runtime.v1.SubscribeConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"A\n\x1fUnsubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\"\xd4\x01\n\x1eSubscribeConfigurationResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12O\n\x05items\x18\x02 \x03(\x0b\x32@.dapr.proto.runtime.v1.SubscribeConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"?\n UnsubscribeConfigurationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x9b\x01\n\x0eTryLockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\x12*\n\x11\x65xpiry_in_seconds\x18\x04 \x01(\x05R\x0f\x65xpiryInSeconds\"\"\n\x0fTryLockResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"n\n\rUnlockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\"\xae\x01\n\x0eUnlockResponse\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.dapr.proto.runtime.v1.UnlockResponse.Status\"^\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\x17\n\x13LOCK_DOES_NOT_EXIST\x10\x01\x12\x1a\n\x16LOCK_BELONGS_TO_OTHERS\x10\x02\x12\x12\n\x0eINTERNAL_ERROR\x10\x03\"\xb0\x01\n\x13SubtleGetKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x34.dapr.proto.runtime.v1.SubtleGetKeyRequest.KeyFormat\"\x1e\n\tKeyFormat\x12\x07\n\x03PEM\x10\x00\x12\x08\n\x04JSON\x10\x01\"C\n\x14SubtleGetKeyResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\npublic_key\x18\x02 \x01(\tR\tpublicKey\"\xb6\x01\n\x14SubtleEncryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x11\n\tplaintext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"8\n\x15SubtleEncryptResponse\x12\x12\n\nciphertext\x18\x01 \x01(\x0c\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xc4\x01\n\x14SubtleDecryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\"*\n\x15SubtleDecryptResponse\x12\x11\n\tplaintext\x18\x01 \x01(\x0c\"\xc8\x01\n\x14SubtleWrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12#\n\rplaintext_key\x18\x02 \x01(\x0cR\x0cplaintextKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"E\n\x15SubtleWrapKeyResponse\x12\x1f\n\x0bwrapped_key\x18\x01 \x01(\x0cR\nwrappedKey\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xd3\x01\n\x16SubtleUnwrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x1f\n\x0bwrapped_key\x18\x02 \x01(\x0cR\nwrappedKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\">\n\x17SubtleUnwrapKeyResponse\x12#\n\rplaintext_key\x18\x01 \x01(\x0cR\x0cplaintextKey\"x\n\x11SubtleSignRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\"\'\n\x12SubtleSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x8d\x01\n\x13SubtleVerifyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"%\n\x14SubtleVerifyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x85\x01\n\x0e\x45ncryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.EncryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\xfe\x01\n\x15\x45ncryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x02 \x01(\tR\x07keyName\x12\x1a\n\x12key_wrap_algorithm\x18\x03 \x01(\t\x12\x1e\n\x16\x64\x61ta_encryption_cipher\x18\n \x01(\t\x12\x37\n\x18omit_decryption_key_name\x18\x0b \x01(\x08R\x15omitDecryptionKeyName\x12.\n\x13\x64\x65\x63ryption_key_name\x18\x0c \x01(\tR\x11\x64\x65\x63ryptionKeyName\"G\n\x0f\x45ncryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\x85\x01\n\x0e\x44\x65\x63ryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.DecryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"Y\n\x15\x44\x65\x63ryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x0c \x01(\tR\x07keyName\"G\n\x0f\x44\x65\x63ryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"d\n\x12GetWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x84\x03\n\x13GetWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12#\n\rworkflow_name\x18\x02 \x01(\tR\x0cworkflowName\x12\x39\n\ncreated_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x42\n\x0flast_updated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\rlastUpdatedAt\x12%\n\x0eruntime_status\x18\x05 \x01(\tR\rruntimeStatus\x12N\n\nproperties\x18\x06 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetWorkflowResponse.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x02\n\x14StartWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12#\n\rworkflow_name\x18\x03 \x01(\tR\x0cworkflowName\x12I\n\x07options\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.StartWorkflowRequest.OptionsEntry\x12\r\n\x05input\x18\x05 \x01(\x0c\x1a.\n\x0cOptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x15StartWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\"j\n\x18TerminateWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"f\n\x14PauseWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"g\n\x15ResumeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x9e\x01\n\x19RaiseEventWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12\x1d\n\nevent_name\x18\x03 \x01(\tR\teventName\x12\x12\n\nevent_data\x18\x04 \x01(\x0c\"f\n\x14PurgeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x11\n\x0fShutdownRequest\"\xed\x02\n\x03Job\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n\x08schedule\x18\x02 \x01(\tH\x00R\x08schedule\x88\x01\x01\x12\x1d\n\x07repeats\x18\x03 \x01(\rH\x01R\x07repeats\x88\x01\x01\x12\x1e\n\x08\x64ue_time\x18\x04 \x01(\tH\x02R\x07\x64ueTime\x88\x01\x01\x12\x15\n\x03ttl\x18\x05 \x01(\tH\x03R\x03ttl\x88\x01\x01\x12(\n\x04\x64\x61ta\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyR\x04\x64\x61ta\x12\x1c\n\toverwrite\x18\x07 \x01(\x08R\toverwrite\x12R\n\x0e\x66\x61ilure_policy\x18\x08 \x01(\x0b\x32&.dapr.proto.common.v1.JobFailurePolicyH\x04R\rfailurePolicy\x88\x01\x01\x42\x0b\n\t_scheduleB\n\n\x08_repeatsB\x0b\n\t_due_timeB\x06\n\x04_ttlB\x11\n\x0f_failure_policy\"=\n\x12ScheduleJobRequest\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\"\x15\n\x13ScheduleJobResponse\"\x1d\n\rGetJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x0eGetJobResponse\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\" \n\x10\x44\x65leteJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteJobResponse\"\xeb\x03\n\x13\x43onversationRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\tcontextID\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x38\n\x06inputs\x18\x03 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationInput\x12N\n\nparameters\x18\x04 \x03(\x0b\x32:.dapr.proto.runtime.v1.ConversationRequest.ParametersEntry\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.ConversationRequest.MetadataEntry\x12\x15\n\x08scrubPII\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01:\x02\x18\x01\x42\x0c\n\n_contextIDB\x0b\n\t_scrubPIIB\x0e\n\x0c_temperature\"\xe6\x04\n\x19\x43onversationRequestAlpha2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\ncontext_id\x18\x02 \x01(\tH\x00\x88\x01\x01\x12>\n\x06inputs\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.ConversationInputAlpha2\x12T\n\nparameters\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.ConversationRequestAlpha2.ParametersEntry\x12P\n\x08metadata\x18\x05 \x03(\x0b\x32>.dapr.proto.runtime.v1.ConversationRequestAlpha2.MetadataEntry\x12\x16\n\tscrub_pii\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x12\x37\n\x05tools\x18\x08 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationTools\x12\x18\n\x0btool_choice\x18\t \x01(\tH\x03\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_context_idB\x0c\n\n_scrub_piiB\x0e\n\x0c_temperatureB\x0e\n\x0c_tool_choice\"h\n\x11\x43onversationInput\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x11\n\x04role\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08scrubPII\x18\x03 \x01(\x08H\x01\x88\x01\x01:\x02\x18\x01\x42\x07\n\x05_roleB\x0b\n\t_scrubPII\"}\n\x17\x43onversationInputAlpha2\x12<\n\x08messages\x18\x01 \x03(\x0b\x32*.dapr.proto.runtime.v1.ConversationMessage\x12\x16\n\tscrub_pii\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\x0c\n\n_scrub_pii\"\x97\x03\n\x13\x43onversationMessage\x12M\n\x0cof_developer\x18\x01 \x01(\x0b\x32\x35.dapr.proto.runtime.v1.ConversationMessageOfDeveloperH\x00\x12G\n\tof_system\x18\x02 \x01(\x0b\x32\x32.dapr.proto.runtime.v1.ConversationMessageOfSystemH\x00\x12\x43\n\x07of_user\x18\x03 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationMessageOfUserH\x00\x12M\n\x0cof_assistant\x18\x04 \x01(\x0b\x32\x35.dapr.proto.runtime.v1.ConversationMessageOfAssistantH\x00\x12\x43\n\x07of_tool\x18\x05 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationMessageOfToolH\x00\x42\x0f\n\rmessage_types\"\x80\x01\n\x1e\x43onversationMessageOfDeveloper\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"}\n\x1b\x43onversationMessageOfSystem\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"{\n\x19\x43onversationMessageOfUser\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"\xc2\x01\n\x1e\x43onversationMessageOfAssistant\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContent\x12@\n\ntool_calls\x18\x03 \x03(\x0b\x32,.dapr.proto.runtime.v1.ConversationToolCallsB\x07\n\x05_name\"\x8f\x01\n\x19\x43onversationMessageOfTool\x12\x14\n\x07tool_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x42\n\x07\x63ontent\x18\x03 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\n\n\x08_tool_id\"\x89\x01\n\x15\x43onversationToolCalls\x12\x0f\n\x02id\x18\x01 \x01(\tH\x01\x88\x01\x01\x12J\n\x08\x66unction\x18\x02 \x01(\x0b\x32\x36.dapr.proto.runtime.v1.ConversationToolCallsOfFunctionH\x00\x42\x0c\n\ntool_typesB\x05\n\x03_id\"B\n\x1f\x43onversationToolCallsOfFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\targuments\x18\x02 \x01(\t\"*\n\x1a\x43onversationMessageContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"\xc0\x01\n\x12\x43onversationResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12M\n\nparameters\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.ConversationResult.ParametersEntry\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01:\x02\x18\x01\"]\n\x18\x43onversationResultAlpha2\x12\x41\n\x07\x63hoices\x18\x01 \x03(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationResultChoices\"\x84\x01\n\x19\x43onversationResultChoices\x12\x15\n\rfinish_reason\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\x03\x12\x41\n\x07message\x18\x03 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationResultMessage\"n\n\x19\x43onversationResultMessage\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12@\n\ntool_calls\x18\x02 \x03(\x0b\x32,.dapr.proto.runtime.v1.ConversationToolCalls\"|\n\x14\x43onversationResponse\x12\x16\n\tcontextID\x18\x01 \x01(\tH\x00\x88\x01\x01\x12:\n\x07outputs\x18\x02 \x03(\x0b\x32).dapr.proto.runtime.v1.ConversationResult:\x02\x18\x01\x42\x0c\n\n_contextID\"\x86\x01\n\x1a\x43onversationResponseAlpha2\x12\x17\n\ncontext_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12@\n\x07outputs\x18\x02 \x03(\x0b\x32/.dapr.proto.runtime.v1.ConversationResultAlpha2B\r\n\x0b_context_id\"g\n\x11\x43onversationTools\x12\x44\n\x08\x66unction\x18\x01 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationToolsFunctionH\x00\x42\x0c\n\ntool_types\"\xf2\x01\n\x19\x43onversationToolsFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x12T\n\nparameters\x18\x03 \x03(\x0b\x32@.dapr.proto.runtime.v1.ConversationToolsFunction.ParametersEntry\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x42\x0e\n\x0c_description*W\n\x16PubsubSubscriptionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x44\x45\x43LARATIVE\x10\x01\x12\x10\n\x0cPROGRAMMATIC\x10\x02\x12\r\n\tSTREAMING\x10\x03\x32\xb7\x32\n\x04\x44\x61pr\x12\x64\n\rInvokeService\x12+.dapr.proto.runtime.v1.InvokeServiceRequest\x1a$.dapr.proto.common.v1.InvokeResponse\"\x00\x12]\n\x08GetState\x12&.dapr.proto.runtime.v1.GetStateRequest\x1a\'.dapr.proto.runtime.v1.GetStateResponse\"\x00\x12i\n\x0cGetBulkState\x12*.dapr.proto.runtime.v1.GetBulkStateRequest\x1a+.dapr.proto.runtime.v1.GetBulkStateResponse\"\x00\x12N\n\tSaveState\x12\'.dapr.proto.runtime.v1.SaveStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12i\n\x10QueryStateAlpha1\x12(.dapr.proto.runtime.v1.QueryStateRequest\x1a).dapr.proto.runtime.v1.QueryStateResponse\"\x00\x12R\n\x0b\x44\x65leteState\x12).dapr.proto.runtime.v1.DeleteStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x0f\x44\x65leteBulkState\x12-.dapr.proto.runtime.v1.DeleteBulkStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17\x45xecuteStateTransaction\x12\x35.dapr.proto.runtime.v1.ExecuteStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n\x0cPublishEvent\x12*.dapr.proto.runtime.v1.PublishEventRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x16\x42ulkPublishEventAlpha1\x12).dapr.proto.runtime.v1.BulkPublishRequest\x1a*.dapr.proto.runtime.v1.BulkPublishResponse\"\x00\x12\x97\x01\n\x1aSubscribeTopicEventsAlpha1\x12\x38.dapr.proto.runtime.v1.SubscribeTopicEventsRequestAlpha1\x1a\x39.dapr.proto.runtime.v1.SubscribeTopicEventsResponseAlpha1\"\x00(\x01\x30\x01\x12l\n\rInvokeBinding\x12+.dapr.proto.runtime.v1.InvokeBindingRequest\x1a,.dapr.proto.runtime.v1.InvokeBindingResponse\"\x00\x12`\n\tGetSecret\x12\'.dapr.proto.runtime.v1.GetSecretRequest\x1a(.dapr.proto.runtime.v1.GetSecretResponse\"\x00\x12l\n\rGetBulkSecret\x12+.dapr.proto.runtime.v1.GetBulkSecretRequest\x1a,.dapr.proto.runtime.v1.GetBulkSecretResponse\"\x00\x12`\n\x12RegisterActorTimer\x12\x30.dapr.proto.runtime.v1.RegisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x14UnregisterActorTimer\x12\x32.dapr.proto.runtime.v1.UnregisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x15RegisterActorReminder\x12\x33.dapr.proto.runtime.v1.RegisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17UnregisterActorReminder\x12\x35.dapr.proto.runtime.v1.UnregisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\rGetActorState\x12+.dapr.proto.runtime.v1.GetActorStateRequest\x1a,.dapr.proto.runtime.v1.GetActorStateResponse\"\x00\x12t\n\x1c\x45xecuteActorStateTransaction\x12:.dapr.proto.runtime.v1.ExecuteActorStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bInvokeActor\x12).dapr.proto.runtime.v1.InvokeActorRequest\x1a*.dapr.proto.runtime.v1.InvokeActorResponse\"\x00\x12{\n\x16GetConfigurationAlpha1\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12u\n\x10GetConfiguration\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12\x8f\x01\n\x1cSubscribeConfigurationAlpha1\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x89\x01\n\x16SubscribeConfiguration\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x93\x01\n\x1eUnsubscribeConfigurationAlpha1\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12\x8d\x01\n\x18UnsubscribeConfiguration\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12`\n\rTryLockAlpha1\x12%.dapr.proto.runtime.v1.TryLockRequest\x1a&.dapr.proto.runtime.v1.TryLockResponse\"\x00\x12]\n\x0cUnlockAlpha1\x12$.dapr.proto.runtime.v1.UnlockRequest\x1a%.dapr.proto.runtime.v1.UnlockResponse\"\x00\x12\x62\n\rEncryptAlpha1\x12%.dapr.proto.runtime.v1.EncryptRequest\x1a&.dapr.proto.runtime.v1.EncryptResponse(\x01\x30\x01\x12\x62\n\rDecryptAlpha1\x12%.dapr.proto.runtime.v1.DecryptRequest\x1a&.dapr.proto.runtime.v1.DecryptResponse(\x01\x30\x01\x12\x66\n\x0bGetMetadata\x12).dapr.proto.runtime.v1.GetMetadataRequest\x1a*.dapr.proto.runtime.v1.GetMetadataResponse\"\x00\x12R\n\x0bSetMetadata\x12).dapr.proto.runtime.v1.SetMetadataRequest\x1a\x16.google.protobuf.Empty\"\x00\x12m\n\x12SubtleGetKeyAlpha1\x12*.dapr.proto.runtime.v1.SubtleGetKeyRequest\x1a+.dapr.proto.runtime.v1.SubtleGetKeyResponse\x12p\n\x13SubtleEncryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleEncryptRequest\x1a,.dapr.proto.runtime.v1.SubtleEncryptResponse\x12p\n\x13SubtleDecryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleDecryptRequest\x1a,.dapr.proto.runtime.v1.SubtleDecryptResponse\x12p\n\x13SubtleWrapKeyAlpha1\x12+.dapr.proto.runtime.v1.SubtleWrapKeyRequest\x1a,.dapr.proto.runtime.v1.SubtleWrapKeyResponse\x12v\n\x15SubtleUnwrapKeyAlpha1\x12-.dapr.proto.runtime.v1.SubtleUnwrapKeyRequest\x1a..dapr.proto.runtime.v1.SubtleUnwrapKeyResponse\x12g\n\x10SubtleSignAlpha1\x12(.dapr.proto.runtime.v1.SubtleSignRequest\x1a).dapr.proto.runtime.v1.SubtleSignResponse\x12m\n\x12SubtleVerifyAlpha1\x12*.dapr.proto.runtime.v1.SubtleVerifyRequest\x1a+.dapr.proto.runtime.v1.SubtleVerifyResponse\x12u\n\x13StartWorkflowAlpha1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x03\x88\x02\x01\x12o\n\x11GetWorkflowAlpha1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x03\x88\x02\x01\x12_\n\x13PurgeWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12g\n\x17TerminateWorkflowAlpha1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12_\n\x13PauseWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12\x61\n\x14ResumeWorkflowAlpha1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12i\n\x18RaiseEventWorkflowAlpha1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12q\n\x12StartWorkflowBeta1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12k\n\x10GetWorkflowBeta1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12[\n\x12PurgeWorkflowBeta1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x63\n\x16TerminateWorkflowBeta1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x12PauseWorkflowBeta1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x13ResumeWorkflowBeta1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x17RaiseEventWorkflowBeta1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12L\n\x08Shutdown\x12&.dapr.proto.runtime.v1.ShutdownRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\x11ScheduleJobAlpha1\x12).dapr.proto.runtime.v1.ScheduleJobRequest\x1a*.dapr.proto.runtime.v1.ScheduleJobResponse\"\x00\x12]\n\x0cGetJobAlpha1\x12$.dapr.proto.runtime.v1.GetJobRequest\x1a%.dapr.proto.runtime.v1.GetJobResponse\"\x00\x12\x66\n\x0f\x44\x65leteJobAlpha1\x12\'.dapr.proto.runtime.v1.DeleteJobRequest\x1a(.dapr.proto.runtime.v1.DeleteJobResponse\"\x00\x12k\n\x0e\x43onverseAlpha1\x12*.dapr.proto.runtime.v1.ConversationRequest\x1a+.dapr.proto.runtime.v1.ConversationResponse\"\x00\x12w\n\x0e\x43onverseAlpha2\x12\x30.dapr.proto.runtime.v1.ConversationRequestAlpha2\x1a\x31.dapr.proto.runtime.v1.ConversationResponseAlpha2\"\x00\x42i\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\xaa\x02\x1b\x44\x61pr.Client.Autogen.Grpc.v1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dapr.proto.runtime.v1.dapr_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - _globals['DESCRIPTOR']._options = None +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\252\002\033Dapr.Client.Autogen.Grpc.v1' - _globals['_GETSTATEREQUEST_METADATAENTRY']._options = None + _globals['_GETSTATEREQUEST_METADATAENTRY']._loaded_options = None _globals['_GETSTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._options = None + _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._loaded_options = None _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BULKSTATEITEM_METADATAENTRY']._options = None + _globals['_BULKSTATEITEM_METADATAENTRY']._loaded_options = None _globals['_BULKSTATEITEM_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETSTATERESPONSE_METADATAENTRY']._options = None + _globals['_GETSTATERESPONSE_METADATAENTRY']._loaded_options = None _globals['_GETSTATERESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_DELETESTATEREQUEST_METADATAENTRY']._options = None + _globals['_DELETESTATEREQUEST_METADATAENTRY']._loaded_options = None _globals['_DELETESTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_QUERYSTATEREQUEST_METADATAENTRY']._options = None + _globals['_QUERYSTATEREQUEST_METADATAENTRY']._loaded_options = None _globals['_QUERYSTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_QUERYSTATERESPONSE_METADATAENTRY']._options = None + _globals['_QUERYSTATERESPONSE_METADATAENTRY']._loaded_options = None _globals['_QUERYSTATERESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._options = None + _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._loaded_options = None _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._options = None + _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._loaded_options = None _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._options = None + _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._loaded_options = None _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._options = None + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._loaded_options = None _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_options = b'8\001' - _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._options = None + _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._loaded_options = None _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._options = None + _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._loaded_options = None _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETSECRETREQUEST_METADATAENTRY']._options = None + _globals['_GETSECRETREQUEST_METADATAENTRY']._loaded_options = None _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETSECRETRESPONSE_DATAENTRY']._options = None + _globals['_GETSECRETRESPONSE_DATAENTRY']._loaded_options = None _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_options = b'8\001' - _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._options = None + _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._loaded_options = None _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SECRETRESPONSE_SECRETSENTRY']._options = None + _globals['_SECRETRESPONSE_SECRETSENTRY']._loaded_options = None _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_options = b'8\001' - _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._options = None + _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._loaded_options = None _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_options = b'8\001' - _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._options = None + _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._loaded_options = None _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._options = None + _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._loaded_options = None _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._options = None + _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._loaded_options = None _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_options = b'8\001' - _globals['_INVOKEACTORREQUEST_METADATAENTRY']._options = None + _globals['_INVOKEACTORREQUEST_METADATAENTRY']._loaded_options = None _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._options = None + _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._loaded_options = None _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_options = b'8\001' - _globals['_GETMETADATARESPONSE'].fields_by_name['active_actors_count']._options = None + _globals['_GETMETADATARESPONSE'].fields_by_name['active_actors_count']._loaded_options = None _globals['_GETMETADATARESPONSE'].fields_by_name['active_actors_count']._serialized_options = b'\030\001' - _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._options = None + _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._loaded_options = None _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._options = None + _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._loaded_options = None _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._options = None + _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._loaded_options = None _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._options = None + _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._loaded_options = None _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._options = None + _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._loaded_options = None _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_options = b'8\001' - _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._options = None + _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._loaded_options = None _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_options = b'8\001' - _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._options = None + _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._loaded_options = None _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._options = None + _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._loaded_options = None _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONREQUEST_METADATAENTRY']._options = None + _globals['_CONVERSATIONREQUEST_METADATAENTRY']._loaded_options = None _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._options = None + _globals['_CONVERSATIONREQUEST']._loaded_options = None + _globals['_CONVERSATIONREQUEST']._serialized_options = b'\030\001' + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._loaded_options = None + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_options = b'8\001' + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._loaded_options = None + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_options = b'8\001' + _globals['_CONVERSATIONINPUT']._loaded_options = None + _globals['_CONVERSATIONINPUT']._serialized_options = b'\030\001' + _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._loaded_options = None _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_options = b'8\001' - _globals['_DAPR'].methods_by_name['StartWorkflowAlpha1']._options = None + _globals['_CONVERSATIONRESULT']._loaded_options = None + _globals['_CONVERSATIONRESULT']._serialized_options = b'\030\001' + _globals['_CONVERSATIONRESPONSE']._loaded_options = None + _globals['_CONVERSATIONRESPONSE']._serialized_options = b'\030\001' + _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._loaded_options = None + _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._serialized_options = b'8\001' + _globals['_DAPR'].methods_by_name['StartWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['StartWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['GetWorkflowAlpha1']._options = None + _globals['_DAPR'].methods_by_name['GetWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['GetWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['PurgeWorkflowAlpha1']._options = None + _globals['_DAPR'].methods_by_name['PurgeWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['PurgeWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['TerminateWorkflowAlpha1']._options = None + _globals['_DAPR'].methods_by_name['TerminateWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['TerminateWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['PauseWorkflowAlpha1']._options = None + _globals['_DAPR'].methods_by_name['PauseWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['PauseWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['ResumeWorkflowAlpha1']._options = None + _globals['_DAPR'].methods_by_name['ResumeWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['ResumeWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['RaiseEventWorkflowAlpha1']._options = None + _globals['_DAPR'].methods_by_name['RaiseEventWorkflowAlpha1']._loaded_options = None _globals['_DAPR'].methods_by_name['RaiseEventWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_start=16265 - _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_end=16352 + _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_start=19242 + _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_end=19329 _globals['_INVOKESERVICEREQUEST']._serialized_start=224 _globals['_INVOKESERVICEREQUEST']._serialized_end=312 _globals['_GETSTATEREQUEST']._serialized_start=315 @@ -380,19 +404,59 @@ _globals['_DELETEJOBRESPONSE']._serialized_start=15339 _globals['_DELETEJOBRESPONSE']._serialized_end=15358 _globals['_CONVERSATIONREQUEST']._serialized_start=15361 - _globals['_CONVERSATIONREQUEST']._serialized_end=15848 + _globals['_CONVERSATIONREQUEST']._serialized_end=15852 _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_start=15685 _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_end=15756 _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_start=513 _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_CONVERSATIONINPUT']._serialized_start=15850 - _globals['_CONVERSATIONINPUT']._serialized_end=15950 - _globals['_CONVERSATIONRESULT']._serialized_start=15953 - _globals['_CONVERSATIONRESULT']._serialized_end=16141 + _globals['_CONVERSATIONREQUESTALPHA2']._serialized_start=15855 + _globals['_CONVERSATIONREQUESTALPHA2']._serialized_end=16469 + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_start=15685 + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_end=15756 + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_start=513 + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_end=560 + _globals['_CONVERSATIONINPUT']._serialized_start=16471 + _globals['_CONVERSATIONINPUT']._serialized_end=16575 + _globals['_CONVERSATIONINPUTALPHA2']._serialized_start=16577 + _globals['_CONVERSATIONINPUTALPHA2']._serialized_end=16702 + _globals['_CONVERSATIONMESSAGE']._serialized_start=16705 + _globals['_CONVERSATIONMESSAGE']._serialized_end=17112 + _globals['_CONVERSATIONMESSAGEOFDEVELOPER']._serialized_start=17115 + _globals['_CONVERSATIONMESSAGEOFDEVELOPER']._serialized_end=17243 + _globals['_CONVERSATIONMESSAGEOFSYSTEM']._serialized_start=17245 + _globals['_CONVERSATIONMESSAGEOFSYSTEM']._serialized_end=17370 + _globals['_CONVERSATIONMESSAGEOFUSER']._serialized_start=17372 + _globals['_CONVERSATIONMESSAGEOFUSER']._serialized_end=17495 + _globals['_CONVERSATIONMESSAGEOFASSISTANT']._serialized_start=17498 + _globals['_CONVERSATIONMESSAGEOFASSISTANT']._serialized_end=17692 + _globals['_CONVERSATIONMESSAGEOFTOOL']._serialized_start=17695 + _globals['_CONVERSATIONMESSAGEOFTOOL']._serialized_end=17838 + _globals['_CONVERSATIONTOOLCALLS']._serialized_start=17841 + _globals['_CONVERSATIONTOOLCALLS']._serialized_end=17978 + _globals['_CONVERSATIONTOOLCALLSOFFUNCTION']._serialized_start=17980 + _globals['_CONVERSATIONTOOLCALLSOFFUNCTION']._serialized_end=18046 + _globals['_CONVERSATIONMESSAGECONTENT']._serialized_start=18048 + _globals['_CONVERSATIONMESSAGECONTENT']._serialized_end=18090 + _globals['_CONVERSATIONRESULT']._serialized_start=18093 + _globals['_CONVERSATIONRESULT']._serialized_end=18285 _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_start=15685 _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_end=15756 - _globals['_CONVERSATIONRESPONSE']._serialized_start=16143 - _globals['_CONVERSATIONRESPONSE']._serialized_end=16263 - _globals['_DAPR']._serialized_start=16355 - _globals['_DAPR']._serialized_end=22689 + _globals['_CONVERSATIONRESULTALPHA2']._serialized_start=18287 + _globals['_CONVERSATIONRESULTALPHA2']._serialized_end=18380 + _globals['_CONVERSATIONRESULTCHOICES']._serialized_start=18383 + _globals['_CONVERSATIONRESULTCHOICES']._serialized_end=18515 + _globals['_CONVERSATIONRESULTMESSAGE']._serialized_start=18517 + _globals['_CONVERSATIONRESULTMESSAGE']._serialized_end=18627 + _globals['_CONVERSATIONRESPONSE']._serialized_start=18629 + _globals['_CONVERSATIONRESPONSE']._serialized_end=18753 + _globals['_CONVERSATIONRESPONSEALPHA2']._serialized_start=18756 + _globals['_CONVERSATIONRESPONSEALPHA2']._serialized_end=18890 + _globals['_CONVERSATIONTOOLS']._serialized_start=18892 + _globals['_CONVERSATIONTOOLS']._serialized_end=18995 + _globals['_CONVERSATIONTOOLSFUNCTION']._serialized_start=18998 + _globals['_CONVERSATIONTOOLSFUNCTION']._serialized_end=19240 + _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._serialized_start=15685 + _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._serialized_end=15756 + _globals['_DAPR']._serialized_start=19332 + _globals['_DAPR']._serialized_end=25787 # @@protoc_insertion_point(module_scope) diff --git a/dapr/proto/runtime/v1/dapr_pb2.pyi b/dapr/proto/runtime/v1/dapr_pb2.pyi index 9099aac9..ed0c2745 100644 --- a/dapr/proto/runtime/v1/dapr_pb2.pyi +++ b/dapr/proto/runtime/v1/dapr_pb2.pyi @@ -3441,8 +3441,129 @@ class ConversationRequest(google.protobuf.message.Message): global___ConversationRequest = ConversationRequest +@typing.final +class ConversationRequestAlpha2(google.protobuf.message.Message): + """ConversationRequestAlpha2 is the new request object for Conversation. + Many of these fields are inspired by openai.ChatCompletionNewParams + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L2106 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class ParametersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.any_pb2.Any: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.any_pb2.Any | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + @typing.final + class MetadataEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + CONTEXT_ID_FIELD_NUMBER: builtins.int + INPUTS_FIELD_NUMBER: builtins.int + PARAMETERS_FIELD_NUMBER: builtins.int + METADATA_FIELD_NUMBER: builtins.int + SCRUB_PII_FIELD_NUMBER: builtins.int + TEMPERATURE_FIELD_NUMBER: builtins.int + TOOLS_FIELD_NUMBER: builtins.int + TOOL_CHOICE_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of Conversation component""" + context_id: builtins.str + """The ID of an existing chat (like in ChatGPT)""" + scrub_pii: builtins.bool + """Scrub PII data that comes back from the LLM""" + temperature: builtins.float + """Temperature for the LLM to optimize for creativity or predictability""" + tool_choice: builtins.str + """Controls which (if any) tool is called by the model. + `none` means the model will not call any tool and instead generates a message. + `auto` means the model can pick between generating a message or calling one or more tools. + Alternatively, a specific tool name may be used here, and casing/syntax must match on tool name. + `none` is the default when no tools are present. + `auto` is the default if tools are present. + `required` requires one or more functions to be called. + ref: https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1976 + ref: https://python.langchain.com/docs/how_to/tool_choice/ + """ + @property + def inputs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationInputAlpha2]: + """Inputs for the conversation, support multiple input in one time. + This is the revamped conversation inputs better matching openai. + """ + + @property + def parameters(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.any_pb2.Any]: + """Parameters for all custom fields.""" + + @property + def metadata(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """The metadata passing to conversation components.""" + + @property + def tools(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationTools]: + """Tools register the tools available to be used by the LLM during the conversation. + These are sent on a per request basis. + The tools available during the first round of the conversation + may be different than tools specified later on. + """ + + def __init__( + self, + *, + name: builtins.str = ..., + context_id: builtins.str | None = ..., + inputs: collections.abc.Iterable[global___ConversationInputAlpha2] | None = ..., + parameters: collections.abc.Mapping[builtins.str, google.protobuf.any_pb2.Any] | None = ..., + metadata: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + scrub_pii: builtins.bool | None = ..., + temperature: builtins.float | None = ..., + tools: collections.abc.Iterable[global___ConversationTools] | None = ..., + tool_choice: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_context_id", b"_context_id", "_scrub_pii", b"_scrub_pii", "_temperature", b"_temperature", "_tool_choice", b"_tool_choice", "context_id", b"context_id", "scrub_pii", b"scrub_pii", "temperature", b"temperature", "tool_choice", b"tool_choice"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_context_id", b"_context_id", "_scrub_pii", b"_scrub_pii", "_temperature", b"_temperature", "_tool_choice", b"_tool_choice", "context_id", b"context_id", "inputs", b"inputs", "metadata", b"metadata", "name", b"name", "parameters", b"parameters", "scrub_pii", b"scrub_pii", "temperature", b"temperature", "tool_choice", b"tool_choice", "tools", b"tools"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_context_id", b"_context_id"]) -> typing.Literal["context_id"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_scrub_pii", b"_scrub_pii"]) -> typing.Literal["scrub_pii"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_temperature", b"_temperature"]) -> typing.Literal["temperature"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_tool_choice", b"_tool_choice"]) -> typing.Literal["tool_choice"] | None: ... + +global___ConversationRequestAlpha2 = ConversationRequestAlpha2 + @typing.final class ConversationInput(google.protobuf.message.Message): + """maintained for backwards compatibility""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor CONTENT_FIELD_NUMBER: builtins.int @@ -3470,6 +3591,309 @@ class ConversationInput(google.protobuf.message.Message): global___ConversationInput = ConversationInput +@typing.final +class ConversationInputAlpha2(google.protobuf.message.Message): + """directly inspired by openai.ChatCompletionNewParams + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L2106 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + MESSAGES_FIELD_NUMBER: builtins.int + SCRUB_PII_FIELD_NUMBER: builtins.int + scrub_pii: builtins.bool + """Scrub PII data that goes into the LLM""" + @property + def messages(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationMessage]: + """The content to send to the llm""" + + def __init__( + self, + *, + messages: collections.abc.Iterable[global___ConversationMessage] | None = ..., + scrub_pii: builtins.bool | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_scrub_pii", b"_scrub_pii", "scrub_pii", b"scrub_pii"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_scrub_pii", b"_scrub_pii", "messages", b"messages", "scrub_pii", b"scrub_pii"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_scrub_pii", b"_scrub_pii"]) -> typing.Literal["scrub_pii"] | None: ... + +global___ConversationInputAlpha2 = ConversationInputAlpha2 + +@typing.final +class ConversationMessage(google.protobuf.message.Message): + """inspired by openai.ChatCompletionMessageParamUnion + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1449 + The role field is inherent to the type of ConversationMessage, + and is propagated in the backend according to the underlying LLM provider type. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + OF_DEVELOPER_FIELD_NUMBER: builtins.int + OF_SYSTEM_FIELD_NUMBER: builtins.int + OF_USER_FIELD_NUMBER: builtins.int + OF_ASSISTANT_FIELD_NUMBER: builtins.int + OF_TOOL_FIELD_NUMBER: builtins.int + @property + def of_developer(self) -> global___ConversationMessageOfDeveloper: ... + @property + def of_system(self) -> global___ConversationMessageOfSystem: ... + @property + def of_user(self) -> global___ConversationMessageOfUser: ... + @property + def of_assistant(self) -> global___ConversationMessageOfAssistant: ... + @property + def of_tool(self) -> global___ConversationMessageOfTool: + """Note: there could be a ConversationMessageOfFunction type here too, + but that is deprecated in openai, so we will not support this. + """ + + def __init__( + self, + *, + of_developer: global___ConversationMessageOfDeveloper | None = ..., + of_system: global___ConversationMessageOfSystem | None = ..., + of_user: global___ConversationMessageOfUser | None = ..., + of_assistant: global___ConversationMessageOfAssistant | None = ..., + of_tool: global___ConversationMessageOfTool | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["message_types", b"message_types", "of_assistant", b"of_assistant", "of_developer", b"of_developer", "of_system", b"of_system", "of_tool", b"of_tool", "of_user", b"of_user"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["message_types", b"message_types", "of_assistant", b"of_assistant", "of_developer", b"of_developer", "of_system", b"of_system", "of_tool", b"of_tool", "of_user", b"of_user"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["message_types", b"message_types"]) -> typing.Literal["of_developer", "of_system", "of_user", "of_assistant", "of_tool"] | None: ... + +global___ConversationMessage = ConversationMessage + +@typing.final +class ConversationMessageOfDeveloper(google.protobuf.message.Message): + """inspired by openai.ChatCompletionDeveloperMessageParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1130 + ConversationMessageOfDeveloper is intended to be the contents of a conversation message, + as the role of a developer. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + CONTENT_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of the participant in the message.""" + @property + def content(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationMessageContent]: ... + def __init__( + self, + *, + name: builtins.str | None = ..., + content: collections.abc.Iterable[global___ConversationMessageContent] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_name", b"_name", "name", b"name"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_name", b"_name", "content", b"content", "name", b"name"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_name", b"_name"]) -> typing.Literal["name"] | None: ... + +global___ConversationMessageOfDeveloper = ConversationMessageOfDeveloper + +@typing.final +class ConversationMessageOfSystem(google.protobuf.message.Message): + """inspired by openai.ChatCompletionSystemMessageParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1842 + ConversationMessageOfSystem is intended to be the contents of a conversation message, + as the role of a system. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + CONTENT_FIELD_NUMBER: builtins.int + name: builtins.str + @property + def content(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationMessageContent]: ... + def __init__( + self, + *, + name: builtins.str | None = ..., + content: collections.abc.Iterable[global___ConversationMessageContent] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_name", b"_name", "name", b"name"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_name", b"_name", "content", b"content", "name", b"name"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_name", b"_name"]) -> typing.Literal["name"] | None: ... + +global___ConversationMessageOfSystem = ConversationMessageOfSystem + +@typing.final +class ConversationMessageOfUser(google.protobuf.message.Message): + """inspired by openai.ChatCompletionUserMessageParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L2060C6-L2060C36 + ConversationMessageOfUser is intended to be the contents of a conversation message, + as the role of an end user. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + CONTENT_FIELD_NUMBER: builtins.int + name: builtins.str + @property + def content(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationMessageContent]: ... + def __init__( + self, + *, + name: builtins.str | None = ..., + content: collections.abc.Iterable[global___ConversationMessageContent] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_name", b"_name", "name", b"name"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_name", b"_name", "content", b"content", "name", b"name"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_name", b"_name"]) -> typing.Literal["name"] | None: ... + +global___ConversationMessageOfUser = ConversationMessageOfUser + +@typing.final +class ConversationMessageOfAssistant(google.protobuf.message.Message): + """inspired by openai.ChatCompletionAssistantMessageParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L310 + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L2060C6-L2060C36 + ConversationMessageOfAssistant is intended to be the contents of a conversation message, + as the role of an assistant. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + CONTENT_FIELD_NUMBER: builtins.int + TOOL_CALLS_FIELD_NUMBER: builtins.int + name: builtins.str + @property + def content(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationMessageContent]: + """TODO: there is an audio field here to bring in when the time comes 1.17 or later.""" + + @property + def tool_calls(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationToolCalls]: + """Tool calls generated by the model, such as function calls for the client to then make.""" + + def __init__( + self, + *, + name: builtins.str | None = ..., + content: collections.abc.Iterable[global___ConversationMessageContent] | None = ..., + tool_calls: collections.abc.Iterable[global___ConversationToolCalls] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_name", b"_name", "name", b"name"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_name", b"_name", "content", b"content", "name", b"name", "tool_calls", b"tool_calls"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_name", b"_name"]) -> typing.Literal["name"] | None: ... + +global___ConversationMessageOfAssistant = ConversationMessageOfAssistant + +@typing.final +class ConversationMessageOfTool(google.protobuf.message.Message): + """inspired by openai.ChatCompletionToolMessageParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L2011 + ConversationMessageOfTool is intended to be the contents of a conversation message, + as the role of a tool. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TOOL_ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + CONTENT_FIELD_NUMBER: builtins.int + tool_id: builtins.str + """Tool ID is helpful for tracking tool history""" + name: builtins.str + """Name of tool associated with the message""" + @property + def content(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationMessageContent]: ... + def __init__( + self, + *, + tool_id: builtins.str | None = ..., + name: builtins.str = ..., + content: collections.abc.Iterable[global___ConversationMessageContent] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_tool_id", b"_tool_id", "tool_id", b"tool_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_tool_id", b"_tool_id", "content", b"content", "name", b"name", "tool_id", b"tool_id"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_tool_id", b"_tool_id"]) -> typing.Literal["tool_id"] | None: ... + +global___ConversationMessageOfTool = ConversationMessageOfTool + +@typing.final +class ConversationToolCalls(google.protobuf.message.Message): + """inspired by openai.ChatCompletionMessageToolCallParam and openai.ChatCompletionMessageToolCall + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1669 + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1611 + ConversationToolCalls is the tool call request sent from the llm to the client to then call to execute. + This assumes that in our api if a client makes a request that would get a tool call response from the llm, + that this client can also have the tool handy itself to execute it. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + FUNCTION_FIELD_NUMBER: builtins.int + id: builtins.str + @property + def function(self) -> global___ConversationToolCallsOfFunction: ... + def __init__( + self, + *, + id: builtins.str | None = ..., + function: global___ConversationToolCallsOfFunction | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_id", b"_id", "function", b"function", "id", b"id", "tool_types", b"tool_types"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_id", b"_id", "function", b"function", "id", b"id", "tool_types", b"tool_types"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_id", b"_id"]) -> typing.Literal["id"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["tool_types", b"tool_types"]) -> typing.Literal["function"] | None: ... + +global___ConversationToolCalls = ConversationToolCalls + +@typing.final +class ConversationToolCallsOfFunction(google.protobuf.message.Message): + """inspired by openai.ChatCompletionMessageToolCallFunctionParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1692 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + ARGUMENTS_FIELD_NUMBER: builtins.int + name: builtins.str + arguments: builtins.str + """The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + def __init__( + self, + *, + name: builtins.str = ..., + arguments: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["arguments", b"arguments", "name", b"name"]) -> None: ... + +global___ConversationToolCallsOfFunction = ConversationToolCallsOfFunction + +@typing.final +class ConversationMessageContent(google.protobuf.message.Message): + """inspired by openai.ChatCompletionContentPartTextParam & openai.ChatCompletionDeveloperMessageParamContentUnion + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1084 + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1154C6-L1154C53 + Note: openai has this message be either a message of string or message of array type, + so instead of this, we support that in one message type instead. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TEXT_FIELD_NUMBER: builtins.int + text: builtins.str + def __init__( + self, + *, + text: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["text", b"text"]) -> None: ... + +global___ConversationMessageContent = ConversationMessageContent + @typing.final class ConversationResult(google.protobuf.message.Message): """ConversationResult is the result for one input.""" @@ -3512,6 +3936,89 @@ class ConversationResult(google.protobuf.message.Message): global___ConversationResult = ConversationResult +@typing.final +class ConversationResultAlpha2(google.protobuf.message.Message): + """inspired by openai.ChatCompletion + ConversationResultAlpha2 is the result for one input. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CHOICES_FIELD_NUMBER: builtins.int + @property + def choices(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationResultChoices]: + """Result for the conversation input.""" + + def __init__( + self, + *, + choices: collections.abc.Iterable[global___ConversationResultChoices] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["choices", b"choices"]) -> None: ... + +global___ConversationResultAlpha2 = ConversationResultAlpha2 + +@typing.final +class ConversationResultChoices(google.protobuf.message.Message): + """inspired by openai.ChatCompletionChoice + based on https://github.com/openai/openai-go/blob/main/chatcompletion.go#L226 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FINISH_REASON_FIELD_NUMBER: builtins.int + INDEX_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + finish_reason: builtins.str + """The reason the model stopped generating tokens. This will be `stop` if the model + hit a natural stop point or a provided stop sequence, `length` if the maximum + number of tokens specified in the request was reached, `content_filter` if + content was omitted due to a flag from our content filters, `tool_calls` if the + model called a tool. + Any of "stop", "length", "tool_calls", "content_filter". + """ + index: builtins.int + """The index of the choice in the list of choices.""" + @property + def message(self) -> global___ConversationResultMessage: ... + def __init__( + self, + *, + finish_reason: builtins.str = ..., + index: builtins.int = ..., + message: global___ConversationResultMessage | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["message", b"message"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["finish_reason", b"finish_reason", "index", b"index", "message", b"message"]) -> None: ... + +global___ConversationResultChoices = ConversationResultChoices + +@typing.final +class ConversationResultMessage(google.protobuf.message.Message): + """inspired by openai.ChatCompletionMessage + based on https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1218C6-L1218C27 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONTENT_FIELD_NUMBER: builtins.int + TOOL_CALLS_FIELD_NUMBER: builtins.int + content: builtins.str + """The contents of the message.""" + @property + def tool_calls(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationToolCalls]: + """The tool calls generated by the model.""" + + def __init__( + self, + *, + content: builtins.str = ..., + tool_calls: collections.abc.Iterable[global___ConversationToolCalls] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["content", b"content", "tool_calls", b"tool_calls"]) -> None: ... + +global___ConversationResultMessage = ConversationResultMessage + @typing.final class ConversationResponse(google.protobuf.message.Message): """ConversationResponse is the response for Conversation.""" @@ -3537,3 +4044,109 @@ class ConversationResponse(google.protobuf.message.Message): def WhichOneof(self, oneof_group: typing.Literal["_contextID", b"_contextID"]) -> typing.Literal["contextID"] | None: ... global___ConversationResponse = ConversationResponse + +@typing.final +class ConversationResponseAlpha2(google.protobuf.message.Message): + """ConversationResponseAlpha2 is the Alpha2 response for Conversation.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONTEXT_ID_FIELD_NUMBER: builtins.int + OUTPUTS_FIELD_NUMBER: builtins.int + context_id: builtins.str + """The ID of an existing chat (like in ChatGPT)""" + @property + def outputs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConversationResultAlpha2]: + """An array of results.""" + + def __init__( + self, + *, + context_id: builtins.str | None = ..., + outputs: collections.abc.Iterable[global___ConversationResultAlpha2] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_context_id", b"_context_id", "context_id", b"context_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_context_id", b"_context_id", "context_id", b"context_id", "outputs", b"outputs"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_context_id", b"_context_id"]) -> typing.Literal["context_id"] | None: ... + +global___ConversationResponseAlpha2 = ConversationResponseAlpha2 + +@typing.final +class ConversationTools(google.protobuf.message.Message): + """ConversationTools are the typed tools available to be called. + inspired by openai.ChatCompletionToolParam + https://github.com/openai/openai-go/blob/main/chatcompletion.go#L1950 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FUNCTION_FIELD_NUMBER: builtins.int + @property + def function(self) -> global___ConversationToolsFunction: ... + def __init__( + self, + *, + function: global___ConversationToolsFunction | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["function", b"function", "tool_types", b"tool_types"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["function", b"function", "tool_types", b"tool_types"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["tool_types", b"tool_types"]) -> typing.Literal["function"] | None: ... + +global___ConversationTools = ConversationTools + +@typing.final +class ConversationToolsFunction(google.protobuf.message.Message): + """ConversationToolsFunction is the main tool type to be used in a conversation. + inspired by openai.FunctionDefinitionParam + https://pkg.go.dev/github.com/openai/openai-go/shared#FunctionDefinitionParam + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class ParametersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> google.protobuf.any_pb2.Any: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: google.protobuf.any_pb2.Any | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + PARAMETERS_FIELD_NUMBER: builtins.int + name: builtins.str + """The name of the function to be called.""" + description: builtins.str + """A description of what the function does, + used by the model to choose when and how to call the function. + """ + @property + def parameters(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.any_pb2.Any]: + """The parameters the functions accepts, described as a JSON Schema object. + See the [guide](https://platform.openai.com/docs/guides/function-calling) for examples, + and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. + Omitting `parameters` defines a function with an empty parameter list. + """ + + def __init__( + self, + *, + name: builtins.str = ..., + description: builtins.str | None = ..., + parameters: collections.abc.Mapping[builtins.str, google.protobuf.any_pb2.Any] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "name", b"name", "parameters", b"parameters"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... + +global___ConversationToolsFunction = ConversationToolsFunction diff --git a/dapr/proto/runtime/v1/dapr_pb2_grpc.py b/dapr/proto/runtime/v1/dapr_pb2_grpc.py index d371896b..bd32a083 100644 --- a/dapr/proto/runtime/v1/dapr_pb2_grpc.py +++ b/dapr/proto/runtime/v1/dapr_pb2_grpc.py @@ -1,11 +1,31 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from dapr.proto.common.v1 import common_pb2 as dapr_dot_proto_dot_common_dot_v1_dot_common__pb2 from dapr.proto.runtime.v1 import dapr_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +GRPC_GENERATED_VERSION = '1.74.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in dapr/proto/runtime/v1/dapr_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + class DaprStub(object): """Dapr service provides APIs to user application to access Dapr building blocks. @@ -21,297 +41,302 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.Dapr/InvokeService', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeServiceRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - ) + _registered_method=True) self.GetState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateResponse.FromString, - ) + _registered_method=True) self.GetBulkState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetBulkState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateResponse.FromString, - ) + _registered_method=True) self.SaveState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SaveState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SaveStateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.QueryStateAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/QueryStateAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateResponse.FromString, - ) + _registered_method=True) self.DeleteState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/DeleteState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteStateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.DeleteBulkState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/DeleteBulkState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteBulkStateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.ExecuteStateTransaction = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ExecuteStateTransaction', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteStateTransactionRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.PublishEvent = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PublishEvent', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PublishEventRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.BulkPublishEventAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/BulkPublishEventAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishResponse.FromString, - ) + _registered_method=True) self.SubscribeTopicEventsAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.FromString, - ) + _registered_method=True) self.InvokeBinding = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/InvokeBinding', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingResponse.FromString, - ) + _registered_method=True) self.GetSecret = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetSecret', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretResponse.FromString, - ) + _registered_method=True) self.GetBulkSecret = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetBulkSecret', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretResponse.FromString, - ) + _registered_method=True) self.RegisterActorTimer = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RegisterActorTimer', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorTimerRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.UnregisterActorTimer = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnregisterActorTimer', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorTimerRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.RegisterActorReminder = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RegisterActorReminder', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorReminderRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.UnregisterActorReminder = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnregisterActorReminder', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorReminderRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.GetActorState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetActorState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateResponse.FromString, - ) + _registered_method=True) self.ExecuteActorStateTransaction = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ExecuteActorStateTransaction', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteActorStateTransactionRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.InvokeActor = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/InvokeActor', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorResponse.FromString, - ) + _registered_method=True) self.GetConfigurationAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetConfigurationAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - ) + _registered_method=True) self.GetConfiguration = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetConfiguration', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - ) + _registered_method=True) self.SubscribeConfigurationAlpha1 = channel.unary_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeConfigurationAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - ) + _registered_method=True) self.SubscribeConfiguration = channel.unary_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeConfiguration', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - ) + _registered_method=True) self.UnsubscribeConfigurationAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfigurationAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - ) + _registered_method=True) self.UnsubscribeConfiguration = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfiguration', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - ) + _registered_method=True) self.TryLockAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/TryLockAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockResponse.FromString, - ) + _registered_method=True) self.UnlockAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnlockAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockResponse.FromString, - ) + _registered_method=True) self.EncryptAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/EncryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptResponse.FromString, - ) + _registered_method=True) self.DecryptAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/DecryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptResponse.FromString, - ) + _registered_method=True) self.GetMetadata = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetMetadata', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataResponse.FromString, - ) + _registered_method=True) self.SetMetadata = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SetMetadata', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SetMetadataRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.SubtleGetKeyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleGetKeyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyResponse.FromString, - ) + _registered_method=True) self.SubtleEncryptAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleEncryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptResponse.FromString, - ) + _registered_method=True) self.SubtleDecryptAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleDecryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptResponse.FromString, - ) + _registered_method=True) self.SubtleWrapKeyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleWrapKeyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyResponse.FromString, - ) + _registered_method=True) self.SubtleUnwrapKeyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleUnwrapKeyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyResponse.FromString, - ) + _registered_method=True) self.SubtleSignAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleSignAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignResponse.FromString, - ) + _registered_method=True) self.SubtleVerifyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleVerifyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyResponse.FromString, - ) + _registered_method=True) self.StartWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/StartWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - ) + _registered_method=True) self.GetWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - ) + _registered_method=True) self.PurgeWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.TerminateWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.PauseWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PauseWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.ResumeWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.RaiseEventWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.StartWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/StartWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - ) + _registered_method=True) self.GetWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - ) + _registered_method=True) self.PurgeWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.TerminateWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.PauseWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PauseWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.ResumeWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.RaiseEventWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.Shutdown = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/Shutdown', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ShutdownRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - ) + _registered_method=True) self.ScheduleJobAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ScheduleJobAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobResponse.FromString, - ) + _registered_method=True) self.GetJobAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetJobAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobResponse.FromString, - ) + _registered_method=True) self.DeleteJobAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/DeleteJobAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobResponse.FromString, - ) + _registered_method=True) self.ConverseAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ConverseAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponse.FromString, - ) + _registered_method=True) + self.ConverseAlpha2 = channel.unary_unary( + '/dapr.proto.runtime.v1.Dapr/ConverseAlpha2', + request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequestAlpha2.SerializeToString, + response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponseAlpha2.FromString, + _registered_method=True) class DaprServicer(object): @@ -733,6 +758,13 @@ def ConverseAlpha1(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def ConverseAlpha2(self, request, context): + """Converse with a LLM service via alpha2 api + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_DaprServicer_to_server(servicer, server): rpc_method_handlers = { @@ -1031,10 +1063,16 @@ def add_DaprServicer_to_server(servicer, server): request_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequest.FromString, response_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponse.SerializeToString, ), + 'ConverseAlpha2': grpc.unary_unary_rpc_method_handler( + servicer.ConverseAlpha2, + request_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequestAlpha2.FromString, + response_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponseAlpha2.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.Dapr', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('dapr.proto.runtime.v1.Dapr', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -1053,11 +1091,21 @@ def InvokeService(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/InvokeService', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/InvokeService', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeServiceRequest.SerializeToString, dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetState(request, @@ -1070,11 +1118,21 @@ def GetState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetState', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetBulkState(request, @@ -1087,11 +1145,21 @@ def GetBulkState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetBulkState', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetBulkState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SaveState(request, @@ -1104,11 +1172,21 @@ def SaveState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SaveState', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SaveState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SaveStateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def QueryStateAlpha1(request, @@ -1121,11 +1199,21 @@ def QueryStateAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/QueryStateAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/QueryStateAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def DeleteState(request, @@ -1138,11 +1226,21 @@ def DeleteState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/DeleteState', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/DeleteState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteStateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def DeleteBulkState(request, @@ -1155,11 +1253,21 @@ def DeleteBulkState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/DeleteBulkState', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/DeleteBulkState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteBulkStateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ExecuteStateTransaction(request, @@ -1172,11 +1280,21 @@ def ExecuteStateTransaction(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ExecuteStateTransaction', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ExecuteStateTransaction', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteStateTransactionRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def PublishEvent(request, @@ -1189,11 +1307,21 @@ def PublishEvent(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PublishEvent', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/PublishEvent', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PublishEventRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def BulkPublishEventAlpha1(request, @@ -1206,11 +1334,21 @@ def BulkPublishEventAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/BulkPublishEventAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/BulkPublishEventAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubscribeTopicEventsAlpha1(request_iterator, @@ -1223,11 +1361,21 @@ def SubscribeTopicEventsAlpha1(request_iterator, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', + return grpc.experimental.stream_stream( + request_iterator, + target, + '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def InvokeBinding(request, @@ -1240,11 +1388,21 @@ def InvokeBinding(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/InvokeBinding', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/InvokeBinding', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetSecret(request, @@ -1257,11 +1415,21 @@ def GetSecret(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetSecret', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetSecret', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetBulkSecret(request, @@ -1274,11 +1442,21 @@ def GetBulkSecret(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetBulkSecret', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetBulkSecret', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def RegisterActorTimer(request, @@ -1291,11 +1469,21 @@ def RegisterActorTimer(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RegisterActorTimer', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/RegisterActorTimer', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorTimerRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def UnregisterActorTimer(request, @@ -1308,11 +1496,21 @@ def UnregisterActorTimer(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnregisterActorTimer', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/UnregisterActorTimer', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorTimerRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def RegisterActorReminder(request, @@ -1325,11 +1523,21 @@ def RegisterActorReminder(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RegisterActorReminder', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/RegisterActorReminder', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorReminderRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def UnregisterActorReminder(request, @@ -1342,11 +1550,21 @@ def UnregisterActorReminder(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnregisterActorReminder', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/UnregisterActorReminder', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorReminderRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetActorState(request, @@ -1359,11 +1577,21 @@ def GetActorState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetActorState', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetActorState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ExecuteActorStateTransaction(request, @@ -1376,11 +1604,21 @@ def ExecuteActorStateTransaction(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ExecuteActorStateTransaction', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ExecuteActorStateTransaction', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteActorStateTransactionRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def InvokeActor(request, @@ -1393,11 +1631,21 @@ def InvokeActor(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/InvokeActor', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/InvokeActor', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetConfigurationAlpha1(request, @@ -1410,11 +1658,21 @@ def GetConfigurationAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetConfigurationAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetConfigurationAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetConfiguration(request, @@ -1427,11 +1685,21 @@ def GetConfiguration(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetConfiguration', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetConfiguration', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubscribeConfigurationAlpha1(request, @@ -1444,11 +1712,21 @@ def SubscribeConfigurationAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_stream(request, target, '/dapr.proto.runtime.v1.Dapr/SubscribeConfigurationAlpha1', + return grpc.experimental.unary_stream( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubscribeConfigurationAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubscribeConfiguration(request, @@ -1461,11 +1739,21 @@ def SubscribeConfiguration(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_stream(request, target, '/dapr.proto.runtime.v1.Dapr/SubscribeConfiguration', + return grpc.experimental.unary_stream( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubscribeConfiguration', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def UnsubscribeConfigurationAlpha1(request, @@ -1478,11 +1766,21 @@ def UnsubscribeConfigurationAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfigurationAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfigurationAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def UnsubscribeConfiguration(request, @@ -1495,11 +1793,21 @@ def UnsubscribeConfiguration(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfiguration', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfiguration', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def TryLockAlpha1(request, @@ -1512,11 +1820,21 @@ def TryLockAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/TryLockAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/TryLockAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def UnlockAlpha1(request, @@ -1529,11 +1847,21 @@ def UnlockAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnlockAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/UnlockAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def EncryptAlpha1(request_iterator, @@ -1546,11 +1874,21 @@ def EncryptAlpha1(request_iterator, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/EncryptAlpha1', + return grpc.experimental.stream_stream( + request_iterator, + target, + '/dapr.proto.runtime.v1.Dapr/EncryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def DecryptAlpha1(request_iterator, @@ -1563,11 +1901,21 @@ def DecryptAlpha1(request_iterator, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/DecryptAlpha1', + return grpc.experimental.stream_stream( + request_iterator, + target, + '/dapr.proto.runtime.v1.Dapr/DecryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetMetadata(request, @@ -1580,11 +1928,21 @@ def GetMetadata(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetMetadata', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetMetadata', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SetMetadata(request, @@ -1597,11 +1955,21 @@ def SetMetadata(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SetMetadata', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SetMetadata', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SetMetadataRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleGetKeyAlpha1(request, @@ -1614,11 +1982,21 @@ def SubtleGetKeyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleGetKeyAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleGetKeyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleEncryptAlpha1(request, @@ -1631,11 +2009,21 @@ def SubtleEncryptAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleEncryptAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleEncryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleDecryptAlpha1(request, @@ -1648,11 +2036,21 @@ def SubtleDecryptAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleDecryptAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleDecryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleWrapKeyAlpha1(request, @@ -1665,11 +2063,21 @@ def SubtleWrapKeyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleWrapKeyAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleWrapKeyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleUnwrapKeyAlpha1(request, @@ -1682,11 +2090,21 @@ def SubtleUnwrapKeyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleUnwrapKeyAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleUnwrapKeyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleSignAlpha1(request, @@ -1699,11 +2117,21 @@ def SubtleSignAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleSignAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleSignAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubtleVerifyAlpha1(request, @@ -1716,11 +2144,21 @@ def SubtleVerifyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleVerifyAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/SubtleVerifyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def StartWorkflowAlpha1(request, @@ -1733,11 +2171,21 @@ def StartWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/StartWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/StartWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetWorkflowAlpha1(request, @@ -1750,11 +2198,21 @@ def GetWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def PurgeWorkflowAlpha1(request, @@ -1767,11 +2225,21 @@ def PurgeWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def TerminateWorkflowAlpha1(request, @@ -1784,11 +2252,21 @@ def TerminateWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def PauseWorkflowAlpha1(request, @@ -1801,11 +2279,21 @@ def PauseWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PauseWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/PauseWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ResumeWorkflowAlpha1(request, @@ -1818,11 +2306,21 @@ def ResumeWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def RaiseEventWorkflowAlpha1(request, @@ -1835,11 +2333,21 @@ def RaiseEventWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def StartWorkflowBeta1(request, @@ -1852,11 +2360,21 @@ def StartWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/StartWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/StartWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetWorkflowBeta1(request, @@ -1869,11 +2387,21 @@ def GetWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def PurgeWorkflowBeta1(request, @@ -1886,11 +2414,21 @@ def PurgeWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def TerminateWorkflowBeta1(request, @@ -1903,11 +2441,21 @@ def TerminateWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def PauseWorkflowBeta1(request, @@ -1920,11 +2468,21 @@ def PauseWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PauseWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/PauseWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ResumeWorkflowBeta1(request, @@ -1937,11 +2495,21 @@ def ResumeWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def RaiseEventWorkflowBeta1(request, @@ -1954,11 +2522,21 @@ def RaiseEventWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowBeta1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def Shutdown(request, @@ -1971,11 +2549,21 @@ def Shutdown(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/Shutdown', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/Shutdown', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ShutdownRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ScheduleJobAlpha1(request, @@ -1988,11 +2576,21 @@ def ScheduleJobAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ScheduleJobAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ScheduleJobAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetJobAlpha1(request, @@ -2005,11 +2603,21 @@ def GetJobAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetJobAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/GetJobAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def DeleteJobAlpha1(request, @@ -2022,11 +2630,21 @@ def DeleteJobAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/DeleteJobAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/DeleteJobAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def ConverseAlpha1(request, @@ -2039,8 +2657,45 @@ def ConverseAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ConverseAlpha1', + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ConverseAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ConverseAlpha2(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/dapr.proto.runtime.v1.Dapr/ConverseAlpha2', + dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequestAlpha2.SerializeToString, + dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponseAlpha2.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/dev-requirements.txt b/dev-requirements.txt index cec56fb2..ec6f841c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -16,3 +16,5 @@ Flask>=1.1 ruff===0.2.2 # needed for dapr-ext-workflow durabletask-dapr >= 0.2.0a7 +# needed for .env file loading in examples +python-dotenv>=1.0.0 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2b8ddf72..d7a62aea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,14 @@ target-version = "py38" line-length = 100 fix = true -extend-exclude = [".github", "dapr/proto"] +extend-exclude = [ + ".github", + "dapr/proto", + "tools/dapr/proto", + "**/*_pb2.py", + "**/*_pb2_grpc.py", + "**/*_pb2.pyi" +] [tool.ruff.lint] select = [ "E", # pycodestyle errors diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 5ffc9da1..3ca7391c 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -34,6 +34,12 @@ EncryptResponse, DecryptRequest, DecryptResponse, + ConversationResultAlpha2, + ConversationResultChoices, + ConversationResultMessage, + ConversationResponseAlpha2, + ConversationToolCalls, + ConversationToolCallsOfFunction, ) from typing import Dict @@ -537,6 +543,77 @@ def ConverseAlpha1(self, request, context): return api_v1.ConversationResponse(contextID=request.contextID, outputs=outputs) + def ConverseAlpha2(self, request, context): + """Mock implementation of the ConverseAlpha2 endpoint.""" + self.check_for_exception(context) + + # Process inputs and create responses with choices structure + outputs = [] + for input_idx, input in enumerate(request.inputs): + choices = [] + + # Process each message in the input + for msg_idx, message in enumerate(input.messages): + response_content = "" + tool_calls = [] + + # Extract content based on message type + if message.HasField('of_user'): + if message.of_user.content: + response_content = f"Response to user: {message.of_user.content[0].text}" + elif message.HasField('of_system'): + if message.of_system.content: + response_content = f"System acknowledged: {message.of_system.content[0].text}" + elif message.HasField('of_assistant'): + if message.of_assistant.content: + response_content = f"Assistant continued: {message.of_assistant.content[0].text}" + elif message.HasField('of_developer'): + if message.of_developer.content: + response_content = f"Developer note processed: {message.of_developer.content[0].text}" + elif message.HasField('of_tool'): + if message.of_tool.content: + response_content = f"Tool result processed: {message.of_tool.content[0].text}" + + # Check if tools are available and simulate tool calling + if request.tools and response_content and "weather" in response_content.lower(): + # Simulate a tool call for weather requests + for tool in request.tools: + if tool.function and "weather" in tool.function.name.lower(): + tool_call = ConversationToolCalls( + id=f"call_{input_idx}_{msg_idx}", + function=ConversationToolCallsOfFunction( + name=tool.function.name, + arguments='{"location": "San Francisco", "unit": "celsius"}' + ) + ) + tool_calls.append(tool_call) + response_content = "I'll check the weather for you." + break + + # Create result message + result_message = ConversationResultMessage( + content=response_content, + tool_calls=tool_calls + ) + + # Create choice + finish_reason = "tool_calls" if tool_calls else "stop" + choice = ConversationResultChoices( + finish_reason=finish_reason, + index=msg_idx, + message=result_message + ) + choices.append(choice) + + # Create result for this input + result = ConversationResultAlpha2(choices=choices) + outputs.append(result) + + return ConversationResponseAlpha2( + context_id=request.context_id if request.HasField('context_id') else None, + outputs=outputs + ) + def ScheduleJobAlpha1(self, request, context): self.check_for_exception(context) diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py new file mode 100644 index 00000000..49029af7 --- /dev/null +++ b/tests/clients/test_conversation.py @@ -0,0 +1,783 @@ +#!/usr/bin/env python3 + +""" +Comprehensive tests for Dapr conversation API functionality. + +This test suite covers: +- Basic conversation API (Alpha1) +- Advanced conversation API (Alpha2) with tool calling +- Multi-turn conversations +- Different message types (user, system, assistant, developer, tool) +- Error handling +- Both sync and async implementations +- Parameter conversion and validation +""" + +import asyncio +import json +import unittest + +from google.protobuf.any_pb2 import Any as GrpcAny +from google.rpc import code_pb2, status_pb2 + +from dapr.aio.clients import DaprClient as AsyncDaprClient +from dapr.clients import DaprClient +from dapr.clients.exceptions import DaprGrpcError +from dapr.clients.grpc._request import ( + ConversationInput, + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageContent, + ConversationMessageOfUser, + ConversationMessageOfSystem, + ConversationMessageOfAssistant, + ConversationMessageOfDeveloper, + ConversationMessageOfTool, + ConversationTools, + ConversationToolsFunction, + ConversationToolCalls, + ConversationToolCallsOfFunction, +) +from tests.clients.fake_dapr_server import FakeDaprSidecar + + +class ConversationTestBase: + """Base class for conversation tests with common setup.""" + + grpc_port = 50011 + http_port = 3510 + scheme = '' + + @classmethod + def setUpClass(cls): + cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls.grpc_port, http_port=cls.http_port) + cls._fake_dapr_server.start() + + @classmethod + def tearDownClass(cls): + cls._fake_dapr_server.stop() + + def create_weather_tool(self): + """Create a weather tool for testing.""" + return ConversationTools( + function=ConversationToolsFunction( + name='get_weather', + description='Get weather information for a location', + parameters={ + 'location': GrpcAny(value=json.dumps({ + 'type': 'string', + 'description': 'The city and state, e.g. San Francisco, CA' + }).encode()), + 'unit': GrpcAny(value=json.dumps({ + 'type': 'string', + 'enum': ['celsius', 'fahrenheit'], + 'description': 'Temperature unit' + }).encode()) + } + ) + ) + + def create_calculate_tool(self): + """Create a calculate tool for testing.""" + return ConversationTools( + function=ConversationToolsFunction( + name='calculate', + description='Perform mathematical calculations', + parameters={ + 'expression': GrpcAny(value=json.dumps({ + 'type': 'string', + 'description': 'Mathematical expression to evaluate' + }).encode()) + } + ) + ) + + def create_user_message(self, text): + """Helper to create a user message for Alpha2.""" + return ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text=text)] + ) + ) + + def create_system_message(self, text): + """Helper to create a system message for Alpha2.""" + return ConversationMessage( + of_system=ConversationMessageOfSystem( + content=[ConversationMessageContent(text=text)] + ) + ) + + def create_assistant_message(self, text, tool_calls=None): + """Helper to create an assistant message for Alpha2.""" + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=[ConversationMessageContent(text=text)], + tool_calls=tool_calls or [] + ) + ) + + def create_tool_message(self, tool_id, name, content): + """Helper to create a tool message for Alpha2.""" + return ConversationMessage( + of_tool=ConversationMessageOfTool( + tool_id=tool_id, + name=name, + content=[ConversationMessageContent(text=content)] + ) + ) + + +class ConversationAlpha1SyncTests(ConversationTestBase, unittest.TestCase): + """Synchronous Alpha1 conversation API tests.""" + + def test_basic_conversation_alpha1(self): + """Test basic Alpha1 conversation functionality.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ + ConversationInput(content='Hello', role='user'), + ConversationInput(content='How are you?', role='user'), + ] + + response = client.converse_alpha1(name='test-llm', inputs=inputs) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 2) + self.assertIn('Hello', response.outputs[0].result) + self.assertIn('How are you?', response.outputs[1].result) + + def test_conversation_alpha1_with_options(self): + """Test Alpha1 conversation with various options.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ConversationInput(content='Hello with options', role='user', scrub_pii=True)] + + response = client.converse_alpha1( + name='test-llm', + inputs=inputs, + context_id='test-context-123', + temperature=0.7, + scrub_pii=True, + metadata={'test_key': 'test_value'} + ) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(response.context_id, 'test-context-123') + + def test_alpha1_parameter_conversion(self): + """Test Alpha1 parameter conversion with raw Python values.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ConversationInput(content='Test with parameters', role='user')] + + # Test with raw Python parameters - these should be automatically converted + response = client.converse_alpha1( + name='test-llm', + inputs=inputs, + parameters={ + 'temperature': 0.7, + 'max_tokens': 1000, + 'top_p': 0.9, + 'frequency_penalty': 0.0, + 'presence_penalty': 0.0, + } + ) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + + def test_alpha1_error_handling(self): + """Test Alpha1 conversation error handling.""" + # Setup server to raise an exception + self._fake_dapr_server.raise_exception_on_next_call( + status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha1 test error') + ) + + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ConversationInput(content='Error test', role='user')] + + with self.assertRaises(DaprGrpcError) as context: + client.converse_alpha1(name='test-llm', inputs=inputs) + self.assertIn('Alpha1 test error', str(context.exception)) + + +class ConversationAlpha2SyncTests(ConversationTestBase, unittest.TestCase): + """Synchronous Alpha2 conversation API tests.""" + + def test_basic_conversation_alpha2(self): + """Test basic Alpha2 conversation functionality.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Hello Alpha2!') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 1) + + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'stop') + self.assertIn('Hello Alpha2!', choice.message.content) + + def test_conversation_alpha2_with_system_message(self): + """Test Alpha2 conversation with system message.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + system_message = self.create_system_message('You are a helpful assistant.') + user_message = self.create_user_message('Hello!') + + input_alpha2 = ConversationInputAlpha2( + messages=[system_message, user_message], + scrub_pii=False + ) + + response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs[0].choices), 2) + + # Check system message response + system_choice = response.outputs[0].choices[0] + self.assertIn('System acknowledged', system_choice.message.content) + + # Check user message response + user_choice = response.outputs[0].choices[1] + self.assertIn('Response to user', user_choice.message.content) + + def test_conversation_alpha2_with_options(self): + """Test Alpha2 conversation with various options.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Alpha2 with options') + input_alpha2 = ConversationInputAlpha2( + messages=[user_message], + scrub_pii=True + ) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + context_id='alpha2-context-123', + temperature=0.8, + scrub_pii=True, + metadata={'alpha2_test': 'true'}, + tool_choice='none' + ) + + self.assertIsNotNone(response) + self.assertEqual(response.context_id, 'alpha2-context-123') + + def test_alpha2_parameter_conversion(self): + """Test Alpha2 parameter conversion with various types.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Parameter conversion test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'model': 'gpt-4o-mini', + 'temperature': 0.7, + 'max_tokens': 1000, + 'top_p': 1.0, + 'frequency_penalty': 0.0, + 'presence_penalty': 0.0, + 'stream': False, + } + ) + + self.assertIsNotNone(response) + + def test_alpha2_error_handling(self): + """Test Alpha2 conversation error handling.""" + self._fake_dapr_server.raise_exception_on_next_call( + status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha2 test error') + ) + + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Error test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + with self.assertRaises(DaprGrpcError) as context: + client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + self.assertIn('Alpha2 test error', str(context.exception)) + + +class ConversationToolCallingSyncTests(ConversationTestBase, unittest.TestCase): + """Synchronous tool calling tests for Alpha2.""" + + def test_tool_calling_weather(self): + """Test tool calling with weather tool.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + weather_tool = self.create_weather_tool() + user_message = self.create_user_message('What is the weather in San Francisco?') + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool], + tool_choice='auto' + ) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + self.assertEqual(len(choice.message.tool_calls), 1) + + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') + self.assertIn('San Francisco', tool_call.function.arguments) + + def test_tool_calling_calculate(self): + """Test tool calling with calculate tool.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + calc_tool = self.create_calculate_tool() + user_message = self.create_user_message('Calculate 15 * 23') + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[calc_tool] + ) + + # Note: Our fake server only triggers weather tools, so this won't return tool calls + # but it tests that the API works with different tools + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertIn('Calculate', choice.message.content) + + def test_multiple_tools(self): + """Test conversation with multiple tools.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + weather_tool = self.create_weather_tool() + calc_tool = self.create_calculate_tool() + + user_message = self.create_user_message('I need weather and calculation help') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool, calc_tool], + tool_choice='auto' + ) + + self.assertIsNotNone(response) + # The fake server will call weather tool if "weather" is in the message + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + + def test_tool_choice_none(self): + """Test tool choice set to 'none'.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + weather_tool = self.create_weather_tool() + user_message = self.create_user_message('What is the weather today?') + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool], + tool_choice='none' + ) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + # With tool_choice='none', should not make tool calls even if weather is mentioned + # (though our fake server may still trigger based on content) + self.assertIsNotNone(choice.message.content) + + def test_tool_choice_specific(self): + """Test tool choice set to specific tool name.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + weather_tool = self.create_weather_tool() + calc_tool = self.create_calculate_tool() + + user_message = self.create_user_message('What is the weather like?') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool, calc_tool], + tool_choice='get_weather' + ) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + if choice.finish_reason == 'tool_calls': + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') + + +class ConversationMultiTurnSyncTests(ConversationTestBase, unittest.TestCase): + """Multi-turn conversation tests for Alpha2.""" + + def test_multi_turn_conversation(self): + """Test multi-turn conversation with different message types.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + # Create a conversation with system, user, and assistant messages + system_message = self.create_system_message('You are a helpful AI assistant.') + user_message1 = self.create_user_message('Hello, how are you?') + assistant_message = self.create_assistant_message('I am doing well, thank you!') + user_message2 = self.create_user_message('What can you help me with?') + + input_alpha2 = ConversationInputAlpha2( + messages=[system_message, user_message1, assistant_message, user_message2] + ) + + response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs[0].choices), 4) + + # Check each response type + choices = response.outputs[0].choices + self.assertIn('System acknowledged', choices[0].message.content) + self.assertIn('Response to user', choices[1].message.content) + self.assertIn('Assistant continued', choices[2].message.content) + self.assertIn('Response to user', choices[3].message.content) + + def test_tool_calling_workflow(self): + """Test complete tool calling workflow.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + # Step 1: User asks for weather + weather_tool = self.create_weather_tool() + user_message = self.create_user_message('What is the weather in Tokyo?') + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response1 = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool] + ) + + # Should get tool call + self.assertIsNotNone(response1) + choice = response1.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + tool_call = choice.message.tool_calls[0] + + # Step 2: Send tool result back + tool_result_message = self.create_tool_message( + tool_id=tool_call.id, + name='get_weather', + content='{"temperature": 18, "condition": "cloudy", "humidity": 75}' + ) + + result_input = ConversationInputAlpha2(messages=[tool_result_message]) + + response2 = client.converse_alpha2(name='test-llm', inputs=[result_input]) + + # Should get processed tool result + self.assertIsNotNone(response2) + result_choice = response2.outputs[0].choices[0] + self.assertIn('Tool result processed', result_choice.message.content) + + def test_conversation_context_continuity(self): + """Test conversation context continuity with context_id.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + context_id = 'multi-turn-test-123' + + # First turn + user_message1 = self.create_user_message('My name is Alice.') + input1 = ConversationInputAlpha2(messages=[user_message1]) + + response1 = client.converse_alpha2( + name='test-llm', + inputs=[input1], + context_id=context_id + ) + + self.assertEqual(response1.context_id, context_id) + + # Second turn with same context + user_message2 = self.create_user_message('What is my name?') + input2 = ConversationInputAlpha2(messages=[user_message2]) + + response2 = client.converse_alpha2( + name='test-llm', + inputs=[input2], + context_id=context_id + ) + + self.assertEqual(response2.context_id, context_id) + self.assertIsNotNone(response2.outputs[0].choices[0].message.content) + + +class ConversationAsyncTests(ConversationTestBase, unittest.IsolatedAsyncioTestCase): + """Asynchronous conversation API tests.""" + + async def test_basic_async_conversation_alpha1(self): + """Test basic async Alpha1 conversation.""" + async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ + ConversationInput(content='Hello async', role='user'), + ConversationInput(content='How are you async?', role='user'), + ] + + response = await client.converse_alpha1(name='test-llm', inputs=inputs) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 2) + self.assertIn('Hello async', response.outputs[0].result) + + async def test_basic_async_conversation_alpha2(self): + """Test basic async Alpha2 conversation.""" + async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Hello async Alpha2!') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = await client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertIn('Hello async Alpha2!', choice.message.content) + + async def test_async_tool_calling(self): + """Test async tool calling.""" + async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + weather_tool = self.create_weather_tool() + user_message = self.create_user_message('Async weather request for London') + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = await client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool] + ) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') + + async def test_concurrent_async_conversations(self): + """Test multiple concurrent async conversations.""" + async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + async def run_alpha1_conversation(message, session_id): + inputs = [ConversationInput(content=message, role='user')] + response = await client.converse_alpha1( + name='test-llm', + inputs=inputs, + context_id=session_id + ) + return response.outputs[0].result + + async def run_alpha2_conversation(message, session_id): + user_message = self.create_user_message(message) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + response = await client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + context_id=session_id + ) + return response.outputs[0].choices[0].message.content + + # Run concurrent conversations with both Alpha1 and Alpha2 + tasks = [ + run_alpha1_conversation('First Alpha1 message', 'concurrent-alpha1'), + run_alpha2_conversation('First Alpha2 message', 'concurrent-alpha2'), + run_alpha1_conversation('Second Alpha1 message', 'concurrent-alpha1-2'), + run_alpha2_conversation('Second Alpha2 message', 'concurrent-alpha2-2'), + ] + + results = await asyncio.gather(*tasks) + + self.assertEqual(len(results), 4) + for result in results: + self.assertIsNotNone(result) + self.assertIsInstance(result, str) + + async def test_async_multi_turn_with_tools(self): + """Test async multi-turn conversation with tool calling.""" + async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + # First turn: user asks for weather + weather_tool = self.create_weather_tool() + user_message = self.create_user_message('Async weather for Paris') + input1 = ConversationInputAlpha2(messages=[user_message]) + + response1 = await client.converse_alpha2( + name='test-llm', + inputs=[input1], + tools=[weather_tool], + context_id='async-multi-turn' + ) + + # Should get tool call + self.assertEqual(response1.outputs[0].choices[0].finish_reason, 'tool_calls') + tool_call = response1.outputs[0].choices[0].message.tool_calls[0] + + # Second turn: provide tool result + tool_result_message = self.create_tool_message( + tool_id=tool_call.id, + name='get_weather', + content='{"temperature": 22, "condition": "sunny"}' + ) + input2 = ConversationInputAlpha2(messages=[tool_result_message]) + + response2 = await client.converse_alpha2( + name='test-llm', + inputs=[input2], + context_id='async-multi-turn' + ) + + self.assertIsNotNone(response2) + self.assertIn('Tool result processed', response2.outputs[0].choices[0].message.content) + + async def test_async_error_handling(self): + """Test async conversation error handling.""" + self._fake_dapr_server.raise_exception_on_next_call( + status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Async test error') + ) + + async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ConversationInput(content='Async error test', role='user')] + + with self.assertRaises(DaprGrpcError) as context: + await client.converse_alpha1(name='test-llm', inputs=inputs) + self.assertIn('Async test error', str(context.exception)) + + +class ConversationParameterTests(ConversationTestBase, unittest.TestCase): + """Tests for parameter handling and conversion.""" + + def test_parameter_backward_compatibility(self): + """Test that pre-wrapped protobuf parameters still work.""" + from google.protobuf.wrappers_pb2 import StringValue + + # Create pre-wrapped parameter (old way) + pre_wrapped_any = GrpcAny() + pre_wrapped_any.Pack(StringValue(value="auto")) + + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + inputs = [ConversationInput(content='Backward compatibility test', role='user')] + + # Mix of old (pre-wrapped) and new (raw) parameters + response = client.converse_alpha1( + name='test-llm', + inputs=inputs, + parameters={ + 'tool_choice': pre_wrapped_any, # Old way (pre-wrapped) + 'temperature': 0.8, # New way (raw value) + 'max_tokens': 500, # New way (raw value) + } + ) + + self.assertIsNotNone(response) + + def test_parameter_edge_cases(self): + """Test parameter conversion with edge cases.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Edge cases test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'int32_max': 2147483647, # Int32 maximum + 'int64_large': 9999999999, # Requires Int64 + 'negative_temp': -0.5, # Negative float + 'zero_value': 0, # Zero integer + 'false_flag': False, # Boolean false + 'true_flag': True, # Boolean true + 'empty_string': '', # Empty string + } + ) + + self.assertIsNotNone(response) + + def test_realistic_provider_parameters(self): + """Test with realistic LLM provider parameters.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + user_message = self.create_user_message('Provider parameters test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + # OpenAI-style parameters + response1 = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'model': 'gpt-4o-mini', + 'temperature': 0.7, + 'max_tokens': 1000, + 'top_p': 1.0, + 'frequency_penalty': 0.0, + 'presence_penalty': 0.0, + 'stream': False, + 'tool_choice': 'auto', + } + ) + + # Anthropic-style parameters + response2 = client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'model': 'claude-3-5-sonnet-20241022', + 'max_tokens': 4096, + 'temperature': 0.8, + 'top_p': 0.9, + 'top_k': 250, + 'stream': False, + } + ) + + self.assertIsNotNone(response1) + self.assertIsNotNone(response2) + + +class ConversationValidationTests(ConversationTestBase, unittest.TestCase): + """Tests for input validation and edge cases.""" + + def test_empty_inputs_alpha1(self): + """Test Alpha1 with empty inputs.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + response = client.converse_alpha1(name='test-llm', inputs=[]) + self.assertIsNotNone(response) + + def test_empty_inputs_alpha2(self): + """Test Alpha2 with empty inputs.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + response = client.converse_alpha2(name='test-llm', inputs=[]) + self.assertIsNotNone(response) + + def test_empty_messages_alpha2(self): + """Test Alpha2 with empty messages in input.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + input_alpha2 = ConversationInputAlpha2(messages=[]) + response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + self.assertIsNotNone(response) + + def test_mixed_alpha1_alpha2_compatibility(self): + """Test that Alpha1 and Alpha2 can be used in the same session.""" + with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + # Alpha1 call + alpha1_inputs = [ConversationInput(content='Alpha1 call', role='user')] + alpha1_response = client.converse_alpha1(name='test-llm', inputs=alpha1_inputs) + + # Alpha2 call + user_message = self.create_user_message('Alpha2 call') + alpha2_input = ConversationInputAlpha2(messages=[user_message]) + alpha2_response = client.converse_alpha2(name='test-llm', inputs=[alpha2_input]) + + # Both should work + self.assertIsNotNone(alpha1_response) + self.assertIsNotNone(alpha2_response) + + # Check response structures are different but valid + self.assertTrue(hasattr(alpha1_response, 'outputs')) + self.assertTrue(hasattr(alpha2_response, 'outputs')) + self.assertTrue(hasattr(alpha2_response.outputs[0], 'choices')) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 369d7c08..6750bb6a 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -37,6 +37,18 @@ TransactionalStateOperation, TransactionOperationType, ConversationInput, + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageContent, + ConversationMessageOfUser, + ConversationMessageOfAssistant, + ConversationMessageOfSystem, + ConversationMessageOfDeveloper, + ConversationMessageOfTool, + ConversationTools, + ConversationToolsFunction, + ConversationToolCalls, + ConversationToolCallsOfFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem @@ -1235,6 +1247,298 @@ def test_converse_alpha1_error_handling(self): dapr.converse_alpha1(name='test-llm', inputs=inputs) self.assertTrue('Invalid argument' in str(context.exception)) + def test_converse_alpha2_basic_user_message(self): + """Test basic Alpha2 conversation with user messages.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create user message + user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + name="TestUser", + content=[ConversationMessageContent(text="Hello, how are you?")] + ) + ) + + # Create Alpha2 input + input_alpha2 = ConversationInputAlpha2( + messages=[user_message], + scrub_pii=False + ) + + response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + # Check response structure + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 1) + + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'stop') + self.assertEqual(choice.index, 0) + self.assertEqual(choice.message.content, 'Response to user: Hello, how are you?') + self.assertEqual(len(choice.message.tool_calls), 0) + + def test_converse_alpha2_with_tools_weather_request(self): + """Test Alpha2 conversation with tool calling for weather requests.""" + from google.protobuf.any_pb2 import Any as GrpcAny + + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create weather tool + weather_tool = ConversationTools( + function=ConversationToolsFunction( + name="get_weather", + description="Get current weather information", + parameters={"location": GrpcAny(value=b'{"type": "string"}')} + ) + ) + + # Create user message asking for weather + user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="What's the weather like?")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = dapr.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool], + tool_choice='auto' + ) + + # Check response structure with tool call + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 1) + + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + self.assertEqual(choice.index, 0) + self.assertEqual(choice.message.content, "I'll check the weather for you.") + self.assertEqual(len(choice.message.tool_calls), 1) + + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') + self.assertEqual(tool_call.function.arguments, '{"location": "San Francisco", "unit": "celsius"}') + self.assertTrue(tool_call.id.startswith('call_')) + + def test_converse_alpha2_system_message(self): + """Test Alpha2 conversation with system messages.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create system message + system_message = ConversationMessage( + of_system=ConversationMessageOfSystem( + content=[ConversationMessageContent(text="You are a helpful assistant.")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[system_message]) + + response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + # Check response + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.message.content, 'System acknowledged: You are a helpful assistant.') + + def test_converse_alpha2_developer_message(self): + """Test Alpha2 conversation with developer messages.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create developer message + developer_message = ConversationMessage( + of_developer=ConversationMessageOfDeveloper( + name="DevTeam", + content=[ConversationMessageContent(text="Debug: Processing user input")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[developer_message]) + + response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + # Check response + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.message.content, 'Developer note processed: Debug: Processing user input') + + def test_converse_alpha2_tool_message(self): + """Test Alpha2 conversation with tool messages.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create tool message + tool_message = ConversationMessage( + of_tool=ConversationMessageOfTool( + tool_id="call_123", + name="get_weather", + content=[ConversationMessageContent(text='{"temperature": 22, "condition": "sunny"}')] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[tool_message]) + + response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + # Check response + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.message.content, 'Tool result processed: {"temperature": 22, "condition": "sunny"}') + + def test_converse_alpha2_assistant_message(self): + """Test Alpha2 conversation with assistant messages.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create assistant message + assistant_message = ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=[ConversationMessageContent(text="I understand your request.")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[assistant_message]) + + response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + # Check response + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.message.content, 'Assistant continued: I understand your request.') + + def test_converse_alpha2_multiple_messages(self): + """Test Alpha2 conversation with multiple messages in one input.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create multiple messages + system_message = ConversationMessage( + of_system=ConversationMessageOfSystem( + content=[ConversationMessageContent(text="You are helpful.")] + ) + ) + + user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="Hello!")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[system_message, user_message]) + + response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + # Check response has choices for both messages + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 2) + + # Check individual responses + self.assertEqual(response.outputs[0].choices[0].message.content, 'System acknowledged: You are helpful.') + self.assertEqual(response.outputs[0].choices[1].message.content, 'Response to user: Hello!') + + def test_converse_alpha2_with_context_and_options(self): + """Test Alpha2 conversation with context ID and various options.""" + from google.protobuf.any_pb2 import Any as GrpcAny + + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="Continue our conversation")] + ) + ) + + input_alpha2 = ConversationInputAlpha2( + messages=[user_message], + scrub_pii=True + ) + + # Create custom parameters + params = {"custom_param": GrpcAny(value=b'{"setting": "value"}')} + + response = dapr.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + context_id='chat-session-123', + parameters=params, + metadata={'env': 'test'}, + scrub_pii=True, + temperature=0.7, + tool_choice='none' + ) + + # Check response + self.assertIsNotNone(response) + self.assertEqual(response.context_id, 'chat-session-123') + choice = response.outputs[0].choices[0] + self.assertEqual(choice.message.content, 'Response to user: Continue our conversation') + + def test_converse_alpha2_error_handling(self): + """Test Alpha2 conversation error handling.""" + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Setup server to raise an exception + self._fake_dapr_server.raise_exception_on_next_call( + status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha2 Invalid argument') + ) + + user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="Test error")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + with self.assertRaises(DaprGrpcError) as context: + dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + self.assertTrue('Alpha2 Invalid argument' in str(context.exception)) + + def test_converse_alpha2_tool_choice_specific(self): + """Test Alpha2 conversation with specific tool choice.""" + from google.protobuf.any_pb2 import Any as GrpcAny + + dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') + + # Create multiple tools + weather_tool = ConversationTools( + function=ConversationToolsFunction( + name="get_weather", + description="Get weather information" + ) + ) + + calculator_tool = ConversationTools( + function=ConversationToolsFunction( + name="calculate", + description="Perform calculations" + ) + ) + + user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="What's the weather today?")] + ) + ) + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = dapr.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool, calculator_tool], + tool_choice='get_weather' # Force specific tool + ) + + # Even though we specified a specific tool, our mock will still trigger + # based on content matching "weather" + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + if 'weather' in choice.message.content.lower(): + self.assertEqual(choice.finish_reason, 'tool_calls') + # # Tests for Jobs API (Alpha) # From bbf381cf1c06e2b7ca62948747f0a814ce166d71 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:00:14 -0500 Subject: [PATCH 02/29] fixes after proto change upstream Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 71 +- dapr/clients/_constants.py | 25 + dapr/clients/base.py | 4 - dapr/clients/grpc/_helpers.py | 277 +++- dapr/clients/grpc/_request.py | 12 +- dapr/clients/grpc/_response.py | 2 +- dapr/clients/grpc/_schema_helpers.py | 336 +++++ dapr/clients/grpc/client.py | 77 +- dapr/clients/http/client.py | 2 +- dapr/proto/common/v1/common_pb2.py | 20 +- dapr/proto/common/v1/common_pb2_grpc.py | 20 - dapr/proto/runtime/v1/appcallback_pb2.py | 24 +- dapr/proto/runtime/v1/appcallback_pb2_grpc.py | 167 +-- dapr/proto/runtime/v1/dapr_pb2.py | 765 +++++----- dapr/proto/runtime/v1/dapr_pb2.pyi | 25 +- dapr/proto/runtime/v1/dapr_pb2_grpc.py | 1101 ++++----------- dev-requirements.txt | 6 +- examples/conversation/.env.example | 20 + examples/conversation/README.md | 676 ++++++++- examples/conversation/config/anthropic.yaml | 12 + examples/conversation/config/deepseek.yaml | 12 + examples/conversation/config/google.yaml | 12 + examples/conversation/config/mistral.yaml | 12 + examples/conversation/config/openai.yaml | 12 + .../real_llm_providers_example.py | 1230 +++++++++++++++++ ext/dapr-ext-grpc/dapr/ext/grpc/_servicer.py | 2 +- tests/clients/fake_dapr_server.py | 41 +- tests/clients/test_conversation.py | 171 +-- tests/clients/test_dapr_grpc_client.py | 94 +- tests/clients/test_grpc_helpers.py | 861 ++++++++++++ 30 files changed, 4343 insertions(+), 1746 deletions(-) create mode 100644 dapr/clients/_constants.py create mode 100644 dapr/clients/grpc/_schema_helpers.py create mode 100644 examples/conversation/.env.example create mode 100644 examples/conversation/config/anthropic.yaml create mode 100644 examples/conversation/config/deepseek.yaml create mode 100644 examples/conversation/config/google.yaml create mode 100644 examples/conversation/config/mistral.yaml create mode 100644 examples/conversation/config/openai.yaml create mode 100644 examples/conversation/real_llm_providers_example.py create mode 100644 tests/clients/test_grpc_helpers.py diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 473c27d2..c669bc13 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -64,6 +64,7 @@ validateNotNone, validateNotBlankString, convert_parameters, + convert_value_to_struct, ) from dapr.aio.clients.grpc._request import ( EncryptRequestIterator, @@ -81,15 +82,8 @@ ConversationInputAlpha2, ConversationMessage, ConversationMessageContent, - ConversationMessageOfUser, - ConversationMessageOfSystem, - ConversationMessageOfAssistant, - ConversationMessageOfDeveloper, - ConversationMessageOfTool, ConversationTools, - ConversationToolsFunction, ConversationToolCalls, - ConversationToolCallsOfFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( @@ -1739,7 +1733,7 @@ async def converse_alpha1( inputs: List[ConversationInput], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, Any]] = None, + parameters: Optional[Dict[str, GrpcAny]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, @@ -1766,14 +1760,14 @@ async def converse_alpha1( for inp in inputs ] - # Convert raw Python parameters to GrpcAny objects - converted_parameters = convert_parameters(parameters) + # # Convert raw Python parameters to GrpcAny objects + # converted_parameters = convert_parameters(parameters) request = api_v1.ConversationRequest( name=name, inputs=inputs_pb, contextID=context_id, - parameters=converted_parameters, + parameters=parameters, metadata=metadata or {}, scrubPII=scrub_pii, temperature=temperature, @@ -1798,7 +1792,7 @@ async def converse_alpha2( inputs: List[ConversationInputAlpha2], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, Any]] = None, + parameters: Optional[Dict[str, GrpcAny]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, @@ -1829,7 +1823,9 @@ def _convert_message_content(content_list: List[ConversationMessageContent]): """Convert message content list to proto format.""" if not content_list: return [] - return [api_v1.ConversationMessageContent(text=content.text) for content in content_list] + return [ + api_v1.ConversationMessageContent(text=content.text) for content in content_list + ] def _convert_tool_calls(tool_calls: List[ConversationToolCalls]): """Convert tool calls to proto format.""" @@ -1852,22 +1848,34 @@ def _convert_message(message: ConversationMessage): if message.of_developer: proto_message.of_developer.name = message.of_developer.name or '' - proto_message.of_developer.content.extend(_convert_message_content(message.of_developer.content or [])) + proto_message.of_developer.content.extend( + _convert_message_content(message.of_developer.content or []) + ) elif message.of_system: proto_message.of_system.name = message.of_system.name or '' - proto_message.of_system.content.extend(_convert_message_content(message.of_system.content or [])) + proto_message.of_system.content.extend( + _convert_message_content(message.of_system.content or []) + ) elif message.of_user: proto_message.of_user.name = message.of_user.name or '' - proto_message.of_user.content.extend(_convert_message_content(message.of_user.content or [])) + proto_message.of_user.content.extend( + _convert_message_content(message.of_user.content or []) + ) elif message.of_assistant: proto_message.of_assistant.name = message.of_assistant.name or '' - proto_message.of_assistant.content.extend(_convert_message_content(message.of_assistant.content or [])) - proto_message.of_assistant.tool_calls.extend(_convert_tool_calls(message.of_assistant.tool_calls or [])) + proto_message.of_assistant.content.extend( + _convert_message_content(message.of_assistant.content or []) + ) + proto_message.of_assistant.tool_calls.extend( + _convert_tool_calls(message.of_assistant.tool_calls or []) + ) elif message.of_tool: if message.of_tool.tool_id: proto_message.of_tool.tool_id = message.of_tool.tool_id proto_message.of_tool.name = message.of_tool.name - proto_message.of_tool.content.extend(_convert_message_content(message.of_tool.content or [])) + proto_message.of_tool.content.extend( + _convert_message_content(message.of_tool.content or []) + ) return proto_message @@ -1893,8 +1901,9 @@ def _convert_message(message: ConversationMessage): if tool.function.description: proto_tool.function.description = tool.function.description if tool.function.parameters: - for key, value in tool.function.parameters.items(): - proto_tool.function.parameters[key].CopyFrom(value) + proto_tool.function.parameters.CopyFrom( + convert_value_to_struct(tool.function.parameters) + ) tools_pb.append(proto_tool) # Convert raw Python parameters to GrpcAny objects @@ -1925,20 +1934,18 @@ def _convert_message(message: ConversationMessage): for output in response.outputs: choices = [] for choice in output.choices: - choices.append(ConversationResultChoices( - finish_reason=choice.finish_reason, - index=choice.index, - message=ConversationResultMessage( - content=choice.message.content, - tool_calls=choice.message.tool_calls + choices.append( + ConversationResultChoices( + finish_reason=choice.finish_reason, + index=choice.index, + message=ConversationResultMessage( + content=choice.message.content, tool_calls=choice.message.tool_calls + ), ) - )) + ) outputs.append(ConversationResultAlpha2(choices=choices)) - return ConversationResponseAlpha2( - context_id=response.context_id, - outputs=outputs - ) + return ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) except grpc.aio.AioRpcError as err: raise DaprGrpcError(err) from err diff --git a/dapr/clients/_constants.py b/dapr/clients/_constants.py new file mode 100644 index 00000000..31f6c4db --- /dev/null +++ b/dapr/clients/_constants.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +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. +""" + +""" +Internal constants for the Dapr clients package. + +This module contains shared constants that can be imported by various +client modules without creating circular dependencies. +""" + +# Encoding and content type constants +DEFAULT_ENCODING = 'utf-8' +DEFAULT_JSON_CONTENT_TYPE = f'application/json; charset={DEFAULT_ENCODING}' diff --git a/dapr/clients/base.py b/dapr/clients/base.py index dccb9624..d2b97224 100644 --- a/dapr/clients/base.py +++ b/dapr/clients/base.py @@ -17,10 +17,6 @@ from typing import Optional -DEFAULT_ENCODING = 'utf-8' -DEFAULT_JSON_CONTENT_TYPE = f'application/json; charset={DEFAULT_ENCODING}' - - class DaprActorClientBase(ABC): """A base class that represents Dapr Actor Client.""" diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index d5879650..35997f93 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -12,8 +12,9 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import Dict, List, Union, Tuple, Optional, Any from enum import Enum +from typing import Any, Dict, List, Optional, Union, Tuple + from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage from google.protobuf.wrappers_pb2 import ( @@ -24,6 +25,7 @@ DoubleValue, BytesValue, ) +from google.protobuf.struct_pb2 import Struct MetadataDict = Dict[str, List[Union[bytes, str]]] MetadataTuple = Tuple[Tuple[str, Union[bytes, str]], ...] @@ -115,21 +117,69 @@ def getWorkflowRuntimeStatus(inputString): return WorkflowRuntimeStatus.UNKNOWN -def convert_parameter_value(value: Any) -> GrpcAny: - """Convert a raw Python value to a GrpcAny protobuf message. +def convert_value_to_struct(value: Dict[str, Any]) -> Struct: + """Convert a raw Python value to a protobuf Struct message. + + This function converts Python values to a protobuf Struct, which is designed + to represent JSON-like dynamic data structures. + + Args: + value: Raw Python value (str, int, float, bool, None, dict, list, or already Struct) + + Returns: + Struct: The value converted to a protobuf Struct message + + Raises: + ValueError: If the value type is not supported or cannot be serialized + + Examples: + >>> convert_value_to_struct("hello") # -> Struct with string value + >>> convert_value_to_struct(42) # -> Struct with number value + >>> convert_value_to_struct(True) # -> Struct with bool value + >>> convert_value_to_struct({"key": "value"}) # -> Struct with nested structure + """ + # If it's already a Struct, return as-is (backward compatibility) + if isinstance(value, Struct): + return value + + # raise an error if the value is not a dictionary + if not isinstance(value, dict) and not isinstance(value, bytes): + raise ValueError(f'Value must be a dictionary, got {type(value)}') + + from google.protobuf import json_format + + # Convert the value to a JSON-serializable format first + # Handle bytes by converting to base64 string for JSON compatibility + if isinstance(value, bytes): + import base64 + + json_value = base64.b64encode(value).decode('utf-8') + else: + json_value = value + try: + # For dict values, use ParseDict directly + struct = Struct() + json_format.ParseDict(json_value, struct) + return struct + + except (TypeError, ValueError) as e: + raise ValueError( + f'Unsupported parameter type or value: {type(value)} = {repr(value)}. ' + f'Must be JSON-serializable. Error: {e}' + ) from e + + +def convert_parameters_to_grpc_any(value: Any) -> GrpcAny: + """Convert a raw Python value to a GrpcAny protobuf message. This function automatically detects the type of the input value and wraps it in the appropriate protobuf wrapper type before packing it into GrpcAny. - Args: value: Raw Python value (str, int, float, bool, bytes, or already GrpcAny) - Returns: GrpcAny: The value wrapped in a GrpcAny protobuf message - Raises: ValueError: If the value type is not supported - Examples: >>> convert_parameter_value("hello") # -> GrpcAny containing StringValue >>> convert_parameter_value(42) # -> GrpcAny containing Int64Value @@ -160,24 +210,22 @@ def convert_parameter_value(value: Any) -> GrpcAny: elif isinstance(value, bytes): any_pb.Pack(BytesValue(value=value)) else: - raise ValueError(f"Unsupported parameter type: {type(value)}. " - f"Supported types: str, int, float, bool, bytes, GrpcAny") + raise ValueError( + f'Unsupported parameter type: {type(value)}. ' + f'Supported types: str, int, float, bool, bytes, GrpcAny' + ) return any_pb def convert_parameters(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAny]: """Convert a dictionary of raw Python values to GrpcAny parameters. - This function takes a dictionary with raw Python values and converts each value to the appropriate GrpcAny protobuf message for use in Dapr API calls. - Args: parameters: Optional dictionary of parameter names to raw Python values - Returns: Dictionary of parameter names to GrpcAny values - Examples: >>> convert_parameters({"temperature": 0.7, "max_tokens": 1000, "stream": False}) >>> # Returns: {"temperature": GrpcAny, "max_tokens": GrpcAny, "stream": GrpcAny} @@ -187,6 +235,207 @@ def convert_parameters(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAn converted = {} for key, value in parameters.items(): - converted[key] = convert_parameter_value(value) + converted[key] = convert_parameters_to_grpc_any(value) return converted + + +def convert_json_schema_to_grpc_any(schema: Dict[str, Any]) -> GrpcAny: + """Convert a parameter schema dictionary to a GrpcAny object. + + This helper converts JSON schema objects to GrpcAny objects using + google.protobuf.struct_pb2.Struct for proper JSON representation. + + Args: + schema: Parameter schema dictionary (JSON schema object) + + Returns: + GrpcAny object containing the JSON schema as a Struct + + Examples: + >>> schema = { + ... "type": "object", + ... "properties": { + ... "location": {"type": "string", "description": "City name"} + ... }, + ... "required": ["location"] + ... } + >>> convert_json_schema_to_grpc_any(schema) + """ + if not isinstance(schema, dict): + raise ValueError(f'Schema must be a dictionary, got {type(schema)}') + + # Use Struct to represent the JSON schema object + struct = Struct() + struct.update(schema) + + any_pb = GrpcAny() + any_pb.Pack(struct) + return any_pb + + +def create_tool_function( + name: str, + description: str, + parameters: Optional[Dict[str, Any]] = None, + required: Optional[List[str]] = None, +) -> 'ConversationToolsFunction': + """Create a tool function with automatic parameter conversion. + + This helper automatically converts Python dictionaries to the proper JSON Schema + format required by the Dapr Conversation API. + + The parameters map directly represents the JSON schema structure using proper protobuf types. + + Args: + name: Function name + description: Human-readable description of what the function does + parameters: Parameter definitions (raw Python dict) + required: List of required parameter names + + Returns: + ConversationToolsFunction ready to use with Alpha2 API + + Examples: + # Simple approach - properties become JSON schema + >>> create_tool_function( + ... name="get_weather", + ... description="Get current weather", + ... parameters={ + ... "location": {"type": "string", "description": "City name"}, + ... "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + ... }, + ... required=["location"] + ... ) + # Creates: parameters = { + # "type": StringValue("object"), + # "properties": Struct({...}), + # "required": ListValue([...]) + # } + + # Full JSON Schema approach (already complete) + >>> create_tool_function( + ... name="calculate", + ... description="Perform calculations", + ... parameters={ + ... "type": "object", + ... "properties": { + ... "expression": {"type": "string", "description": "Math expression"} + ... }, + ... "required": ["expression"] + ... } + ... ) + # Maps schema fields directly to protobuf map + + # No parameters + >>> create_tool_function( + ... name="get_time", + ... description="Get current time" + ... ) + # Creates: parameters = {} + """ + from dapr.clients.grpc._request import ConversationToolsFunction + + if parameters is None: + # No parameters - simple case + return ConversationToolsFunction(name=name, description=description, parameters={}) + + # Build the complete JSON Schema object + if isinstance(parameters, dict): + # Check if it's already a complete JSON schema + if 'type' in parameters and parameters['type'] == 'object': + # Complete JSON schema provided - use as-is + json_schema = parameters.copy() + # Add required field if provided separately and not already present + if required and 'required' not in json_schema: + json_schema['required'] = required + elif 'properties' in parameters: + # Properties provided directly - wrap in JSON schema + json_schema = {'type': 'object', 'properties': parameters['properties']} + if required: + json_schema['required'] = required + elif 'required' in parameters: + json_schema['required'] = parameters['required'] + else: + # Assume it's a direct mapping of parameter names to schemas + json_schema = {'type': 'object', 'properties': parameters} + if required: + json_schema['required'] = required + else: + raise ValueError(f'Parameters must be a dictionary, got {type(parameters)}') + + # Map JSON schema fields directly to protobuf map entries + # The parameters map directly represents the JSON schema structure + converted_params = {} + if json_schema: + for key, value in json_schema.items(): + converted_params[key] = convert_value_to_struct(value) + + return ConversationToolsFunction( + name=name, description=description, parameters=converted_params + ) + + +def create_tool( + name: str, + description: str, + parameters: Optional[Dict[str, Any]] = None, + required: Optional[List[str]] = None, +) -> 'ConversationTools': + """Create a complete tool with automatic parameter conversion. + + Args: + name: Function name + description: Human-readable description of what the function does + parameters: JSON schema for function parameters (raw Python dict) + required: List of required parameter names + + Returns: + ConversationTools ready to use with converse_alpha2() + + Examples: + # Weather tool + >>> weather_tool = create_tool( + ... name="get_weather", + ... description="Get current weather for a location", + ... parameters={ + ... "location": { + ... "type": "string", + ... "description": "The city and state or country" + ... }, + ... "unit": { + ... "type": "string", + ... "enum": ["celsius", "fahrenheit"], + ... "description": "Temperature unit" + ... } + ... }, + ... required=["location"] + ... ) + + # Calculator tool with full schema + >>> calc_tool = create_tool( + ... name="calculate", + ... description="Perform mathematical calculations", + ... parameters={ + ... "type": "object", + ... "properties": { + ... "expression": { + ... "type": "string", + ... "description": "Mathematical expression to evaluate" + ... } + ... }, + ... "required": ["expression"] + ... } + ... ) + + # Simple tool with no parameters + >>> time_tool = create_tool( + ... name="get_current_time", + ... description="Get the current date and time" + ... ) + """ + from dapr.clients.grpc._request import ConversationTools + + function = create_tool_function(name, description, parameters, required) + + return ConversationTools(function=function) diff --git a/dapr/clients/grpc/_request.py b/dapr/clients/grpc/_request.py index f6fe3854..c479812c 100644 --- a/dapr/clients/grpc/_request.py +++ b/dapr/clients/grpc/_request.py @@ -22,7 +22,7 @@ from google.protobuf.message import Message as GrpcMessage from dapr.proto import api_v1, common_v1 -from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE +from dapr.clients._constants import DEFAULT_JSON_CONTENT_TYPE from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc._helpers import ( MetadataDict, @@ -527,7 +527,15 @@ class ConversationToolsFunction: name: str description: Optional[str] = None - parameters: Optional[Dict[str, GrpcAny]] = None + parameters: Optional[Dict] = None + + def schema_as_dict(self) -> Dict: + """Return the function's schema as a dictionary. + + Returns: + Dict: The JSON schema for the function parameters. + """ + return self.parameters or {} @dataclass diff --git a/dapr/clients/grpc/_response.py b/dapr/clients/grpc/_response.py index cbaf16b3..9c885933 100644 --- a/dapr/clients/grpc/_response.py +++ b/dapr/clients/grpc/_response.py @@ -40,7 +40,7 @@ from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage -from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE +from dapr.clients._constants import DEFAULT_JSON_CONTENT_TYPE from dapr.clients.grpc._helpers import ( MetadataDict, MetadataTuple, diff --git a/dapr/clients/grpc/_schema_helpers.py b/dapr/clients/grpc/_schema_helpers.py new file mode 100644 index 00000000..dfcf5d47 --- /dev/null +++ b/dapr/clients/grpc/_schema_helpers.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- + +""" +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 inspect +from dataclasses import fields, is_dataclass +from enum import Enum +from typing import Any, Dict, Optional, Union, get_args, get_origin, get_type_hints + +""" +Schema Helpers for Dapr Conversation API. + +This module provides function-to-JSON-schema helpers that automatically +convert typed Python functions to tools for the Conversation API. +""" + + +def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[str, Any]: + """Convert a Python type hint to JSON schema format. + + Args: + python_type: The Python type to convert + field_name: The name of the field (for better error messages) + + Returns: + Dict representing the JSON schema for this type + + Examples: + >>> python_type_to_json_schema(str) + {"type": "string"} + >>> python_type_to_json_schema(Optional[int]) + {"type": "integer"} + >>> python_type_to_json_schema(List[str]) + {"type": "array", "items": {"type": "string"}} + """ + # Handle None type + if python_type is type(None): + return {'type': 'null'} + + # Get the origin type for generic types (List, Dict, Union, etc.) + origin = get_origin(python_type) + args = get_args(python_type) + + # Handle Union types (including Optional which is Union[T, None]) + if origin is Union: + # Check if this is Optional[T] (Union[T, None]) + non_none_args = [arg for arg in args if arg is not type(None)] + if len(non_none_args) == 1 and type(None) in args: + # This is Optional[T], convert T + return python_type_to_json_schema(non_none_args[0], field_name) + else: + # This is a true Union, use anyOf + return {'anyOf': [python_type_to_json_schema(arg, field_name) for arg in args]} + + # Handle List types + if origin is list or python_type is list: + if args: + return { + 'type': 'array', + 'items': python_type_to_json_schema(args[0], f'{field_name}[]'), + } + else: + return {'type': 'array'} + + # Handle Dict types + if origin is dict or python_type is dict: + schema = {'type': 'object'} + if args and len(args) == 2: + # Dict[str, ValueType] - add additionalProperties + key_type, value_type = args + if key_type is str: + schema['additionalProperties'] = python_type_to_json_schema( + value_type, f'{field_name}.*' + ) + return schema + + # Handle basic types + if python_type is str: + return {'type': 'string'} + elif python_type is int: + return {'type': 'integer'} + elif python_type is float: + return {'type': 'number'} + elif python_type is bool: + return {'type': 'boolean'} + elif python_type is bytes: + return {'type': 'string', 'format': 'byte'} + + # Handle Enum types + if inspect.isclass(python_type) and issubclass(python_type, Enum): + return {'type': 'string', 'enum': [item.value for item in python_type]} + + # Handle Pydantic models (if available) + if hasattr(python_type, 'model_json_schema'): + try: + return python_type.model_json_schema() + except Exception: + pass + elif hasattr(python_type, 'schema'): + try: + return python_type.schema() + except Exception: + pass + + # Handle dataclasses + if is_dataclass(python_type): + from dataclasses import MISSING + + schema = {'type': 'object', 'properties': {}, 'required': []} + + for field in fields(python_type): + field_schema = python_type_to_json_schema(field.type, field.name) + schema['properties'][field.name] = field_schema + + # Check if field has no default (required) - use MISSING for dataclasses + if field.default is MISSING: + schema['required'].append(field.name) + + return schema + + # Fallback for unknown types + return {'type': 'string', 'description': f'Unknown type: {python_type}'} + + +def extract_docstring_info(func) -> Dict[str, str]: + """Extract parameter descriptions from function docstring. + + Supports Google-style, NumPy-style, and Sphinx-style docstrings. + + Args: + func: The function to analyze + + Returns: + Dict mapping parameter names to their descriptions + """ + docstring = inspect.getdoc(func) + if not docstring: + return {} + + param_descriptions = {} + + # Simple regex-based extraction for common docstring formats + lines = docstring.split('\n') + in_args_section = False + current_param = None + + for line in lines: + line = line.strip() + + # Detect Args/Parameters section + if line.lower() in ('args:', 'arguments:', 'parameters:', 'params:'): + in_args_section = True + continue + + # Exit args section on new section + if in_args_section and line.endswith(':') and not line.startswith(' '): + in_args_section = False + continue + + if in_args_section and line: + # Look for parameter definitions (contains colon and doesn't look like a continuation) + if ':' in line and not line.startswith(' '): + parts = line.split(':', 1) + if len(parts) == 2: + param_name = parts[0].strip() + description = parts[1].strip() + # Handle type annotations like "param_name (type): description" + if '(' in param_name and ')' in param_name: + param_name = param_name.split('(')[0].strip() + param_descriptions[param_name] = description + current_param = param_name + elif current_param: + # Continuation of previous parameter description + param_descriptions[current_param] += ' ' + line.strip() + + return param_descriptions + + +def function_to_json_schema( + func, name: Optional[str] = None, description: Optional[str] = None +) -> Dict[str, Any]: + """Convert a Python function to a JSON schema for tool calling. + + Args: + func: The Python function to convert + name: Override the function name (defaults to func.__name__) + description: Override the function description (defaults to first line of docstring) + + Returns: + Complete JSON schema with properties and required fields + + Examples: + >>> def get_weather(location: str, unit: str = "fahrenheit") -> str: + ... '''Get weather for a location. + ... + ... Args: + ... location: The city name + ... unit: Temperature unit (celsius or fahrenheit) + ... ''' + ... pass + >>> schema = function_to_json_schema(get_weather) + >>> schema["properties"]["location"]["type"] + 'string' + """ + # Get function signature and type hints + sig = inspect.signature(func) + type_hints = get_type_hints(func) + + # Extract parameter descriptions from docstring + param_descriptions = extract_docstring_info(func) + + # Get function description + if description is None: + docstring = inspect.getdoc(func) + if docstring: + # Use first line of docstring as description + description = docstring.split('\n')[0].strip() + else: + description = f'Function {func.__name__}' + + # Build JSON schema + schema = {'type': 'object', 'properties': {}, 'required': []} + + for param_name, param in sig.parameters.items(): + # Skip *args and **kwargs + if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD): + continue + + # Get type hint + param_type = type_hints.get(param_name, str) + + # Convert to JSON schema + param_schema = python_type_to_json_schema(param_type, param_name) + + # Add description if available + if param_name in param_descriptions: + param_schema['description'] = param_descriptions[param_name] + + schema['properties'][param_name] = param_schema + + # Check if parameter is required (no default value) + if param.default is param.empty: + schema['required'].append(param_name) + + return schema + + +def create_tool_from_function(func, name: Optional[str] = None, description: Optional[str] = None): + """Create a ConversationTools from a Python function with type hints. + + This provides the ultimate developer experience - just define a typed function + and automatically get a properly configured tool for the Conversation API. + + Args: + func: Python function with type hints + name: Override function name (defaults to func.__name__) + description: Override description (defaults to docstring) + + Returns: + ConversationTools ready to use with Alpha2 API + + Examples: + >>> def get_weather(location: str, unit: str = "fahrenheit") -> str: + ... '''Get current weather for a location. + ... + ... Args: + ... location: The city and state or country + ... unit: Temperature unit preference + ... ''' + ... return f"Weather in {location}: sunny, 72°F" + + >>> weather_tool = create_tool_from_function(get_weather) + # Now use weather_tool in your conversation API calls! + + >>> # With Pydantic models + >>> from pydantic import BaseModel + >>> class SearchQuery(BaseModel): + ... query: str + ... limit: int = 10 + ... include_images: bool = False + + >>> def web_search(params: SearchQuery) -> str: + ... '''Search the web for information.''' + ... return f"Search results for: {params.query}" + + >>> search_tool = create_tool_from_function(web_search) + + >>> # With Enums + >>> from enum import Enum + >>> class Units(Enum): + ... CELSIUS = "celsius" + ... FAHRENHEIT = "fahrenheit" + + >>> def get_temperature(city: str, unit: Units = Units.FAHRENHEIT) -> float: + ... '''Get temperature for a city.''' + ... return 72.0 + + >>> temp_tool = create_tool_from_function(get_temperature) + """ + # Import here to avoid circular imports + from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools + + # Generate JSON schema from function + json_schema = function_to_json_schema(func, name, description) + + # Use provided name or function name + tool_name = name or func.__name__ + + # Use provided description or extract from function + tool_description = description + if tool_description is None: + docstring = inspect.getdoc(func) + if docstring: + tool_description = docstring.split('\n')[0].strip() + else: + tool_description = f'Function {tool_name}' + + # Create the tool function + function = ConversationToolsFunction( + name=tool_name, description=tool_description, parameters=json_schema + ) + + # Return the complete tool + return ConversationTools(function=function) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index a389c910..33402fe3 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -40,7 +40,7 @@ from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._state import StateOptions, StateItem -from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus +from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus, validateNotBlankString, validateNotNone from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc.subscription import Subscription, StreamInactiveError from dapr.clients.grpc.interceptors import DaprClientInterceptor, DaprClientTimeoutInterceptor @@ -53,14 +53,10 @@ from dapr.version import __version__ from dapr.clients.grpc._helpers import ( - MetadataDict, MetadataTuple, to_bytes, - to_str, - tuple_to_dict, - unpack, - WorkflowRuntimeStatus, convert_parameters, + convert_value_to_struct, ) from dapr.conf.helpers import GrpcEndpoint from dapr.clients.grpc._request import ( @@ -73,13 +69,7 @@ ConversationInputAlpha2, ConversationMessage, ConversationMessageContent, - ConversationMessageOfDeveloper, - ConversationMessageOfSystem, - ConversationMessageOfUser, - ConversationMessageOfAssistant, - ConversationMessageOfTool, ConversationTools, - ConversationToolsFunction, ConversationToolCalls, ConversationToolCallsOfFunction, ) @@ -1804,7 +1794,7 @@ def converse_alpha2( inputs: List[ConversationInputAlpha2], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, Any]] = None, + parameters: Optional[Dict[str, GrpcAny | Any]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, @@ -1835,7 +1825,9 @@ def _convert_message_content(content_list: List[ConversationMessageContent]): """Convert message content list to proto format.""" if not content_list: return [] - return [api_v1.ConversationMessageContent(text=content.text) for content in content_list] + return [ + api_v1.ConversationMessageContent(text=content.text) for content in content_list + ] def _convert_tool_calls(tool_calls: List[ConversationToolCalls]): """Convert tool calls to proto format.""" @@ -1858,22 +1850,34 @@ def _convert_message(message: ConversationMessage): if message.of_developer: proto_message.of_developer.name = message.of_developer.name or '' - proto_message.of_developer.content.extend(_convert_message_content(message.of_developer.content or [])) + proto_message.of_developer.content.extend( + _convert_message_content(message.of_developer.content or []) + ) elif message.of_system: proto_message.of_system.name = message.of_system.name or '' - proto_message.of_system.content.extend(_convert_message_content(message.of_system.content or [])) + proto_message.of_system.content.extend( + _convert_message_content(message.of_system.content or []) + ) elif message.of_user: proto_message.of_user.name = message.of_user.name or '' - proto_message.of_user.content.extend(_convert_message_content(message.of_user.content or [])) + proto_message.of_user.content.extend( + _convert_message_content(message.of_user.content or []) + ) elif message.of_assistant: proto_message.of_assistant.name = message.of_assistant.name or '' - proto_message.of_assistant.content.extend(_convert_message_content(message.of_assistant.content or [])) - proto_message.of_assistant.tool_calls.extend(_convert_tool_calls(message.of_assistant.tool_calls or [])) + proto_message.of_assistant.content.extend( + _convert_message_content(message.of_assistant.content or []) + ) + proto_message.of_assistant.tool_calls.extend( + _convert_tool_calls(message.of_assistant.tool_calls or []) + ) elif message.of_tool: if message.of_tool.tool_id: proto_message.of_tool.tool_id = message.of_tool.tool_id proto_message.of_tool.name = message.of_tool.name - proto_message.of_tool.content.extend(_convert_message_content(message.of_tool.content or [])) + proto_message.of_tool.content.extend( + _convert_message_content(message.of_tool.content or []) + ) return proto_message @@ -1899,8 +1903,10 @@ def _convert_message(message: ConversationMessage): if tool.function.description: proto_tool.function.description = tool.function.description if tool.function.parameters: - for key, value in tool.function.parameters.items(): - proto_tool.function.parameters[key].CopyFrom(value) + # we only keep type, properties and required + proto_tool.function.parameters.CopyFrom( + convert_value_to_struct(tool.function.parameters) + ) tools_pb.append(proto_tool) # Convert raw Python parameters to GrpcAny objects @@ -1936,24 +1942,25 @@ def _convert_message(message: ConversationMessage): tool_calls = [] for tool_call in choice.message.tool_calls: function_call = ConversationToolCallsOfFunction( - name=tool_call.function.name, - arguments=tool_call.function.arguments + name=tool_call.function.name, arguments=tool_call.function.arguments + ) + tool_calls.append( + ConversationToolCalls( + id=tool_call.id if tool_call.id else None, function=function_call + ) ) - tool_calls.append(ConversationToolCalls( - id=tool_call.id if tool_call.id else None, - function=function_call - )) result_message = ConversationResultMessage( - content=choice.message.content, - tool_calls=tool_calls + content=choice.message.content, tool_calls=tool_calls ) - choices.append(ConversationResultChoices( - finish_reason=choice.finish_reason, - index=choice.index, - message=result_message - )) + choices.append( + ConversationResultChoices( + finish_reason=choice.finish_reason, + index=choice.index, + message=result_message, + ) + ) outputs.append(ConversationResultAlpha2(choices=choices)) diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 5944e278..86e9ab6f 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -30,7 +30,7 @@ from dapr.serializers import Serializer from dapr.conf import settings -from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE +from dapr.clients._constants import DEFAULT_JSON_CONTENT_TYPE from dapr.clients.exceptions import DaprHttpError, DaprInternalError diff --git a/dapr/proto/common/v1/common_pb2.py b/dapr/proto/common/v1/common_pb2.py index 21ef9de3..7f8feb46 100644 --- a/dapr/proto/common/v1/common_pb2.py +++ b/dapr/proto/common/v1/common_pb2.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/common/v1/common.proto -# Protobuf Python Version: 6.31.1 +# Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'dapr/proto/common/v1/common.proto' -) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -31,12 +21,12 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dapr.proto.common.v1.common_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\nio.dapr.v1B\014CommonProtosZ/github.com/dapr/dapr/pkg/proto/common/v1;common\252\002\033Dapr.Client.Autogen.Grpc.v1' - _globals['_STATEITEM_METADATAENTRY']._loaded_options = None + _globals['_STATEITEM_METADATAENTRY']._options = None _globals['_STATEITEM_METADATAENTRY']._serialized_options = b'8\001' - _globals['_CONFIGURATIONITEM_METADATAENTRY']._loaded_options = None + _globals['_CONFIGURATIONITEM_METADATAENTRY']._options = None _globals['_CONFIGURATIONITEM_METADATAENTRY']._serialized_options = b'8\001' _globals['_HTTPEXTENSION']._serialized_start=119 _globals['_HTTPEXTENSION']._serialized_end=327 diff --git a/dapr/proto/common/v1/common_pb2_grpc.py b/dapr/proto/common/v1/common_pb2_grpc.py index 5d08ae56..2daafffe 100644 --- a/dapr/proto/common/v1/common_pb2_grpc.py +++ b/dapr/proto/common/v1/common_pb2_grpc.py @@ -1,24 +1,4 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc -import warnings - -GRPC_GENERATED_VERSION = '1.74.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in dapr/proto/common/v1/common_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/dapr/proto/runtime/v1/appcallback_pb2.py b/dapr/proto/runtime/v1/appcallback_pb2.py index eb8fdea4..6773f762 100644 --- a/dapr/proto/runtime/v1/appcallback_pb2.py +++ b/dapr/proto/runtime/v1/appcallback_pb2.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/runtime/v1/appcallback.proto -# Protobuf Python Version: 6.31.1 +# Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'dapr/proto/runtime/v1/appcallback.proto' -) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -33,16 +23,16 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dapr.proto.runtime.v1.appcallback_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\nio.dapr.v1B\025DaprAppCallbackProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\252\002 Dapr.AppCallback.Autogen.Grpc.v1' - _globals['_TOPICEVENTBULKREQUESTENTRY_METADATAENTRY']._loaded_options = None + _globals['_TOPICEVENTBULKREQUESTENTRY_METADATAENTRY']._options = None _globals['_TOPICEVENTBULKREQUESTENTRY_METADATAENTRY']._serialized_options = b'8\001' - _globals['_TOPICEVENTBULKREQUEST_METADATAENTRY']._loaded_options = None + _globals['_TOPICEVENTBULKREQUEST_METADATAENTRY']._options = None _globals['_TOPICEVENTBULKREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BINDINGEVENTREQUEST_METADATAENTRY']._loaded_options = None + _globals['_BINDINGEVENTREQUEST_METADATAENTRY']._options = None _globals['_BINDINGEVENTREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_TOPICSUBSCRIPTION_METADATAENTRY']._loaded_options = None + _globals['_TOPICSUBSCRIPTION_METADATAENTRY']._options = None _globals['_TOPICSUBSCRIPTION_METADATAENTRY']._serialized_options = b'8\001' _globals['_JOBEVENTREQUEST']._serialized_start=188 _globals['_JOBEVENTREQUEST']._serialized_end=354 diff --git a/dapr/proto/runtime/v1/appcallback_pb2_grpc.py b/dapr/proto/runtime/v1/appcallback_pb2_grpc.py index 3d8afd6c..b203f7db 100644 --- a/dapr/proto/runtime/v1/appcallback_pb2_grpc.py +++ b/dapr/proto/runtime/v1/appcallback_pb2_grpc.py @@ -1,31 +1,11 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc -import warnings from dapr.proto.common.v1 import common_pb2 as dapr_dot_proto_dot_common_dot_v1_dot_common__pb2 from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -GRPC_GENERATED_VERSION = '1.74.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in dapr/proto/runtime/v1/appcallback_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - class AppCallbackStub(object): """AppCallback V1 allows user application to interact with Dapr runtime. @@ -43,27 +23,27 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.AppCallback/OnInvoke', request_serializer=dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - _registered_method=True) + ) self.ListTopicSubscriptions = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/ListTopicSubscriptions', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListTopicSubscriptionsResponse.FromString, - _registered_method=True) + ) self.OnTopicEvent = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/OnTopicEvent', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventResponse.FromString, - _registered_method=True) + ) self.ListInputBindings = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/ListInputBindings', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListInputBindingsResponse.FromString, - _registered_method=True) + ) self.OnBindingEvent = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallback/OnBindingEvent', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventResponse.FromString, - _registered_method=True) + ) class AppCallbackServicer(object): @@ -142,7 +122,6 @@ def add_AppCallbackServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallback', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallback', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -163,21 +142,11 @@ def OnInvoke(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallback/OnInvoke', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/OnInvoke', dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeRequest.SerializeToString, dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ListTopicSubscriptions(request, @@ -190,21 +159,11 @@ def ListTopicSubscriptions(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallback/ListTopicSubscriptions', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/ListTopicSubscriptions', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListTopicSubscriptionsResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def OnTopicEvent(request, @@ -217,21 +176,11 @@ def OnTopicEvent(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallback/OnTopicEvent', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/OnTopicEvent', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ListInputBindings(request, @@ -244,21 +193,11 @@ def ListInputBindings(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallback/ListInputBindings', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/ListInputBindings', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.ListInputBindingsResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def OnBindingEvent(request, @@ -271,21 +210,11 @@ def OnBindingEvent(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallback/OnBindingEvent', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallback/OnBindingEvent', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.BindingEventResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) class AppCallbackHealthCheckStub(object): @@ -303,7 +232,7 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.AppCallbackHealthCheck/HealthCheck', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.HealthCheckResponse.FromString, - _registered_method=True) + ) class AppCallbackHealthCheckServicer(object): @@ -330,7 +259,6 @@ def add_AppCallbackHealthCheckServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallbackHealthCheck', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallbackHealthCheck', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -350,21 +278,11 @@ def HealthCheck(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallbackHealthCheck/HealthCheck', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallbackHealthCheck/HealthCheck', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.HealthCheckResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) class AppCallbackAlphaStub(object): @@ -382,12 +300,12 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.AppCallbackAlpha/OnBulkTopicEventAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkResponse.FromString, - _registered_method=True) + ) self.OnJobEventAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.AppCallbackAlpha/OnJobEventAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventResponse.FromString, - _registered_method=True) + ) class AppCallbackAlphaServicer(object): @@ -426,7 +344,6 @@ def add_AppCallbackAlphaServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.AppCallbackAlpha', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('dapr.proto.runtime.v1.AppCallbackAlpha', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -446,21 +363,11 @@ def OnBulkTopicEventAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallbackAlpha/OnBulkTopicEventAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallbackAlpha/OnBulkTopicEventAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.TopicEventBulkResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def OnJobEventAlpha1(request, @@ -473,18 +380,8 @@ def OnJobEventAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.AppCallbackAlpha/OnJobEventAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.AppCallbackAlpha/OnJobEventAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2.JobEventResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/dapr/proto/runtime/v1/dapr_pb2.py b/dapr/proto/runtime/v1/dapr_pb2.py index ec2d728b..1a7b7b7e 100644 --- a/dapr/proto/runtime/v1/dapr_pb2.py +++ b/dapr/proto/runtime/v1/dapr_pb2.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE # source: dapr/proto/runtime/v1/dapr.proto -# Protobuf Python Version: 6.31.1 +# Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'dapr/proto/runtime/v1/dapr.proto' -) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -25,438 +15,435 @@ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 from dapr.proto.common.v1 import common_pb2 as dapr_dot_proto_dot_common_dot_v1_dot_common__pb2 from dapr.proto.runtime.v1 import appcallback_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_appcallback__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n dapr/proto/runtime/v1/dapr.proto\x12\x15\x64\x61pr.proto.runtime.v1\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!dapr/proto/common/v1/common.proto\x1a\'dapr/proto/runtime/v1/appcallback.proto\"X\n\x14InvokeServiceRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x34\n\x07message\x18\x03 \x01(\x0b\x32#.dapr.proto.common.v1.InvokeRequest\"\xf5\x01\n\x0fGetStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12H\n\x0b\x63onsistency\x18\x03 \x01(\x0e\x32\x33.dapr.proto.common.v1.StateOptions.StateConsistency\x12\x46\n\x08metadata\x18\x04 \x03(\x0b\x32\x34.dapr.proto.runtime.v1.GetStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc9\x01\n\x13GetBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12\x13\n\x0bparallelism\x18\x03 \x01(\x05\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.GetBulkStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x14GetBulkStateResponse\x12\x33\n\x05items\x18\x01 \x03(\x0b\x32$.dapr.proto.runtime.v1.BulkStateItem\"\xbe\x01\n\rBulkStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x44\n\x08metadata\x18\x05 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.BulkStateItem.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa8\x01\n\x10GetStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\x12\x44\x65leteStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12(\n\x04\x65tag\x18\x03 \x01(\x0b\x32\x1a.dapr.proto.common.v1.Etag\x12\x33\n\x07options\x18\x04 \x01(\x0b\x32\".dapr.proto.common.v1.StateOptions\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.DeleteStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x16\x44\x65leteBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"W\n\x10SaveStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\xbc\x01\n\x11QueryStateRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\r\n\x05query\x18\x02 \x01(\t\x12H\n\x08metadata\x18\x03 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.QueryStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"H\n\x0eQueryStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd7\x01\n\x12QueryStateResponse\x12\x36\n\x07results\x18\x01 \x03(\x0b\x32%.dapr.proto.runtime.v1.QueryStateItem\x12\r\n\x05token\x18\x02 \x01(\t\x12I\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.QueryStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xdf\x01\n\x13PublishEventRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x11\x64\x61ta_content_type\x18\x04 \x01(\t\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.PublishEventRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf5\x01\n\x12\x42ulkPublishRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12?\n\x07\x65ntries\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.BulkPublishRequestEntry\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.BulkPublishRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd1\x01\n\x17\x42ulkPublishRequestEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12N\n\x08metadata\x18\x04 \x03(\x0b\x32<.dapr.proto.runtime.v1.BulkPublishRequestEntry.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"c\n\x13\x42ulkPublishResponse\x12L\n\rfailedEntries\x18\x01 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.BulkPublishResponseFailedEntry\"A\n\x1e\x42ulkPublishResponseFailedEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x84\x02\n!SubscribeTopicEventsRequestAlpha1\x12Z\n\x0finitial_request\x18\x01 \x01(\x0b\x32?.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1H\x00\x12\\\n\x0f\x65vent_processed\x18\x02 \x01(\x0b\x32\x41.dapr.proto.runtime.v1.SubscribeTopicEventsRequestProcessedAlpha1H\x00\x42%\n#subscribe_topic_events_request_type\"\x96\x02\n(SubscribeTopicEventsRequestInitialAlpha1\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12_\n\x08metadata\x18\x03 \x03(\x0b\x32M.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1.MetadataEntry\x12\x1e\n\x11\x64\x65\x61\x64_letter_topic\x18\x04 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x14\n\x12_dead_letter_topic\"s\n*SubscribeTopicEventsRequestProcessedAlpha1\x12\n\n\x02id\x18\x01 \x01(\t\x12\x39\n\x06status\x18\x02 \x01(\x0b\x32).dapr.proto.runtime.v1.TopicEventResponse\"\xed\x01\n\"SubscribeTopicEventsResponseAlpha1\x12\\\n\x10initial_response\x18\x01 \x01(\x0b\x32@.dapr.proto.runtime.v1.SubscribeTopicEventsResponseInitialAlpha1H\x00\x12\x41\n\revent_message\x18\x02 \x01(\x0b\x32(.dapr.proto.runtime.v1.TopicEventRequestH\x00\x42&\n$subscribe_topic_events_response_type\"+\n)SubscribeTopicEventsResponseInitialAlpha1\"\xc3\x01\n\x14InvokeBindingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12K\n\x08metadata\x18\x03 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.InvokeBindingRequest.MetadataEntry\x12\x11\n\toperation\x18\x04 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa4\x01\n\x15InvokeBindingResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.InvokeBindingResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb8\x01\n\x10GetSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x0b\n\x03key\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x11GetSecretResponse\x12@\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.GetSecretResponse.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb3\x01\n\x14GetBulkSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12K\n\x08metadata\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.GetBulkSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x0eSecretResponse\x12\x43\n\x07secrets\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.SecretResponse.SecretsEntry\x1a.\n\x0cSecretsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb1\x01\n\x15GetBulkSecretResponse\x12\x44\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.GetBulkSecretResponse.DataEntry\x1aR\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x34\n\x05value\x18\x02 \x01(\x0b\x32%.dapr.proto.runtime.v1.SecretResponse:\x02\x38\x01\"f\n\x1bTransactionalStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x30\n\x07request\x18\x02 \x01(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\x83\x02\n\x1e\x45xecuteStateTransactionRequest\x12\x11\n\tstoreName\x18\x01 \x01(\t\x12\x46\n\noperations\x18\x02 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.TransactionalStateOperation\x12U\n\x08metadata\x18\x03 \x03(\x0b\x32\x43.dapr.proto.runtime.v1.ExecuteStateTransactionRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x01\n\x19RegisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x06 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\x0b\n\x03ttl\x18\x08 \x01(\t\"e\n\x1bUnregisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xac\x01\n\x1cRegisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x0b\n\x03ttl\x18\x07 \x01(\t\"h\n\x1eUnregisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"]\n\x14GetActorStateRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0b\n\x03key\x18\x03 \x01(\t\"\xa4\x01\n\x15GetActorStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetActorStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n#ExecuteActorStateTransactionRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12K\n\noperations\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.TransactionalActorStateOperation\"\xf5\x01\n TransactionalActorStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12W\n\x08metadata\x18\x04 \x03(\x0b\x32\x45.dapr.proto.runtime.v1.TransactionalActorStateOperation.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe8\x01\n\x12InvokeActorRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.InvokeActorRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"#\n\x13InvokeActorResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x14\n\x12GetMetadataRequest\"\xf6\x06\n\x13GetMetadataResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12Q\n\x13\x61\x63tive_actors_count\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountB\x02\x18\x01R\x06\x61\x63tors\x12V\n\x15registered_components\x18\x03 \x03(\x0b\x32+.dapr.proto.runtime.v1.RegisteredComponentsR\ncomponents\x12\x65\n\x11\x65xtended_metadata\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.GetMetadataResponse.ExtendedMetadataEntryR\x08\x65xtended\x12O\n\rsubscriptions\x18\x05 \x03(\x0b\x32).dapr.proto.runtime.v1.PubsubSubscriptionR\rsubscriptions\x12R\n\x0ehttp_endpoints\x18\x06 \x03(\x0b\x32+.dapr.proto.runtime.v1.MetadataHTTPEndpointR\rhttpEndpoints\x12j\n\x19\x61pp_connection_properties\x18\x07 \x01(\x0b\x32..dapr.proto.runtime.v1.AppConnectionPropertiesR\x17\x61ppConnectionProperties\x12\'\n\x0fruntime_version\x18\x08 \x01(\tR\x0eruntimeVersion\x12)\n\x10\x65nabled_features\x18\t \x03(\tR\x0f\x65nabledFeatures\x12H\n\ractor_runtime\x18\n \x01(\x0b\x32#.dapr.proto.runtime.v1.ActorRuntimeR\x0c\x61\x63torRuntime\x12K\n\tscheduler\x18\x0b \x01(\x0b\x32(.dapr.proto.runtime.v1.MetadataSchedulerH\x00R\tscheduler\x88\x01\x01\x1a\x37\n\x15\x45xtendedMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_scheduler\"0\n\x11MetadataScheduler\x12\x1b\n\x13\x63onnected_addresses\x18\x01 \x03(\t\"\xbc\x02\n\x0c\x41\x63torRuntime\x12]\n\x0eruntime_status\x18\x01 \x01(\x0e\x32\x36.dapr.proto.runtime.v1.ActorRuntime.ActorRuntimeStatusR\rruntimeStatus\x12M\n\ractive_actors\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountR\x0c\x61\x63tiveActors\x12\x1d\n\nhost_ready\x18\x03 \x01(\x08R\thostReady\x12\x1c\n\tplacement\x18\x04 \x01(\tR\tplacement\"A\n\x12\x41\x63torRuntimeStatus\x12\x10\n\x0cINITIALIZING\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x0b\n\x07RUNNING\x10\x02\"0\n\x11\x41\x63tiveActorsCount\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"Y\n\x14RegisteredComponents\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\"*\n\x14MetadataHTTPEndpoint\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xd1\x01\n\x17\x41ppConnectionProperties\x12\x0c\n\x04port\x18\x01 \x01(\x05\x12\x10\n\x08protocol\x18\x02 \x01(\t\x12\'\n\x0f\x63hannel_address\x18\x03 \x01(\tR\x0e\x63hannelAddress\x12\'\n\x0fmax_concurrency\x18\x04 \x01(\x05R\x0emaxConcurrency\x12\x44\n\x06health\x18\x05 \x01(\x0b\x32\x34.dapr.proto.runtime.v1.AppConnectionHealthProperties\"\xdc\x01\n\x1d\x41ppConnectionHealthProperties\x12*\n\x11health_check_path\x18\x01 \x01(\tR\x0fhealthCheckPath\x12\x32\n\x15health_probe_interval\x18\x02 \x01(\tR\x13healthProbeInterval\x12\x30\n\x14health_probe_timeout\x18\x03 \x01(\tR\x12healthProbeTimeout\x12)\n\x10health_threshold\x18\x04 \x01(\x05R\x0fhealthThreshold\"\x86\x03\n\x12PubsubSubscription\x12\x1f\n\x0bpubsub_name\x18\x01 \x01(\tR\npubsubname\x12\x14\n\x05topic\x18\x02 \x01(\tR\x05topic\x12S\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.PubsubSubscription.MetadataEntryR\x08metadata\x12\x44\n\x05rules\x18\x04 \x01(\x0b\x32..dapr.proto.runtime.v1.PubsubSubscriptionRulesR\x05rules\x12*\n\x11\x64\x65\x61\x64_letter_topic\x18\x05 \x01(\tR\x0f\x64\x65\x61\x64LetterTopic\x12\x41\n\x04type\x18\x06 \x01(\x0e\x32-.dapr.proto.runtime.v1.PubsubSubscriptionTypeR\x04type\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"W\n\x17PubsubSubscriptionRules\x12<\n\x05rules\x18\x01 \x03(\x0b\x32-.dapr.proto.runtime.v1.PubsubSubscriptionRule\"5\n\x16PubsubSubscriptionRule\x12\r\n\x05match\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"0\n\x12SetMetadataRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xbc\x01\n\x17GetConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12N\n\x08metadata\x18\x03 \x03(\x0b\x32<.dapr.proto.runtime.v1.GetConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\x18GetConfigurationResponse\x12I\n\x05items\x18\x01 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"\xc8\x01\n\x1dSubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12T\n\x08metadata\x18\x03 \x03(\x0b\x32\x42.dapr.proto.runtime.v1.SubscribeConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"A\n\x1fUnsubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\"\xd4\x01\n\x1eSubscribeConfigurationResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12O\n\x05items\x18\x02 \x03(\x0b\x32@.dapr.proto.runtime.v1.SubscribeConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"?\n UnsubscribeConfigurationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x9b\x01\n\x0eTryLockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\x12*\n\x11\x65xpiry_in_seconds\x18\x04 \x01(\x05R\x0f\x65xpiryInSeconds\"\"\n\x0fTryLockResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"n\n\rUnlockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\"\xae\x01\n\x0eUnlockResponse\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.dapr.proto.runtime.v1.UnlockResponse.Status\"^\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\x17\n\x13LOCK_DOES_NOT_EXIST\x10\x01\x12\x1a\n\x16LOCK_BELONGS_TO_OTHERS\x10\x02\x12\x12\n\x0eINTERNAL_ERROR\x10\x03\"\xb0\x01\n\x13SubtleGetKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x34.dapr.proto.runtime.v1.SubtleGetKeyRequest.KeyFormat\"\x1e\n\tKeyFormat\x12\x07\n\x03PEM\x10\x00\x12\x08\n\x04JSON\x10\x01\"C\n\x14SubtleGetKeyResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\npublic_key\x18\x02 \x01(\tR\tpublicKey\"\xb6\x01\n\x14SubtleEncryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x11\n\tplaintext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"8\n\x15SubtleEncryptResponse\x12\x12\n\nciphertext\x18\x01 \x01(\x0c\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xc4\x01\n\x14SubtleDecryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\"*\n\x15SubtleDecryptResponse\x12\x11\n\tplaintext\x18\x01 \x01(\x0c\"\xc8\x01\n\x14SubtleWrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12#\n\rplaintext_key\x18\x02 \x01(\x0cR\x0cplaintextKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"E\n\x15SubtleWrapKeyResponse\x12\x1f\n\x0bwrapped_key\x18\x01 \x01(\x0cR\nwrappedKey\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xd3\x01\n\x16SubtleUnwrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x1f\n\x0bwrapped_key\x18\x02 \x01(\x0cR\nwrappedKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\">\n\x17SubtleUnwrapKeyResponse\x12#\n\rplaintext_key\x18\x01 \x01(\x0cR\x0cplaintextKey\"x\n\x11SubtleSignRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\"\'\n\x12SubtleSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x8d\x01\n\x13SubtleVerifyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"%\n\x14SubtleVerifyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x85\x01\n\x0e\x45ncryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.EncryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\xfe\x01\n\x15\x45ncryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x02 \x01(\tR\x07keyName\x12\x1a\n\x12key_wrap_algorithm\x18\x03 \x01(\t\x12\x1e\n\x16\x64\x61ta_encryption_cipher\x18\n \x01(\t\x12\x37\n\x18omit_decryption_key_name\x18\x0b \x01(\x08R\x15omitDecryptionKeyName\x12.\n\x13\x64\x65\x63ryption_key_name\x18\x0c \x01(\tR\x11\x64\x65\x63ryptionKeyName\"G\n\x0f\x45ncryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\x85\x01\n\x0e\x44\x65\x63ryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.DecryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"Y\n\x15\x44\x65\x63ryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x0c \x01(\tR\x07keyName\"G\n\x0f\x44\x65\x63ryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"d\n\x12GetWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x84\x03\n\x13GetWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12#\n\rworkflow_name\x18\x02 \x01(\tR\x0cworkflowName\x12\x39\n\ncreated_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x42\n\x0flast_updated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\rlastUpdatedAt\x12%\n\x0eruntime_status\x18\x05 \x01(\tR\rruntimeStatus\x12N\n\nproperties\x18\x06 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetWorkflowResponse.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x02\n\x14StartWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12#\n\rworkflow_name\x18\x03 \x01(\tR\x0cworkflowName\x12I\n\x07options\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.StartWorkflowRequest.OptionsEntry\x12\r\n\x05input\x18\x05 \x01(\x0c\x1a.\n\x0cOptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x15StartWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\"j\n\x18TerminateWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"f\n\x14PauseWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"g\n\x15ResumeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x9e\x01\n\x19RaiseEventWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12\x1d\n\nevent_name\x18\x03 \x01(\tR\teventName\x12\x12\n\nevent_data\x18\x04 \x01(\x0c\"f\n\x14PurgeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x11\n\x0fShutdownRequest\"\xed\x02\n\x03Job\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n\x08schedule\x18\x02 \x01(\tH\x00R\x08schedule\x88\x01\x01\x12\x1d\n\x07repeats\x18\x03 \x01(\rH\x01R\x07repeats\x88\x01\x01\x12\x1e\n\x08\x64ue_time\x18\x04 \x01(\tH\x02R\x07\x64ueTime\x88\x01\x01\x12\x15\n\x03ttl\x18\x05 \x01(\tH\x03R\x03ttl\x88\x01\x01\x12(\n\x04\x64\x61ta\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyR\x04\x64\x61ta\x12\x1c\n\toverwrite\x18\x07 \x01(\x08R\toverwrite\x12R\n\x0e\x66\x61ilure_policy\x18\x08 \x01(\x0b\x32&.dapr.proto.common.v1.JobFailurePolicyH\x04R\rfailurePolicy\x88\x01\x01\x42\x0b\n\t_scheduleB\n\n\x08_repeatsB\x0b\n\t_due_timeB\x06\n\x04_ttlB\x11\n\x0f_failure_policy\"=\n\x12ScheduleJobRequest\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\"\x15\n\x13ScheduleJobResponse\"\x1d\n\rGetJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x0eGetJobResponse\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\" \n\x10\x44\x65leteJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteJobResponse\"\xeb\x03\n\x13\x43onversationRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\tcontextID\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x38\n\x06inputs\x18\x03 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationInput\x12N\n\nparameters\x18\x04 \x03(\x0b\x32:.dapr.proto.runtime.v1.ConversationRequest.ParametersEntry\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.ConversationRequest.MetadataEntry\x12\x15\n\x08scrubPII\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01:\x02\x18\x01\x42\x0c\n\n_contextIDB\x0b\n\t_scrubPIIB\x0e\n\x0c_temperature\"\xe6\x04\n\x19\x43onversationRequestAlpha2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\ncontext_id\x18\x02 \x01(\tH\x00\x88\x01\x01\x12>\n\x06inputs\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.ConversationInputAlpha2\x12T\n\nparameters\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.ConversationRequestAlpha2.ParametersEntry\x12P\n\x08metadata\x18\x05 \x03(\x0b\x32>.dapr.proto.runtime.v1.ConversationRequestAlpha2.MetadataEntry\x12\x16\n\tscrub_pii\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x12\x37\n\x05tools\x18\x08 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationTools\x12\x18\n\x0btool_choice\x18\t \x01(\tH\x03\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_context_idB\x0c\n\n_scrub_piiB\x0e\n\x0c_temperatureB\x0e\n\x0c_tool_choice\"h\n\x11\x43onversationInput\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x11\n\x04role\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08scrubPII\x18\x03 \x01(\x08H\x01\x88\x01\x01:\x02\x18\x01\x42\x07\n\x05_roleB\x0b\n\t_scrubPII\"}\n\x17\x43onversationInputAlpha2\x12<\n\x08messages\x18\x01 \x03(\x0b\x32*.dapr.proto.runtime.v1.ConversationMessage\x12\x16\n\tscrub_pii\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\x0c\n\n_scrub_pii\"\x97\x03\n\x13\x43onversationMessage\x12M\n\x0cof_developer\x18\x01 \x01(\x0b\x32\x35.dapr.proto.runtime.v1.ConversationMessageOfDeveloperH\x00\x12G\n\tof_system\x18\x02 \x01(\x0b\x32\x32.dapr.proto.runtime.v1.ConversationMessageOfSystemH\x00\x12\x43\n\x07of_user\x18\x03 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationMessageOfUserH\x00\x12M\n\x0cof_assistant\x18\x04 \x01(\x0b\x32\x35.dapr.proto.runtime.v1.ConversationMessageOfAssistantH\x00\x12\x43\n\x07of_tool\x18\x05 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationMessageOfToolH\x00\x42\x0f\n\rmessage_types\"\x80\x01\n\x1e\x43onversationMessageOfDeveloper\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"}\n\x1b\x43onversationMessageOfSystem\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"{\n\x19\x43onversationMessageOfUser\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"\xc2\x01\n\x1e\x43onversationMessageOfAssistant\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContent\x12@\n\ntool_calls\x18\x03 \x03(\x0b\x32,.dapr.proto.runtime.v1.ConversationToolCallsB\x07\n\x05_name\"\x8f\x01\n\x19\x43onversationMessageOfTool\x12\x14\n\x07tool_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x42\n\x07\x63ontent\x18\x03 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\n\n\x08_tool_id\"\x89\x01\n\x15\x43onversationToolCalls\x12\x0f\n\x02id\x18\x01 \x01(\tH\x01\x88\x01\x01\x12J\n\x08\x66unction\x18\x02 \x01(\x0b\x32\x36.dapr.proto.runtime.v1.ConversationToolCallsOfFunctionH\x00\x42\x0c\n\ntool_typesB\x05\n\x03_id\"B\n\x1f\x43onversationToolCallsOfFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\targuments\x18\x02 \x01(\t\"*\n\x1a\x43onversationMessageContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"\xc0\x01\n\x12\x43onversationResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12M\n\nparameters\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.ConversationResult.ParametersEntry\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01:\x02\x18\x01\"]\n\x18\x43onversationResultAlpha2\x12\x41\n\x07\x63hoices\x18\x01 \x03(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationResultChoices\"\x84\x01\n\x19\x43onversationResultChoices\x12\x15\n\rfinish_reason\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\x03\x12\x41\n\x07message\x18\x03 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationResultMessage\"n\n\x19\x43onversationResultMessage\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12@\n\ntool_calls\x18\x02 \x03(\x0b\x32,.dapr.proto.runtime.v1.ConversationToolCalls\"|\n\x14\x43onversationResponse\x12\x16\n\tcontextID\x18\x01 \x01(\tH\x00\x88\x01\x01\x12:\n\x07outputs\x18\x02 \x03(\x0b\x32).dapr.proto.runtime.v1.ConversationResult:\x02\x18\x01\x42\x0c\n\n_contextID\"\x86\x01\n\x1a\x43onversationResponseAlpha2\x12\x17\n\ncontext_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12@\n\x07outputs\x18\x02 \x03(\x0b\x32/.dapr.proto.runtime.v1.ConversationResultAlpha2B\r\n\x0b_context_id\"g\n\x11\x43onversationTools\x12\x44\n\x08\x66unction\x18\x01 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationToolsFunctionH\x00\x42\x0c\n\ntool_types\"\xf2\x01\n\x19\x43onversationToolsFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x12T\n\nparameters\x18\x03 \x03(\x0b\x32@.dapr.proto.runtime.v1.ConversationToolsFunction.ParametersEntry\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x42\x0e\n\x0c_description*W\n\x16PubsubSubscriptionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x44\x45\x43LARATIVE\x10\x01\x12\x10\n\x0cPROGRAMMATIC\x10\x02\x12\r\n\tSTREAMING\x10\x03\x32\xb7\x32\n\x04\x44\x61pr\x12\x64\n\rInvokeService\x12+.dapr.proto.runtime.v1.InvokeServiceRequest\x1a$.dapr.proto.common.v1.InvokeResponse\"\x00\x12]\n\x08GetState\x12&.dapr.proto.runtime.v1.GetStateRequest\x1a\'.dapr.proto.runtime.v1.GetStateResponse\"\x00\x12i\n\x0cGetBulkState\x12*.dapr.proto.runtime.v1.GetBulkStateRequest\x1a+.dapr.proto.runtime.v1.GetBulkStateResponse\"\x00\x12N\n\tSaveState\x12\'.dapr.proto.runtime.v1.SaveStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12i\n\x10QueryStateAlpha1\x12(.dapr.proto.runtime.v1.QueryStateRequest\x1a).dapr.proto.runtime.v1.QueryStateResponse\"\x00\x12R\n\x0b\x44\x65leteState\x12).dapr.proto.runtime.v1.DeleteStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x0f\x44\x65leteBulkState\x12-.dapr.proto.runtime.v1.DeleteBulkStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17\x45xecuteStateTransaction\x12\x35.dapr.proto.runtime.v1.ExecuteStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n\x0cPublishEvent\x12*.dapr.proto.runtime.v1.PublishEventRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x16\x42ulkPublishEventAlpha1\x12).dapr.proto.runtime.v1.BulkPublishRequest\x1a*.dapr.proto.runtime.v1.BulkPublishResponse\"\x00\x12\x97\x01\n\x1aSubscribeTopicEventsAlpha1\x12\x38.dapr.proto.runtime.v1.SubscribeTopicEventsRequestAlpha1\x1a\x39.dapr.proto.runtime.v1.SubscribeTopicEventsResponseAlpha1\"\x00(\x01\x30\x01\x12l\n\rInvokeBinding\x12+.dapr.proto.runtime.v1.InvokeBindingRequest\x1a,.dapr.proto.runtime.v1.InvokeBindingResponse\"\x00\x12`\n\tGetSecret\x12\'.dapr.proto.runtime.v1.GetSecretRequest\x1a(.dapr.proto.runtime.v1.GetSecretResponse\"\x00\x12l\n\rGetBulkSecret\x12+.dapr.proto.runtime.v1.GetBulkSecretRequest\x1a,.dapr.proto.runtime.v1.GetBulkSecretResponse\"\x00\x12`\n\x12RegisterActorTimer\x12\x30.dapr.proto.runtime.v1.RegisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x14UnregisterActorTimer\x12\x32.dapr.proto.runtime.v1.UnregisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x15RegisterActorReminder\x12\x33.dapr.proto.runtime.v1.RegisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17UnregisterActorReminder\x12\x35.dapr.proto.runtime.v1.UnregisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\rGetActorState\x12+.dapr.proto.runtime.v1.GetActorStateRequest\x1a,.dapr.proto.runtime.v1.GetActorStateResponse\"\x00\x12t\n\x1c\x45xecuteActorStateTransaction\x12:.dapr.proto.runtime.v1.ExecuteActorStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bInvokeActor\x12).dapr.proto.runtime.v1.InvokeActorRequest\x1a*.dapr.proto.runtime.v1.InvokeActorResponse\"\x00\x12{\n\x16GetConfigurationAlpha1\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12u\n\x10GetConfiguration\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12\x8f\x01\n\x1cSubscribeConfigurationAlpha1\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x89\x01\n\x16SubscribeConfiguration\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x93\x01\n\x1eUnsubscribeConfigurationAlpha1\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12\x8d\x01\n\x18UnsubscribeConfiguration\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12`\n\rTryLockAlpha1\x12%.dapr.proto.runtime.v1.TryLockRequest\x1a&.dapr.proto.runtime.v1.TryLockResponse\"\x00\x12]\n\x0cUnlockAlpha1\x12$.dapr.proto.runtime.v1.UnlockRequest\x1a%.dapr.proto.runtime.v1.UnlockResponse\"\x00\x12\x62\n\rEncryptAlpha1\x12%.dapr.proto.runtime.v1.EncryptRequest\x1a&.dapr.proto.runtime.v1.EncryptResponse(\x01\x30\x01\x12\x62\n\rDecryptAlpha1\x12%.dapr.proto.runtime.v1.DecryptRequest\x1a&.dapr.proto.runtime.v1.DecryptResponse(\x01\x30\x01\x12\x66\n\x0bGetMetadata\x12).dapr.proto.runtime.v1.GetMetadataRequest\x1a*.dapr.proto.runtime.v1.GetMetadataResponse\"\x00\x12R\n\x0bSetMetadata\x12).dapr.proto.runtime.v1.SetMetadataRequest\x1a\x16.google.protobuf.Empty\"\x00\x12m\n\x12SubtleGetKeyAlpha1\x12*.dapr.proto.runtime.v1.SubtleGetKeyRequest\x1a+.dapr.proto.runtime.v1.SubtleGetKeyResponse\x12p\n\x13SubtleEncryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleEncryptRequest\x1a,.dapr.proto.runtime.v1.SubtleEncryptResponse\x12p\n\x13SubtleDecryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleDecryptRequest\x1a,.dapr.proto.runtime.v1.SubtleDecryptResponse\x12p\n\x13SubtleWrapKeyAlpha1\x12+.dapr.proto.runtime.v1.SubtleWrapKeyRequest\x1a,.dapr.proto.runtime.v1.SubtleWrapKeyResponse\x12v\n\x15SubtleUnwrapKeyAlpha1\x12-.dapr.proto.runtime.v1.SubtleUnwrapKeyRequest\x1a..dapr.proto.runtime.v1.SubtleUnwrapKeyResponse\x12g\n\x10SubtleSignAlpha1\x12(.dapr.proto.runtime.v1.SubtleSignRequest\x1a).dapr.proto.runtime.v1.SubtleSignResponse\x12m\n\x12SubtleVerifyAlpha1\x12*.dapr.proto.runtime.v1.SubtleVerifyRequest\x1a+.dapr.proto.runtime.v1.SubtleVerifyResponse\x12u\n\x13StartWorkflowAlpha1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x03\x88\x02\x01\x12o\n\x11GetWorkflowAlpha1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x03\x88\x02\x01\x12_\n\x13PurgeWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12g\n\x17TerminateWorkflowAlpha1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12_\n\x13PauseWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12\x61\n\x14ResumeWorkflowAlpha1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12i\n\x18RaiseEventWorkflowAlpha1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12q\n\x12StartWorkflowBeta1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12k\n\x10GetWorkflowBeta1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12[\n\x12PurgeWorkflowBeta1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x63\n\x16TerminateWorkflowBeta1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x12PauseWorkflowBeta1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x13ResumeWorkflowBeta1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x17RaiseEventWorkflowBeta1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12L\n\x08Shutdown\x12&.dapr.proto.runtime.v1.ShutdownRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\x11ScheduleJobAlpha1\x12).dapr.proto.runtime.v1.ScheduleJobRequest\x1a*.dapr.proto.runtime.v1.ScheduleJobResponse\"\x00\x12]\n\x0cGetJobAlpha1\x12$.dapr.proto.runtime.v1.GetJobRequest\x1a%.dapr.proto.runtime.v1.GetJobResponse\"\x00\x12\x66\n\x0f\x44\x65leteJobAlpha1\x12\'.dapr.proto.runtime.v1.DeleteJobRequest\x1a(.dapr.proto.runtime.v1.DeleteJobResponse\"\x00\x12k\n\x0e\x43onverseAlpha1\x12*.dapr.proto.runtime.v1.ConversationRequest\x1a+.dapr.proto.runtime.v1.ConversationResponse\"\x00\x12w\n\x0e\x43onverseAlpha2\x12\x30.dapr.proto.runtime.v1.ConversationRequestAlpha2\x1a\x31.dapr.proto.runtime.v1.ConversationResponseAlpha2\"\x00\x42i\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\xaa\x02\x1b\x44\x61pr.Client.Autogen.Grpc.v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n dapr/proto/runtime/v1/dapr.proto\x12\x15\x64\x61pr.proto.runtime.v1\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a!dapr/proto/common/v1/common.proto\x1a\'dapr/proto/runtime/v1/appcallback.proto\"X\n\x14InvokeServiceRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x34\n\x07message\x18\x03 \x01(\x0b\x32#.dapr.proto.common.v1.InvokeRequest\"\xf5\x01\n\x0fGetStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12H\n\x0b\x63onsistency\x18\x03 \x01(\x0e\x32\x33.dapr.proto.common.v1.StateOptions.StateConsistency\x12\x46\n\x08metadata\x18\x04 \x03(\x0b\x32\x34.dapr.proto.runtime.v1.GetStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc9\x01\n\x13GetBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12\x13\n\x0bparallelism\x18\x03 \x01(\x05\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.GetBulkStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"K\n\x14GetBulkStateResponse\x12\x33\n\x05items\x18\x01 \x03(\x0b\x32$.dapr.proto.runtime.v1.BulkStateItem\"\xbe\x01\n\rBulkStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x44\n\x08metadata\x18\x05 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.BulkStateItem.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa8\x01\n\x10GetStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\x12\x44\x65leteStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12(\n\x04\x65tag\x18\x03 \x01(\x0b\x32\x1a.dapr.proto.common.v1.Etag\x12\x33\n\x07options\x18\x04 \x01(\x0b\x32\".dapr.proto.common.v1.StateOptions\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.DeleteStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x16\x44\x65leteBulkStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"W\n\x10SaveStateRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12/\n\x06states\x18\x02 \x03(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\xbc\x01\n\x11QueryStateRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\r\n\x05query\x18\x02 \x01(\t\x12H\n\x08metadata\x18\x03 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.QueryStateRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"H\n\x0eQueryStateItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04\x65tag\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd7\x01\n\x12QueryStateResponse\x12\x36\n\x07results\x18\x01 \x03(\x0b\x32%.dapr.proto.runtime.v1.QueryStateItem\x12\r\n\x05token\x18\x02 \x01(\t\x12I\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.QueryStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xdf\x01\n\x13PublishEventRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x11\x64\x61ta_content_type\x18\x04 \x01(\t\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.PublishEventRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf5\x01\n\x12\x42ulkPublishRequest\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12?\n\x07\x65ntries\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.BulkPublishRequestEntry\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.BulkPublishRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd1\x01\n\x17\x42ulkPublishRequestEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65vent\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12N\n\x08metadata\x18\x04 \x03(\x0b\x32<.dapr.proto.runtime.v1.BulkPublishRequestEntry.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"c\n\x13\x42ulkPublishResponse\x12L\n\rfailedEntries\x18\x01 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.BulkPublishResponseFailedEntry\"A\n\x1e\x42ulkPublishResponseFailedEntry\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x84\x02\n!SubscribeTopicEventsRequestAlpha1\x12Z\n\x0finitial_request\x18\x01 \x01(\x0b\x32?.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1H\x00\x12\\\n\x0f\x65vent_processed\x18\x02 \x01(\x0b\x32\x41.dapr.proto.runtime.v1.SubscribeTopicEventsRequestProcessedAlpha1H\x00\x42%\n#subscribe_topic_events_request_type\"\x96\x02\n(SubscribeTopicEventsRequestInitialAlpha1\x12\x13\n\x0bpubsub_name\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12_\n\x08metadata\x18\x03 \x03(\x0b\x32M.dapr.proto.runtime.v1.SubscribeTopicEventsRequestInitialAlpha1.MetadataEntry\x12\x1e\n\x11\x64\x65\x61\x64_letter_topic\x18\x04 \x01(\tH\x00\x88\x01\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x14\n\x12_dead_letter_topic\"s\n*SubscribeTopicEventsRequestProcessedAlpha1\x12\n\n\x02id\x18\x01 \x01(\t\x12\x39\n\x06status\x18\x02 \x01(\x0b\x32).dapr.proto.runtime.v1.TopicEventResponse\"\xed\x01\n\"SubscribeTopicEventsResponseAlpha1\x12\\\n\x10initial_response\x18\x01 \x01(\x0b\x32@.dapr.proto.runtime.v1.SubscribeTopicEventsResponseInitialAlpha1H\x00\x12\x41\n\revent_message\x18\x02 \x01(\x0b\x32(.dapr.proto.runtime.v1.TopicEventRequestH\x00\x42&\n$subscribe_topic_events_response_type\"+\n)SubscribeTopicEventsResponseInitialAlpha1\"\xc3\x01\n\x14InvokeBindingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12K\n\x08metadata\x18\x03 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.InvokeBindingRequest.MetadataEntry\x12\x11\n\toperation\x18\x04 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa4\x01\n\x15InvokeBindingResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.InvokeBindingResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb8\x01\n\x10GetSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x0b\n\x03key\x18\x02 \x01(\t\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.dapr.proto.runtime.v1.GetSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x11GetSecretResponse\x12@\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.GetSecretResponse.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb3\x01\n\x14GetBulkSecretRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12K\n\x08metadata\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.GetBulkSecretRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x0eSecretResponse\x12\x43\n\x07secrets\x18\x01 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.SecretResponse.SecretsEntry\x1a.\n\x0cSecretsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb1\x01\n\x15GetBulkSecretResponse\x12\x44\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x36.dapr.proto.runtime.v1.GetBulkSecretResponse.DataEntry\x1aR\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x34\n\x05value\x18\x02 \x01(\x0b\x32%.dapr.proto.runtime.v1.SecretResponse:\x02\x38\x01\"f\n\x1bTransactionalStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x30\n\x07request\x18\x02 \x01(\x0b\x32\x1f.dapr.proto.common.v1.StateItem\"\x83\x02\n\x1e\x45xecuteStateTransactionRequest\x12\x11\n\tstoreName\x18\x01 \x01(\t\x12\x46\n\noperations\x18\x02 \x03(\x0b\x32\x32.dapr.proto.runtime.v1.TransactionalStateOperation\x12U\n\x08metadata\x18\x03 \x03(\x0b\x32\x43.dapr.proto.runtime.v1.ExecuteStateTransactionRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbb\x01\n\x19RegisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61llback\x18\x06 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\x0b\n\x03ttl\x18\x08 \x01(\t\"e\n\x1bUnregisterActorTimerRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"\xac\x01\n\x1cRegisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x19\n\x08\x64ue_time\x18\x04 \x01(\tR\x07\x64ueTime\x12\x0e\n\x06period\x18\x05 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x0b\n\x03ttl\x18\x07 \x01(\t\"h\n\x1eUnregisterActorReminderRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0c\n\x04name\x18\x03 \x01(\t\"]\n\x14GetActorStateRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0b\n\x03key\x18\x03 \x01(\t\"\xa4\x01\n\x15GetActorStateResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12L\n\x08metadata\x18\x02 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetActorStateResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n#ExecuteActorStateTransactionRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12K\n\noperations\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.TransactionalActorStateOperation\"\xf5\x01\n TransactionalActorStateOperation\x12\x15\n\roperationType\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12W\n\x08metadata\x18\x04 \x03(\x0b\x32\x45.dapr.proto.runtime.v1.TransactionalActorStateOperation.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe8\x01\n\x12InvokeActorRequest\x12\x1d\n\nactor_type\x18\x01 \x01(\tR\tactorType\x12\x19\n\x08\x61\x63tor_id\x18\x02 \x01(\tR\x07\x61\x63torId\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12I\n\x08metadata\x18\x05 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.InvokeActorRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"#\n\x13InvokeActorResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x14\n\x12GetMetadataRequest\"\xf6\x06\n\x13GetMetadataResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12Q\n\x13\x61\x63tive_actors_count\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountB\x02\x18\x01R\x06\x61\x63tors\x12V\n\x15registered_components\x18\x03 \x03(\x0b\x32+.dapr.proto.runtime.v1.RegisteredComponentsR\ncomponents\x12\x65\n\x11\x65xtended_metadata\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.GetMetadataResponse.ExtendedMetadataEntryR\x08\x65xtended\x12O\n\rsubscriptions\x18\x05 \x03(\x0b\x32).dapr.proto.runtime.v1.PubsubSubscriptionR\rsubscriptions\x12R\n\x0ehttp_endpoints\x18\x06 \x03(\x0b\x32+.dapr.proto.runtime.v1.MetadataHTTPEndpointR\rhttpEndpoints\x12j\n\x19\x61pp_connection_properties\x18\x07 \x01(\x0b\x32..dapr.proto.runtime.v1.AppConnectionPropertiesR\x17\x61ppConnectionProperties\x12\'\n\x0fruntime_version\x18\x08 \x01(\tR\x0eruntimeVersion\x12)\n\x10\x65nabled_features\x18\t \x03(\tR\x0f\x65nabledFeatures\x12H\n\ractor_runtime\x18\n \x01(\x0b\x32#.dapr.proto.runtime.v1.ActorRuntimeR\x0c\x61\x63torRuntime\x12K\n\tscheduler\x18\x0b \x01(\x0b\x32(.dapr.proto.runtime.v1.MetadataSchedulerH\x00R\tscheduler\x88\x01\x01\x1a\x37\n\x15\x45xtendedMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_scheduler\"0\n\x11MetadataScheduler\x12\x1b\n\x13\x63onnected_addresses\x18\x01 \x03(\t\"\xbc\x02\n\x0c\x41\x63torRuntime\x12]\n\x0eruntime_status\x18\x01 \x01(\x0e\x32\x36.dapr.proto.runtime.v1.ActorRuntime.ActorRuntimeStatusR\rruntimeStatus\x12M\n\ractive_actors\x18\x02 \x03(\x0b\x32(.dapr.proto.runtime.v1.ActiveActorsCountR\x0c\x61\x63tiveActors\x12\x1d\n\nhost_ready\x18\x03 \x01(\x08R\thostReady\x12\x1c\n\tplacement\x18\x04 \x01(\tR\tplacement\"A\n\x12\x41\x63torRuntimeStatus\x12\x10\n\x0cINITIALIZING\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x0b\n\x07RUNNING\x10\x02\"0\n\x11\x41\x63tiveActorsCount\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"Y\n\x14RegisteredComponents\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\"*\n\x14MetadataHTTPEndpoint\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xd1\x01\n\x17\x41ppConnectionProperties\x12\x0c\n\x04port\x18\x01 \x01(\x05\x12\x10\n\x08protocol\x18\x02 \x01(\t\x12\'\n\x0f\x63hannel_address\x18\x03 \x01(\tR\x0e\x63hannelAddress\x12\'\n\x0fmax_concurrency\x18\x04 \x01(\x05R\x0emaxConcurrency\x12\x44\n\x06health\x18\x05 \x01(\x0b\x32\x34.dapr.proto.runtime.v1.AppConnectionHealthProperties\"\xdc\x01\n\x1d\x41ppConnectionHealthProperties\x12*\n\x11health_check_path\x18\x01 \x01(\tR\x0fhealthCheckPath\x12\x32\n\x15health_probe_interval\x18\x02 \x01(\tR\x13healthProbeInterval\x12\x30\n\x14health_probe_timeout\x18\x03 \x01(\tR\x12healthProbeTimeout\x12)\n\x10health_threshold\x18\x04 \x01(\x05R\x0fhealthThreshold\"\x86\x03\n\x12PubsubSubscription\x12\x1f\n\x0bpubsub_name\x18\x01 \x01(\tR\npubsubname\x12\x14\n\x05topic\x18\x02 \x01(\tR\x05topic\x12S\n\x08metadata\x18\x03 \x03(\x0b\x32\x37.dapr.proto.runtime.v1.PubsubSubscription.MetadataEntryR\x08metadata\x12\x44\n\x05rules\x18\x04 \x01(\x0b\x32..dapr.proto.runtime.v1.PubsubSubscriptionRulesR\x05rules\x12*\n\x11\x64\x65\x61\x64_letter_topic\x18\x05 \x01(\tR\x0f\x64\x65\x61\x64LetterTopic\x12\x41\n\x04type\x18\x06 \x01(\x0e\x32-.dapr.proto.runtime.v1.PubsubSubscriptionTypeR\x04type\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"W\n\x17PubsubSubscriptionRules\x12<\n\x05rules\x18\x01 \x03(\x0b\x32-.dapr.proto.runtime.v1.PubsubSubscriptionRule\"5\n\x16PubsubSubscriptionRule\x12\r\n\x05match\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"0\n\x12SetMetadataRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xbc\x01\n\x17GetConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12N\n\x08metadata\x18\x03 \x03(\x0b\x32<.dapr.proto.runtime.v1.GetConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\x18GetConfigurationResponse\x12I\n\x05items\x18\x01 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"\xc8\x01\n\x1dSubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\t\x12T\n\x08metadata\x18\x03 \x03(\x0b\x32\x42.dapr.proto.runtime.v1.SubscribeConfigurationRequest.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"A\n\x1fUnsubscribeConfigurationRequest\x12\x12\n\nstore_name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t\"\xd4\x01\n\x1eSubscribeConfigurationResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12O\n\x05items\x18\x02 \x03(\x0b\x32@.dapr.proto.runtime.v1.SubscribeConfigurationResponse.ItemsEntry\x1aU\n\nItemsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.dapr.proto.common.v1.ConfigurationItem:\x02\x38\x01\"?\n UnsubscribeConfigurationResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x9b\x01\n\x0eTryLockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\x12*\n\x11\x65xpiry_in_seconds\x18\x04 \x01(\x05R\x0f\x65xpiryInSeconds\"\"\n\x0fTryLockResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"n\n\rUnlockRequest\x12\x1d\n\nstore_name\x18\x01 \x01(\tR\tstoreName\x12\x1f\n\x0bresource_id\x18\x02 \x01(\tR\nresourceId\x12\x1d\n\nlock_owner\x18\x03 \x01(\tR\tlockOwner\"\xae\x01\n\x0eUnlockResponse\x12<\n\x06status\x18\x01 \x01(\x0e\x32,.dapr.proto.runtime.v1.UnlockResponse.Status\"^\n\x06Status\x12\x0b\n\x07SUCCESS\x10\x00\x12\x17\n\x13LOCK_DOES_NOT_EXIST\x10\x01\x12\x1a\n\x16LOCK_BELONGS_TO_OTHERS\x10\x02\x12\x12\n\x0eINTERNAL_ERROR\x10\x03\"\xb0\x01\n\x13SubtleGetKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x44\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x34.dapr.proto.runtime.v1.SubtleGetKeyRequest.KeyFormat\"\x1e\n\tKeyFormat\x12\x07\n\x03PEM\x10\x00\x12\x08\n\x04JSON\x10\x01\"C\n\x14SubtleGetKeyResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1d\n\npublic_key\x18\x02 \x01(\tR\tpublicKey\"\xb6\x01\n\x14SubtleEncryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x11\n\tplaintext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"8\n\x15SubtleEncryptResponse\x12\x12\n\nciphertext\x18\x01 \x01(\x0c\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xc4\x01\n\x14SubtleDecryptRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x12\n\nciphertext\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\"*\n\x15SubtleDecryptResponse\x12\x11\n\tplaintext\x18\x01 \x01(\x0c\"\xc8\x01\n\x14SubtleWrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12#\n\rplaintext_key\x18\x02 \x01(\x0cR\x0cplaintextKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x06 \x01(\x0cR\x0e\x61ssociatedData\"E\n\x15SubtleWrapKeyResponse\x12\x1f\n\x0bwrapped_key\x18\x01 \x01(\x0cR\nwrappedKey\x12\x0b\n\x03tag\x18\x02 \x01(\x0c\"\xd3\x01\n\x16SubtleUnwrapKeyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x1f\n\x0bwrapped_key\x18\x02 \x01(\x0cR\nwrappedKey\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\r\n\x05nonce\x18\x05 \x01(\x0c\x12\x0b\n\x03tag\x18\x06 \x01(\x0c\x12\'\n\x0f\x61ssociated_data\x18\x07 \x01(\x0cR\x0e\x61ssociatedData\">\n\x17SubtleUnwrapKeyResponse\x12#\n\rplaintext_key\x18\x01 \x01(\x0cR\x0cplaintextKey\"x\n\x11SubtleSignRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\"\'\n\x12SubtleSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x8d\x01\n\x13SubtleVerifyRequest\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x0e\n\x06\x64igest\x18\x02 \x01(\x0c\x12\x11\n\talgorithm\x18\x03 \x01(\t\x12\x19\n\x08key_name\x18\x04 \x01(\tR\x07keyName\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"%\n\x14SubtleVerifyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x85\x01\n\x0e\x45ncryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.EncryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\xfe\x01\n\x15\x45ncryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x02 \x01(\tR\x07keyName\x12\x1a\n\x12key_wrap_algorithm\x18\x03 \x01(\t\x12\x1e\n\x16\x64\x61ta_encryption_cipher\x18\n \x01(\t\x12\x37\n\x18omit_decryption_key_name\x18\x0b \x01(\x08R\x15omitDecryptionKeyName\x12.\n\x13\x64\x65\x63ryption_key_name\x18\x0c \x01(\tR\x11\x64\x65\x63ryptionKeyName\"G\n\x0f\x45ncryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"\x85\x01\n\x0e\x44\x65\x63ryptRequest\x12=\n\x07options\x18\x01 \x01(\x0b\x32,.dapr.proto.runtime.v1.DecryptRequestOptions\x12\x34\n\x07payload\x18\x02 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"Y\n\x15\x44\x65\x63ryptRequestOptions\x12%\n\x0e\x63omponent_name\x18\x01 \x01(\tR\rcomponentName\x12\x19\n\x08key_name\x18\x0c \x01(\tR\x07keyName\"G\n\x0f\x44\x65\x63ryptResponse\x12\x34\n\x07payload\x18\x01 \x01(\x0b\x32#.dapr.proto.common.v1.StreamPayload\"d\n\x12GetWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x84\x03\n\x13GetWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12#\n\rworkflow_name\x18\x02 \x01(\tR\x0cworkflowName\x12\x39\n\ncreated_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x42\n\x0flast_updated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\rlastUpdatedAt\x12%\n\x0eruntime_status\x18\x05 \x01(\tR\rruntimeStatus\x12N\n\nproperties\x18\x06 \x03(\x0b\x32:.dapr.proto.runtime.v1.GetWorkflowResponse.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x02\n\x14StartWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12#\n\rworkflow_name\x18\x03 \x01(\tR\x0cworkflowName\x12I\n\x07options\x18\x04 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.StartWorkflowRequest.OptionsEntry\x12\r\n\x05input\x18\x05 \x01(\x0c\x1a.\n\x0cOptionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x15StartWorkflowResponse\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\"j\n\x18TerminateWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"f\n\x14PauseWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"g\n\x15ResumeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x9e\x01\n\x19RaiseEventWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\x12\x1d\n\nevent_name\x18\x03 \x01(\tR\teventName\x12\x12\n\nevent_data\x18\x04 \x01(\x0c\"f\n\x14PurgeWorkflowRequest\x12\x1f\n\x0binstance_id\x18\x01 \x01(\tR\ninstanceID\x12-\n\x12workflow_component\x18\x02 \x01(\tR\x11workflowComponent\"\x11\n\x0fShutdownRequest\"\xed\x02\n\x03Job\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n\x08schedule\x18\x02 \x01(\tH\x00R\x08schedule\x88\x01\x01\x12\x1d\n\x07repeats\x18\x03 \x01(\rH\x01R\x07repeats\x88\x01\x01\x12\x1e\n\x08\x64ue_time\x18\x04 \x01(\tH\x02R\x07\x64ueTime\x88\x01\x01\x12\x15\n\x03ttl\x18\x05 \x01(\tH\x03R\x03ttl\x88\x01\x01\x12(\n\x04\x64\x61ta\x18\x06 \x01(\x0b\x32\x14.google.protobuf.AnyR\x04\x64\x61ta\x12\x1c\n\toverwrite\x18\x07 \x01(\x08R\toverwrite\x12R\n\x0e\x66\x61ilure_policy\x18\x08 \x01(\x0b\x32&.dapr.proto.common.v1.JobFailurePolicyH\x04R\rfailurePolicy\x88\x01\x01\x42\x0b\n\t_scheduleB\n\n\x08_repeatsB\x0b\n\t_due_timeB\x06\n\x04_ttlB\x11\n\x0f_failure_policy\"=\n\x12ScheduleJobRequest\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\"\x15\n\x13ScheduleJobResponse\"\x1d\n\rGetJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x0eGetJobResponse\x12\'\n\x03job\x18\x01 \x01(\x0b\x32\x1a.dapr.proto.runtime.v1.Job\" \n\x10\x44\x65leteJobRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteJobResponse\"\xeb\x03\n\x13\x43onversationRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\tcontextID\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x38\n\x06inputs\x18\x03 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationInput\x12N\n\nparameters\x18\x04 \x03(\x0b\x32:.dapr.proto.runtime.v1.ConversationRequest.ParametersEntry\x12J\n\x08metadata\x18\x05 \x03(\x0b\x32\x38.dapr.proto.runtime.v1.ConversationRequest.MetadataEntry\x12\x15\n\x08scrubPII\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01:\x02\x18\x01\x42\x0c\n\n_contextIDB\x0b\n\t_scrubPIIB\x0e\n\x0c_temperature\"\xe6\x04\n\x19\x43onversationRequestAlpha2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\ncontext_id\x18\x02 \x01(\tH\x00\x88\x01\x01\x12>\n\x06inputs\x18\x03 \x03(\x0b\x32..dapr.proto.runtime.v1.ConversationInputAlpha2\x12T\n\nparameters\x18\x04 \x03(\x0b\x32@.dapr.proto.runtime.v1.ConversationRequestAlpha2.ParametersEntry\x12P\n\x08metadata\x18\x05 \x03(\x0b\x32>.dapr.proto.runtime.v1.ConversationRequestAlpha2.MetadataEntry\x12\x16\n\tscrub_pii\x18\x06 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x07 \x01(\x01H\x02\x88\x01\x01\x12\x37\n\x05tools\x18\x08 \x03(\x0b\x32(.dapr.proto.runtime.v1.ConversationTools\x12\x18\n\x0btool_choice\x18\t \x01(\tH\x03\x88\x01\x01\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b_context_idB\x0c\n\n_scrub_piiB\x0e\n\x0c_temperatureB\x0e\n\x0c_tool_choice\"h\n\x11\x43onversationInput\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x11\n\x04role\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08scrubPII\x18\x03 \x01(\x08H\x01\x88\x01\x01:\x02\x18\x01\x42\x07\n\x05_roleB\x0b\n\t_scrubPII\"}\n\x17\x43onversationInputAlpha2\x12<\n\x08messages\x18\x01 \x03(\x0b\x32*.dapr.proto.runtime.v1.ConversationMessage\x12\x16\n\tscrub_pii\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\x0c\n\n_scrub_pii\"\x97\x03\n\x13\x43onversationMessage\x12M\n\x0cof_developer\x18\x01 \x01(\x0b\x32\x35.dapr.proto.runtime.v1.ConversationMessageOfDeveloperH\x00\x12G\n\tof_system\x18\x02 \x01(\x0b\x32\x32.dapr.proto.runtime.v1.ConversationMessageOfSystemH\x00\x12\x43\n\x07of_user\x18\x03 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationMessageOfUserH\x00\x12M\n\x0cof_assistant\x18\x04 \x01(\x0b\x32\x35.dapr.proto.runtime.v1.ConversationMessageOfAssistantH\x00\x12\x43\n\x07of_tool\x18\x05 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationMessageOfToolH\x00\x42\x0f\n\rmessage_types\"\x80\x01\n\x1e\x43onversationMessageOfDeveloper\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"}\n\x1b\x43onversationMessageOfSystem\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"{\n\x19\x43onversationMessageOfUser\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\x07\n\x05_name\"\xc2\x01\n\x1e\x43onversationMessageOfAssistant\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x42\n\x07\x63ontent\x18\x02 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContent\x12@\n\ntool_calls\x18\x03 \x03(\x0b\x32,.dapr.proto.runtime.v1.ConversationToolCallsB\x07\n\x05_name\"\x8f\x01\n\x19\x43onversationMessageOfTool\x12\x14\n\x07tool_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x42\n\x07\x63ontent\x18\x03 \x03(\x0b\x32\x31.dapr.proto.runtime.v1.ConversationMessageContentB\n\n\x08_tool_id\"\x89\x01\n\x15\x43onversationToolCalls\x12\x0f\n\x02id\x18\x01 \x01(\tH\x01\x88\x01\x01\x12J\n\x08\x66unction\x18\x02 \x01(\x0b\x32\x36.dapr.proto.runtime.v1.ConversationToolCallsOfFunctionH\x00\x42\x0c\n\ntool_typesB\x05\n\x03_id\"B\n\x1f\x43onversationToolCallsOfFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\targuments\x18\x02 \x01(\t\"*\n\x1a\x43onversationMessageContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"\xc0\x01\n\x12\x43onversationResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12M\n\nparameters\x18\x02 \x03(\x0b\x32\x39.dapr.proto.runtime.v1.ConversationResult.ParametersEntry\x1aG\n\x0fParametersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01:\x02\x18\x01\"]\n\x18\x43onversationResultAlpha2\x12\x41\n\x07\x63hoices\x18\x01 \x03(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationResultChoices\"\x84\x01\n\x19\x43onversationResultChoices\x12\x15\n\rfinish_reason\x18\x01 \x01(\t\x12\r\n\x05index\x18\x02 \x01(\x03\x12\x41\n\x07message\x18\x03 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationResultMessage\"n\n\x19\x43onversationResultMessage\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12@\n\ntool_calls\x18\x02 \x03(\x0b\x32,.dapr.proto.runtime.v1.ConversationToolCalls\"|\n\x14\x43onversationResponse\x12\x16\n\tcontextID\x18\x01 \x01(\tH\x00\x88\x01\x01\x12:\n\x07outputs\x18\x02 \x03(\x0b\x32).dapr.proto.runtime.v1.ConversationResult:\x02\x18\x01\x42\x0c\n\n_contextID\"\x86\x01\n\x1a\x43onversationResponseAlpha2\x12\x17\n\ncontext_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12@\n\x07outputs\x18\x02 \x03(\x0b\x32/.dapr.proto.runtime.v1.ConversationResultAlpha2B\r\n\x0b_context_id\"g\n\x11\x43onversationTools\x12\x44\n\x08\x66unction\x18\x01 \x01(\x0b\x32\x30.dapr.proto.runtime.v1.ConversationToolsFunctionH\x00\x42\x0c\n\ntool_types\"\x80\x01\n\x19\x43onversationToolsFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x12+\n\nparameters\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructB\x0e\n\x0c_description*W\n\x16PubsubSubscriptionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x44\x45\x43LARATIVE\x10\x01\x12\x10\n\x0cPROGRAMMATIC\x10\x02\x12\r\n\tSTREAMING\x10\x03\x32\xb7\x32\n\x04\x44\x61pr\x12\x64\n\rInvokeService\x12+.dapr.proto.runtime.v1.InvokeServiceRequest\x1a$.dapr.proto.common.v1.InvokeResponse\"\x00\x12]\n\x08GetState\x12&.dapr.proto.runtime.v1.GetStateRequest\x1a\'.dapr.proto.runtime.v1.GetStateResponse\"\x00\x12i\n\x0cGetBulkState\x12*.dapr.proto.runtime.v1.GetBulkStateRequest\x1a+.dapr.proto.runtime.v1.GetBulkStateResponse\"\x00\x12N\n\tSaveState\x12\'.dapr.proto.runtime.v1.SaveStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12i\n\x10QueryStateAlpha1\x12(.dapr.proto.runtime.v1.QueryStateRequest\x1a).dapr.proto.runtime.v1.QueryStateResponse\"\x00\x12R\n\x0b\x44\x65leteState\x12).dapr.proto.runtime.v1.DeleteStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x0f\x44\x65leteBulkState\x12-.dapr.proto.runtime.v1.DeleteBulkStateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17\x45xecuteStateTransaction\x12\x35.dapr.proto.runtime.v1.ExecuteStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n\x0cPublishEvent\x12*.dapr.proto.runtime.v1.PublishEventRequest\x1a\x16.google.protobuf.Empty\"\x00\x12q\n\x16\x42ulkPublishEventAlpha1\x12).dapr.proto.runtime.v1.BulkPublishRequest\x1a*.dapr.proto.runtime.v1.BulkPublishResponse\"\x00\x12\x97\x01\n\x1aSubscribeTopicEventsAlpha1\x12\x38.dapr.proto.runtime.v1.SubscribeTopicEventsRequestAlpha1\x1a\x39.dapr.proto.runtime.v1.SubscribeTopicEventsResponseAlpha1\"\x00(\x01\x30\x01\x12l\n\rInvokeBinding\x12+.dapr.proto.runtime.v1.InvokeBindingRequest\x1a,.dapr.proto.runtime.v1.InvokeBindingResponse\"\x00\x12`\n\tGetSecret\x12\'.dapr.proto.runtime.v1.GetSecretRequest\x1a(.dapr.proto.runtime.v1.GetSecretResponse\"\x00\x12l\n\rGetBulkSecret\x12+.dapr.proto.runtime.v1.GetBulkSecretRequest\x1a,.dapr.proto.runtime.v1.GetBulkSecretResponse\"\x00\x12`\n\x12RegisterActorTimer\x12\x30.dapr.proto.runtime.v1.RegisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x14UnregisterActorTimer\x12\x32.dapr.proto.runtime.v1.UnregisterActorTimerRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x15RegisterActorReminder\x12\x33.dapr.proto.runtime.v1.RegisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x17UnregisterActorReminder\x12\x35.dapr.proto.runtime.v1.UnregisterActorReminderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\rGetActorState\x12+.dapr.proto.runtime.v1.GetActorStateRequest\x1a,.dapr.proto.runtime.v1.GetActorStateResponse\"\x00\x12t\n\x1c\x45xecuteActorStateTransaction\x12:.dapr.proto.runtime.v1.ExecuteActorStateTransactionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x66\n\x0bInvokeActor\x12).dapr.proto.runtime.v1.InvokeActorRequest\x1a*.dapr.proto.runtime.v1.InvokeActorResponse\"\x00\x12{\n\x16GetConfigurationAlpha1\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12u\n\x10GetConfiguration\x12..dapr.proto.runtime.v1.GetConfigurationRequest\x1a/.dapr.proto.runtime.v1.GetConfigurationResponse\"\x00\x12\x8f\x01\n\x1cSubscribeConfigurationAlpha1\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x89\x01\n\x16SubscribeConfiguration\x12\x34.dapr.proto.runtime.v1.SubscribeConfigurationRequest\x1a\x35.dapr.proto.runtime.v1.SubscribeConfigurationResponse\"\x00\x30\x01\x12\x93\x01\n\x1eUnsubscribeConfigurationAlpha1\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12\x8d\x01\n\x18UnsubscribeConfiguration\x12\x36.dapr.proto.runtime.v1.UnsubscribeConfigurationRequest\x1a\x37.dapr.proto.runtime.v1.UnsubscribeConfigurationResponse\"\x00\x12`\n\rTryLockAlpha1\x12%.dapr.proto.runtime.v1.TryLockRequest\x1a&.dapr.proto.runtime.v1.TryLockResponse\"\x00\x12]\n\x0cUnlockAlpha1\x12$.dapr.proto.runtime.v1.UnlockRequest\x1a%.dapr.proto.runtime.v1.UnlockResponse\"\x00\x12\x62\n\rEncryptAlpha1\x12%.dapr.proto.runtime.v1.EncryptRequest\x1a&.dapr.proto.runtime.v1.EncryptResponse(\x01\x30\x01\x12\x62\n\rDecryptAlpha1\x12%.dapr.proto.runtime.v1.DecryptRequest\x1a&.dapr.proto.runtime.v1.DecryptResponse(\x01\x30\x01\x12\x66\n\x0bGetMetadata\x12).dapr.proto.runtime.v1.GetMetadataRequest\x1a*.dapr.proto.runtime.v1.GetMetadataResponse\"\x00\x12R\n\x0bSetMetadata\x12).dapr.proto.runtime.v1.SetMetadataRequest\x1a\x16.google.protobuf.Empty\"\x00\x12m\n\x12SubtleGetKeyAlpha1\x12*.dapr.proto.runtime.v1.SubtleGetKeyRequest\x1a+.dapr.proto.runtime.v1.SubtleGetKeyResponse\x12p\n\x13SubtleEncryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleEncryptRequest\x1a,.dapr.proto.runtime.v1.SubtleEncryptResponse\x12p\n\x13SubtleDecryptAlpha1\x12+.dapr.proto.runtime.v1.SubtleDecryptRequest\x1a,.dapr.proto.runtime.v1.SubtleDecryptResponse\x12p\n\x13SubtleWrapKeyAlpha1\x12+.dapr.proto.runtime.v1.SubtleWrapKeyRequest\x1a,.dapr.proto.runtime.v1.SubtleWrapKeyResponse\x12v\n\x15SubtleUnwrapKeyAlpha1\x12-.dapr.proto.runtime.v1.SubtleUnwrapKeyRequest\x1a..dapr.proto.runtime.v1.SubtleUnwrapKeyResponse\x12g\n\x10SubtleSignAlpha1\x12(.dapr.proto.runtime.v1.SubtleSignRequest\x1a).dapr.proto.runtime.v1.SubtleSignResponse\x12m\n\x12SubtleVerifyAlpha1\x12*.dapr.proto.runtime.v1.SubtleVerifyRequest\x1a+.dapr.proto.runtime.v1.SubtleVerifyResponse\x12u\n\x13StartWorkflowAlpha1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x03\x88\x02\x01\x12o\n\x11GetWorkflowAlpha1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x03\x88\x02\x01\x12_\n\x13PurgeWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12g\n\x17TerminateWorkflowAlpha1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12_\n\x13PauseWorkflowAlpha1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12\x61\n\x14ResumeWorkflowAlpha1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12i\n\x18RaiseEventWorkflowAlpha1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x03\x88\x02\x01\x12q\n\x12StartWorkflowBeta1\x12+.dapr.proto.runtime.v1.StartWorkflowRequest\x1a,.dapr.proto.runtime.v1.StartWorkflowResponse\"\x00\x12k\n\x10GetWorkflowBeta1\x12).dapr.proto.runtime.v1.GetWorkflowRequest\x1a*.dapr.proto.runtime.v1.GetWorkflowResponse\"\x00\x12[\n\x12PurgeWorkflowBeta1\x12+.dapr.proto.runtime.v1.PurgeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x63\n\x16TerminateWorkflowBeta1\x12/.dapr.proto.runtime.v1.TerminateWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x12PauseWorkflowBeta1\x12+.dapr.proto.runtime.v1.PauseWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x13ResumeWorkflowBeta1\x12,.dapr.proto.runtime.v1.ResumeWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x17RaiseEventWorkflowBeta1\x12\x30.dapr.proto.runtime.v1.RaiseEventWorkflowRequest\x1a\x16.google.protobuf.Empty\"\x00\x12L\n\x08Shutdown\x12&.dapr.proto.runtime.v1.ShutdownRequest\x1a\x16.google.protobuf.Empty\"\x00\x12l\n\x11ScheduleJobAlpha1\x12).dapr.proto.runtime.v1.ScheduleJobRequest\x1a*.dapr.proto.runtime.v1.ScheduleJobResponse\"\x00\x12]\n\x0cGetJobAlpha1\x12$.dapr.proto.runtime.v1.GetJobRequest\x1a%.dapr.proto.runtime.v1.GetJobResponse\"\x00\x12\x66\n\x0f\x44\x65leteJobAlpha1\x12\'.dapr.proto.runtime.v1.DeleteJobRequest\x1a(.dapr.proto.runtime.v1.DeleteJobResponse\"\x00\x12k\n\x0e\x43onverseAlpha1\x12*.dapr.proto.runtime.v1.ConversationRequest\x1a+.dapr.proto.runtime.v1.ConversationResponse\"\x00\x12w\n\x0e\x43onverseAlpha2\x12\x30.dapr.proto.runtime.v1.ConversationRequestAlpha2\x1a\x31.dapr.proto.runtime.v1.ConversationResponseAlpha2\"\x00\x42i\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\xaa\x02\x1b\x44\x61pr.Client.Autogen.Grpc.v1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dapr.proto.runtime.v1.dapr_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\nio.dapr.v1B\nDaprProtosZ1github.com/dapr/dapr/pkg/proto/runtime/v1;runtime\252\002\033Dapr.Client.Autogen.Grpc.v1' - _globals['_GETSTATEREQUEST_METADATAENTRY']._loaded_options = None + _globals['_GETSTATEREQUEST_METADATAENTRY']._options = None _globals['_GETSTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._loaded_options = None + _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._options = None _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BULKSTATEITEM_METADATAENTRY']._loaded_options = None + _globals['_BULKSTATEITEM_METADATAENTRY']._options = None _globals['_BULKSTATEITEM_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETSTATERESPONSE_METADATAENTRY']._loaded_options = None + _globals['_GETSTATERESPONSE_METADATAENTRY']._options = None _globals['_GETSTATERESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_DELETESTATEREQUEST_METADATAENTRY']._loaded_options = None + _globals['_DELETESTATEREQUEST_METADATAENTRY']._options = None _globals['_DELETESTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_QUERYSTATEREQUEST_METADATAENTRY']._loaded_options = None + _globals['_QUERYSTATEREQUEST_METADATAENTRY']._options = None _globals['_QUERYSTATEREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_QUERYSTATERESPONSE_METADATAENTRY']._loaded_options = None + _globals['_QUERYSTATERESPONSE_METADATAENTRY']._options = None _globals['_QUERYSTATERESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._loaded_options = None + _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._options = None _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._loaded_options = None + _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._options = None _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._loaded_options = None + _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._options = None _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._loaded_options = None + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._options = None _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_options = b'8\001' - _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._loaded_options = None + _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._options = None _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._loaded_options = None + _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._options = None _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETSECRETREQUEST_METADATAENTRY']._loaded_options = None + _globals['_GETSECRETREQUEST_METADATAENTRY']._options = None _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETSECRETRESPONSE_DATAENTRY']._loaded_options = None + _globals['_GETSECRETRESPONSE_DATAENTRY']._options = None _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_options = b'8\001' - _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._loaded_options = None + _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._options = None _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SECRETRESPONSE_SECRETSENTRY']._loaded_options = None + _globals['_SECRETRESPONSE_SECRETSENTRY']._options = None _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_options = b'8\001' - _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._loaded_options = None + _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._options = None _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_options = b'8\001' - _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._loaded_options = None + _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._options = None _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._loaded_options = None + _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._options = None _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_options = b'8\001' - _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._loaded_options = None + _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._options = None _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_options = b'8\001' - _globals['_INVOKEACTORREQUEST_METADATAENTRY']._loaded_options = None + _globals['_INVOKEACTORREQUEST_METADATAENTRY']._options = None _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._loaded_options = None + _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._options = None _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_options = b'8\001' - _globals['_GETMETADATARESPONSE'].fields_by_name['active_actors_count']._loaded_options = None + _globals['_GETMETADATARESPONSE'].fields_by_name['active_actors_count']._options = None _globals['_GETMETADATARESPONSE'].fields_by_name['active_actors_count']._serialized_options = b'\030\001' - _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._loaded_options = None + _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._options = None _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._loaded_options = None + _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._options = None _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._loaded_options = None + _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._options = None _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._loaded_options = None + _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._options = None _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._loaded_options = None + _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._options = None _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_options = b'8\001' - _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._loaded_options = None + _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._options = None _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_options = b'8\001' - _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._loaded_options = None + _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._options = None _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._loaded_options = None + _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._options = None _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONREQUEST_METADATAENTRY']._loaded_options = None + _globals['_CONVERSATIONREQUEST_METADATAENTRY']._options = None _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONREQUEST']._loaded_options = None + _globals['_CONVERSATIONREQUEST']._options = None _globals['_CONVERSATIONREQUEST']._serialized_options = b'\030\001' - _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._loaded_options = None + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._options = None _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._loaded_options = None + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._options = None _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONINPUT']._loaded_options = None + _globals['_CONVERSATIONINPUT']._options = None _globals['_CONVERSATIONINPUT']._serialized_options = b'\030\001' - _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._loaded_options = None + _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._options = None _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_options = b'8\001' - _globals['_CONVERSATIONRESULT']._loaded_options = None + _globals['_CONVERSATIONRESULT']._options = None _globals['_CONVERSATIONRESULT']._serialized_options = b'\030\001' - _globals['_CONVERSATIONRESPONSE']._loaded_options = None + _globals['_CONVERSATIONRESPONSE']._options = None _globals['_CONVERSATIONRESPONSE']._serialized_options = b'\030\001' - _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._loaded_options = None - _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._serialized_options = b'8\001' - _globals['_DAPR'].methods_by_name['StartWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['StartWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['StartWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['GetWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['GetWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['GetWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['PurgeWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['PurgeWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['PurgeWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['TerminateWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['TerminateWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['TerminateWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['PauseWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['PauseWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['PauseWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['ResumeWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['ResumeWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['ResumeWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_DAPR'].methods_by_name['RaiseEventWorkflowAlpha1']._loaded_options = None + _globals['_DAPR'].methods_by_name['RaiseEventWorkflowAlpha1']._options = None _globals['_DAPR'].methods_by_name['RaiseEventWorkflowAlpha1']._serialized_options = b'\210\002\001' - _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_start=19242 - _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_end=19329 - _globals['_INVOKESERVICEREQUEST']._serialized_start=224 - _globals['_INVOKESERVICEREQUEST']._serialized_end=312 - _globals['_GETSTATEREQUEST']._serialized_start=315 - _globals['_GETSTATEREQUEST']._serialized_end=560 - _globals['_GETSTATEREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_GETSTATEREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_GETBULKSTATEREQUEST']._serialized_start=563 - _globals['_GETBULKSTATEREQUEST']._serialized_end=764 - _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_GETBULKSTATERESPONSE']._serialized_start=766 - _globals['_GETBULKSTATERESPONSE']._serialized_end=841 - _globals['_BULKSTATEITEM']._serialized_start=844 - _globals['_BULKSTATEITEM']._serialized_end=1034 - _globals['_BULKSTATEITEM_METADATAENTRY']._serialized_start=513 - _globals['_BULKSTATEITEM_METADATAENTRY']._serialized_end=560 - _globals['_GETSTATERESPONSE']._serialized_start=1037 - _globals['_GETSTATERESPONSE']._serialized_end=1205 - _globals['_GETSTATERESPONSE_METADATAENTRY']._serialized_start=513 - _globals['_GETSTATERESPONSE_METADATAENTRY']._serialized_end=560 - _globals['_DELETESTATEREQUEST']._serialized_start=1208 - _globals['_DELETESTATEREQUEST']._serialized_end=1480 - _globals['_DELETESTATEREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_DELETESTATEREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_DELETEBULKSTATEREQUEST']._serialized_start=1482 - _globals['_DELETEBULKSTATEREQUEST']._serialized_end=1575 - _globals['_SAVESTATEREQUEST']._serialized_start=1577 - _globals['_SAVESTATEREQUEST']._serialized_end=1664 - _globals['_QUERYSTATEREQUEST']._serialized_start=1667 - _globals['_QUERYSTATEREQUEST']._serialized_end=1855 - _globals['_QUERYSTATEREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_QUERYSTATEREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_QUERYSTATEITEM']._serialized_start=1857 - _globals['_QUERYSTATEITEM']._serialized_end=1929 - _globals['_QUERYSTATERESPONSE']._serialized_start=1932 - _globals['_QUERYSTATERESPONSE']._serialized_end=2147 - _globals['_QUERYSTATERESPONSE_METADATAENTRY']._serialized_start=513 - _globals['_QUERYSTATERESPONSE_METADATAENTRY']._serialized_end=560 - _globals['_PUBLISHEVENTREQUEST']._serialized_start=2150 - _globals['_PUBLISHEVENTREQUEST']._serialized_end=2373 - _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_BULKPUBLISHREQUEST']._serialized_start=2376 - _globals['_BULKPUBLISHREQUEST']._serialized_end=2621 - _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_BULKPUBLISHREQUESTENTRY']._serialized_start=2624 - _globals['_BULKPUBLISHREQUESTENTRY']._serialized_end=2833 - _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_start=513 - _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_end=560 - _globals['_BULKPUBLISHRESPONSE']._serialized_start=2835 - _globals['_BULKPUBLISHRESPONSE']._serialized_end=2934 - _globals['_BULKPUBLISHRESPONSEFAILEDENTRY']._serialized_start=2936 - _globals['_BULKPUBLISHRESPONSEFAILEDENTRY']._serialized_end=3001 - _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_start=3004 - _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_end=3264 - _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1']._serialized_start=3267 - _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1']._serialized_end=3545 - _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_start=513 - _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_end=560 - _globals['_SUBSCRIBETOPICEVENTSREQUESTPROCESSEDALPHA1']._serialized_start=3547 - _globals['_SUBSCRIBETOPICEVENTSREQUESTPROCESSEDALPHA1']._serialized_end=3662 - _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_start=3665 - _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_end=3902 - _globals['_SUBSCRIBETOPICEVENTSRESPONSEINITIALALPHA1']._serialized_start=3904 - _globals['_SUBSCRIBETOPICEVENTSRESPONSEINITIALALPHA1']._serialized_end=3947 - _globals['_INVOKEBINDINGREQUEST']._serialized_start=3950 - _globals['_INVOKEBINDINGREQUEST']._serialized_end=4145 - _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_INVOKEBINDINGRESPONSE']._serialized_start=4148 - _globals['_INVOKEBINDINGRESPONSE']._serialized_end=4312 - _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_start=513 - _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_end=560 - _globals['_GETSECRETREQUEST']._serialized_start=4315 - _globals['_GETSECRETREQUEST']._serialized_end=4499 - _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_GETSECRETRESPONSE']._serialized_start=4502 - _globals['_GETSECRETRESPONSE']._serialized_end=4632 - _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_start=4589 - _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_end=4632 - _globals['_GETBULKSECRETREQUEST']._serialized_start=4635 - _globals['_GETBULKSECRETREQUEST']._serialized_end=4814 - _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_SECRETRESPONSE']._serialized_start=4817 - _globals['_SECRETRESPONSE']._serialized_end=4950 - _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_start=4904 - _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_end=4950 - _globals['_GETBULKSECRETRESPONSE']._serialized_start=4953 - _globals['_GETBULKSECRETRESPONSE']._serialized_end=5130 - _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_start=5048 - _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_end=5130 - _globals['_TRANSACTIONALSTATEOPERATION']._serialized_start=5132 - _globals['_TRANSACTIONALSTATEOPERATION']._serialized_end=5234 - _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_start=5237 - _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_end=5496 - _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_REGISTERACTORTIMERREQUEST']._serialized_start=5499 - _globals['_REGISTERACTORTIMERREQUEST']._serialized_end=5686 - _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_start=5688 - _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_end=5789 - _globals['_REGISTERACTORREMINDERREQUEST']._serialized_start=5792 - _globals['_REGISTERACTORREMINDERREQUEST']._serialized_end=5964 - _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_start=5966 - _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_end=6070 - _globals['_GETACTORSTATEREQUEST']._serialized_start=6072 - _globals['_GETACTORSTATEREQUEST']._serialized_end=6165 - _globals['_GETACTORSTATERESPONSE']._serialized_start=6168 - _globals['_GETACTORSTATERESPONSE']._serialized_end=6332 - _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_start=513 - _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_end=560 - _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_start=6335 - _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_end=6507 - _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_start=6510 - _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_end=6755 - _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_start=513 - _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_end=560 - _globals['_INVOKEACTORREQUEST']._serialized_start=6758 - _globals['_INVOKEACTORREQUEST']._serialized_end=6990 - _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_INVOKEACTORRESPONSE']._serialized_start=6992 - _globals['_INVOKEACTORRESPONSE']._serialized_end=7027 - _globals['_GETMETADATAREQUEST']._serialized_start=7029 - _globals['_GETMETADATAREQUEST']._serialized_end=7049 - _globals['_GETMETADATARESPONSE']._serialized_start=7052 - _globals['_GETMETADATARESPONSE']._serialized_end=7938 - _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_start=7869 - _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_end=7924 - _globals['_METADATASCHEDULER']._serialized_start=7940 - _globals['_METADATASCHEDULER']._serialized_end=7988 - _globals['_ACTORRUNTIME']._serialized_start=7991 - _globals['_ACTORRUNTIME']._serialized_end=8307 - _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_start=8242 - _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_end=8307 - _globals['_ACTIVEACTORSCOUNT']._serialized_start=8309 - _globals['_ACTIVEACTORSCOUNT']._serialized_end=8357 - _globals['_REGISTEREDCOMPONENTS']._serialized_start=8359 - _globals['_REGISTEREDCOMPONENTS']._serialized_end=8448 - _globals['_METADATAHTTPENDPOINT']._serialized_start=8450 - _globals['_METADATAHTTPENDPOINT']._serialized_end=8492 - _globals['_APPCONNECTIONPROPERTIES']._serialized_start=8495 - _globals['_APPCONNECTIONPROPERTIES']._serialized_end=8704 - _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_start=8707 - _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_end=8927 - _globals['_PUBSUBSUBSCRIPTION']._serialized_start=8930 - _globals['_PUBSUBSUBSCRIPTION']._serialized_end=9320 - _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_start=513 - _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_end=560 - _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_start=9322 - _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_end=9409 - _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_start=9411 - _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_end=9464 - _globals['_SETMETADATAREQUEST']._serialized_start=9466 - _globals['_SETMETADATAREQUEST']._serialized_end=9514 - _globals['_GETCONFIGURATIONREQUEST']._serialized_start=9517 - _globals['_GETCONFIGURATIONREQUEST']._serialized_end=9705 - _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_GETCONFIGURATIONRESPONSE']._serialized_start=9708 - _globals['_GETCONFIGURATIONRESPONSE']._serialized_end=9896 - _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9811 - _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9896 - _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_start=9899 - _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_end=10099 - _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_start=10101 - _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_end=10166 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=10169 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10381 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9811 - _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9896 - _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=10383 - _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10446 - _globals['_TRYLOCKREQUEST']._serialized_start=10449 - _globals['_TRYLOCKREQUEST']._serialized_end=10604 - _globals['_TRYLOCKRESPONSE']._serialized_start=10606 - _globals['_TRYLOCKRESPONSE']._serialized_end=10640 - _globals['_UNLOCKREQUEST']._serialized_start=10642 - _globals['_UNLOCKREQUEST']._serialized_end=10752 - _globals['_UNLOCKRESPONSE']._serialized_start=10755 - _globals['_UNLOCKRESPONSE']._serialized_end=10929 - _globals['_UNLOCKRESPONSE_STATUS']._serialized_start=10835 - _globals['_UNLOCKRESPONSE_STATUS']._serialized_end=10929 - _globals['_SUBTLEGETKEYREQUEST']._serialized_start=10932 - _globals['_SUBTLEGETKEYREQUEST']._serialized_end=11108 - _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_start=11078 - _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_end=11108 - _globals['_SUBTLEGETKEYRESPONSE']._serialized_start=11110 - _globals['_SUBTLEGETKEYRESPONSE']._serialized_end=11177 - _globals['_SUBTLEENCRYPTREQUEST']._serialized_start=11180 - _globals['_SUBTLEENCRYPTREQUEST']._serialized_end=11362 - _globals['_SUBTLEENCRYPTRESPONSE']._serialized_start=11364 - _globals['_SUBTLEENCRYPTRESPONSE']._serialized_end=11420 - _globals['_SUBTLEDECRYPTREQUEST']._serialized_start=11423 - _globals['_SUBTLEDECRYPTREQUEST']._serialized_end=11619 - _globals['_SUBTLEDECRYPTRESPONSE']._serialized_start=11621 - _globals['_SUBTLEDECRYPTRESPONSE']._serialized_end=11663 - _globals['_SUBTLEWRAPKEYREQUEST']._serialized_start=11666 - _globals['_SUBTLEWRAPKEYREQUEST']._serialized_end=11866 - _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_start=11868 - _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_end=11937 - _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_start=11940 - _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_end=12151 - _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_start=12153 - _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_end=12215 - _globals['_SUBTLESIGNREQUEST']._serialized_start=12217 - _globals['_SUBTLESIGNREQUEST']._serialized_end=12337 - _globals['_SUBTLESIGNRESPONSE']._serialized_start=12339 - _globals['_SUBTLESIGNRESPONSE']._serialized_end=12378 - _globals['_SUBTLEVERIFYREQUEST']._serialized_start=12381 - _globals['_SUBTLEVERIFYREQUEST']._serialized_end=12522 - _globals['_SUBTLEVERIFYRESPONSE']._serialized_start=12524 - _globals['_SUBTLEVERIFYRESPONSE']._serialized_end=12561 - _globals['_ENCRYPTREQUEST']._serialized_start=12564 - _globals['_ENCRYPTREQUEST']._serialized_end=12697 - _globals['_ENCRYPTREQUESTOPTIONS']._serialized_start=12700 - _globals['_ENCRYPTREQUESTOPTIONS']._serialized_end=12954 - _globals['_ENCRYPTRESPONSE']._serialized_start=12956 - _globals['_ENCRYPTRESPONSE']._serialized_end=13027 - _globals['_DECRYPTREQUEST']._serialized_start=13030 - _globals['_DECRYPTREQUEST']._serialized_end=13163 - _globals['_DECRYPTREQUESTOPTIONS']._serialized_start=13165 - _globals['_DECRYPTREQUESTOPTIONS']._serialized_end=13254 - _globals['_DECRYPTRESPONSE']._serialized_start=13256 - _globals['_DECRYPTRESPONSE']._serialized_end=13327 - _globals['_GETWORKFLOWREQUEST']._serialized_start=13329 - _globals['_GETWORKFLOWREQUEST']._serialized_end=13429 - _globals['_GETWORKFLOWRESPONSE']._serialized_start=13432 - _globals['_GETWORKFLOWRESPONSE']._serialized_end=13820 - _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_start=13771 - _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_end=13820 - _globals['_STARTWORKFLOWREQUEST']._serialized_start=13823 - _globals['_STARTWORKFLOWREQUEST']._serialized_end=14100 - _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_start=14054 - _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_end=14100 - _globals['_STARTWORKFLOWRESPONSE']._serialized_start=14102 - _globals['_STARTWORKFLOWRESPONSE']._serialized_end=14158 - _globals['_TERMINATEWORKFLOWREQUEST']._serialized_start=14160 - _globals['_TERMINATEWORKFLOWREQUEST']._serialized_end=14266 - _globals['_PAUSEWORKFLOWREQUEST']._serialized_start=14268 - _globals['_PAUSEWORKFLOWREQUEST']._serialized_end=14370 - _globals['_RESUMEWORKFLOWREQUEST']._serialized_start=14372 - _globals['_RESUMEWORKFLOWREQUEST']._serialized_end=14475 - _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_start=14478 - _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_end=14636 - _globals['_PURGEWORKFLOWREQUEST']._serialized_start=14638 - _globals['_PURGEWORKFLOWREQUEST']._serialized_end=14740 - _globals['_SHUTDOWNREQUEST']._serialized_start=14742 - _globals['_SHUTDOWNREQUEST']._serialized_end=14759 - _globals['_JOB']._serialized_start=14762 - _globals['_JOB']._serialized_end=15127 - _globals['_SCHEDULEJOBREQUEST']._serialized_start=15129 - _globals['_SCHEDULEJOBREQUEST']._serialized_end=15190 - _globals['_SCHEDULEJOBRESPONSE']._serialized_start=15192 - _globals['_SCHEDULEJOBRESPONSE']._serialized_end=15213 - _globals['_GETJOBREQUEST']._serialized_start=15215 - _globals['_GETJOBREQUEST']._serialized_end=15244 - _globals['_GETJOBRESPONSE']._serialized_start=15246 - _globals['_GETJOBRESPONSE']._serialized_end=15303 - _globals['_DELETEJOBREQUEST']._serialized_start=15305 - _globals['_DELETEJOBREQUEST']._serialized_end=15337 - _globals['_DELETEJOBRESPONSE']._serialized_start=15339 - _globals['_DELETEJOBRESPONSE']._serialized_end=15358 - _globals['_CONVERSATIONREQUEST']._serialized_start=15361 - _globals['_CONVERSATIONREQUEST']._serialized_end=15852 - _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_start=15685 - _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_end=15756 - _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_start=513 - _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_end=560 - _globals['_CONVERSATIONREQUESTALPHA2']._serialized_start=15855 - _globals['_CONVERSATIONREQUESTALPHA2']._serialized_end=16469 - _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_start=15685 - _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_end=15756 - _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_start=513 - _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_end=560 - _globals['_CONVERSATIONINPUT']._serialized_start=16471 - _globals['_CONVERSATIONINPUT']._serialized_end=16575 - _globals['_CONVERSATIONINPUTALPHA2']._serialized_start=16577 - _globals['_CONVERSATIONINPUTALPHA2']._serialized_end=16702 - _globals['_CONVERSATIONMESSAGE']._serialized_start=16705 - _globals['_CONVERSATIONMESSAGE']._serialized_end=17112 - _globals['_CONVERSATIONMESSAGEOFDEVELOPER']._serialized_start=17115 - _globals['_CONVERSATIONMESSAGEOFDEVELOPER']._serialized_end=17243 - _globals['_CONVERSATIONMESSAGEOFSYSTEM']._serialized_start=17245 - _globals['_CONVERSATIONMESSAGEOFSYSTEM']._serialized_end=17370 - _globals['_CONVERSATIONMESSAGEOFUSER']._serialized_start=17372 - _globals['_CONVERSATIONMESSAGEOFUSER']._serialized_end=17495 - _globals['_CONVERSATIONMESSAGEOFASSISTANT']._serialized_start=17498 - _globals['_CONVERSATIONMESSAGEOFASSISTANT']._serialized_end=17692 - _globals['_CONVERSATIONMESSAGEOFTOOL']._serialized_start=17695 - _globals['_CONVERSATIONMESSAGEOFTOOL']._serialized_end=17838 - _globals['_CONVERSATIONTOOLCALLS']._serialized_start=17841 - _globals['_CONVERSATIONTOOLCALLS']._serialized_end=17978 - _globals['_CONVERSATIONTOOLCALLSOFFUNCTION']._serialized_start=17980 - _globals['_CONVERSATIONTOOLCALLSOFFUNCTION']._serialized_end=18046 - _globals['_CONVERSATIONMESSAGECONTENT']._serialized_start=18048 - _globals['_CONVERSATIONMESSAGECONTENT']._serialized_end=18090 - _globals['_CONVERSATIONRESULT']._serialized_start=18093 - _globals['_CONVERSATIONRESULT']._serialized_end=18285 - _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_start=15685 - _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_end=15756 - _globals['_CONVERSATIONRESULTALPHA2']._serialized_start=18287 - _globals['_CONVERSATIONRESULTALPHA2']._serialized_end=18380 - _globals['_CONVERSATIONRESULTCHOICES']._serialized_start=18383 - _globals['_CONVERSATIONRESULTCHOICES']._serialized_end=18515 - _globals['_CONVERSATIONRESULTMESSAGE']._serialized_start=18517 - _globals['_CONVERSATIONRESULTMESSAGE']._serialized_end=18627 - _globals['_CONVERSATIONRESPONSE']._serialized_start=18629 - _globals['_CONVERSATIONRESPONSE']._serialized_end=18753 - _globals['_CONVERSATIONRESPONSEALPHA2']._serialized_start=18756 - _globals['_CONVERSATIONRESPONSEALPHA2']._serialized_end=18890 - _globals['_CONVERSATIONTOOLS']._serialized_start=18892 - _globals['_CONVERSATIONTOOLS']._serialized_end=18995 - _globals['_CONVERSATIONTOOLSFUNCTION']._serialized_start=18998 - _globals['_CONVERSATIONTOOLSFUNCTION']._serialized_end=19240 - _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._serialized_start=15685 - _globals['_CONVERSATIONTOOLSFUNCTION_PARAMETERSENTRY']._serialized_end=15756 - _globals['_DAPR']._serialized_start=19332 - _globals['_DAPR']._serialized_end=25787 + _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_start=19158 + _globals['_PUBSUBSUBSCRIPTIONTYPE']._serialized_end=19245 + _globals['_INVOKESERVICEREQUEST']._serialized_start=254 + _globals['_INVOKESERVICEREQUEST']._serialized_end=342 + _globals['_GETSTATEREQUEST']._serialized_start=345 + _globals['_GETSTATEREQUEST']._serialized_end=590 + _globals['_GETSTATEREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_GETSTATEREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_GETBULKSTATEREQUEST']._serialized_start=593 + _globals['_GETBULKSTATEREQUEST']._serialized_end=794 + _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_GETBULKSTATEREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_GETBULKSTATERESPONSE']._serialized_start=796 + _globals['_GETBULKSTATERESPONSE']._serialized_end=871 + _globals['_BULKSTATEITEM']._serialized_start=874 + _globals['_BULKSTATEITEM']._serialized_end=1064 + _globals['_BULKSTATEITEM_METADATAENTRY']._serialized_start=543 + _globals['_BULKSTATEITEM_METADATAENTRY']._serialized_end=590 + _globals['_GETSTATERESPONSE']._serialized_start=1067 + _globals['_GETSTATERESPONSE']._serialized_end=1235 + _globals['_GETSTATERESPONSE_METADATAENTRY']._serialized_start=543 + _globals['_GETSTATERESPONSE_METADATAENTRY']._serialized_end=590 + _globals['_DELETESTATEREQUEST']._serialized_start=1238 + _globals['_DELETESTATEREQUEST']._serialized_end=1510 + _globals['_DELETESTATEREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_DELETESTATEREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_DELETEBULKSTATEREQUEST']._serialized_start=1512 + _globals['_DELETEBULKSTATEREQUEST']._serialized_end=1605 + _globals['_SAVESTATEREQUEST']._serialized_start=1607 + _globals['_SAVESTATEREQUEST']._serialized_end=1694 + _globals['_QUERYSTATEREQUEST']._serialized_start=1697 + _globals['_QUERYSTATEREQUEST']._serialized_end=1885 + _globals['_QUERYSTATEREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_QUERYSTATEREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_QUERYSTATEITEM']._serialized_start=1887 + _globals['_QUERYSTATEITEM']._serialized_end=1959 + _globals['_QUERYSTATERESPONSE']._serialized_start=1962 + _globals['_QUERYSTATERESPONSE']._serialized_end=2177 + _globals['_QUERYSTATERESPONSE_METADATAENTRY']._serialized_start=543 + _globals['_QUERYSTATERESPONSE_METADATAENTRY']._serialized_end=590 + _globals['_PUBLISHEVENTREQUEST']._serialized_start=2180 + _globals['_PUBLISHEVENTREQUEST']._serialized_end=2403 + _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_PUBLISHEVENTREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_BULKPUBLISHREQUEST']._serialized_start=2406 + _globals['_BULKPUBLISHREQUEST']._serialized_end=2651 + _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_BULKPUBLISHREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_BULKPUBLISHREQUESTENTRY']._serialized_start=2654 + _globals['_BULKPUBLISHREQUESTENTRY']._serialized_end=2863 + _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_start=543 + _globals['_BULKPUBLISHREQUESTENTRY_METADATAENTRY']._serialized_end=590 + _globals['_BULKPUBLISHRESPONSE']._serialized_start=2865 + _globals['_BULKPUBLISHRESPONSE']._serialized_end=2964 + _globals['_BULKPUBLISHRESPONSEFAILEDENTRY']._serialized_start=2966 + _globals['_BULKPUBLISHRESPONSEFAILEDENTRY']._serialized_end=3031 + _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_start=3034 + _globals['_SUBSCRIBETOPICEVENTSREQUESTALPHA1']._serialized_end=3294 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1']._serialized_start=3297 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1']._serialized_end=3575 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_start=543 + _globals['_SUBSCRIBETOPICEVENTSREQUESTINITIALALPHA1_METADATAENTRY']._serialized_end=590 + _globals['_SUBSCRIBETOPICEVENTSREQUESTPROCESSEDALPHA1']._serialized_start=3577 + _globals['_SUBSCRIBETOPICEVENTSREQUESTPROCESSEDALPHA1']._serialized_end=3692 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_start=3695 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEALPHA1']._serialized_end=3932 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEINITIALALPHA1']._serialized_start=3934 + _globals['_SUBSCRIBETOPICEVENTSRESPONSEINITIALALPHA1']._serialized_end=3977 + _globals['_INVOKEBINDINGREQUEST']._serialized_start=3980 + _globals['_INVOKEBINDINGREQUEST']._serialized_end=4175 + _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_INVOKEBINDINGREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_INVOKEBINDINGRESPONSE']._serialized_start=4178 + _globals['_INVOKEBINDINGRESPONSE']._serialized_end=4342 + _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_start=543 + _globals['_INVOKEBINDINGRESPONSE_METADATAENTRY']._serialized_end=590 + _globals['_GETSECRETREQUEST']._serialized_start=4345 + _globals['_GETSECRETREQUEST']._serialized_end=4529 + _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_GETSECRETREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_GETSECRETRESPONSE']._serialized_start=4532 + _globals['_GETSECRETRESPONSE']._serialized_end=4662 + _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_start=4619 + _globals['_GETSECRETRESPONSE_DATAENTRY']._serialized_end=4662 + _globals['_GETBULKSECRETREQUEST']._serialized_start=4665 + _globals['_GETBULKSECRETREQUEST']._serialized_end=4844 + _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_GETBULKSECRETREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_SECRETRESPONSE']._serialized_start=4847 + _globals['_SECRETRESPONSE']._serialized_end=4980 + _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_start=4934 + _globals['_SECRETRESPONSE_SECRETSENTRY']._serialized_end=4980 + _globals['_GETBULKSECRETRESPONSE']._serialized_start=4983 + _globals['_GETBULKSECRETRESPONSE']._serialized_end=5160 + _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_start=5078 + _globals['_GETBULKSECRETRESPONSE_DATAENTRY']._serialized_end=5160 + _globals['_TRANSACTIONALSTATEOPERATION']._serialized_start=5162 + _globals['_TRANSACTIONALSTATEOPERATION']._serialized_end=5264 + _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_start=5267 + _globals['_EXECUTESTATETRANSACTIONREQUEST']._serialized_end=5526 + _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_EXECUTESTATETRANSACTIONREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_REGISTERACTORTIMERREQUEST']._serialized_start=5529 + _globals['_REGISTERACTORTIMERREQUEST']._serialized_end=5716 + _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_start=5718 + _globals['_UNREGISTERACTORTIMERREQUEST']._serialized_end=5819 + _globals['_REGISTERACTORREMINDERREQUEST']._serialized_start=5822 + _globals['_REGISTERACTORREMINDERREQUEST']._serialized_end=5994 + _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_start=5996 + _globals['_UNREGISTERACTORREMINDERREQUEST']._serialized_end=6100 + _globals['_GETACTORSTATEREQUEST']._serialized_start=6102 + _globals['_GETACTORSTATEREQUEST']._serialized_end=6195 + _globals['_GETACTORSTATERESPONSE']._serialized_start=6198 + _globals['_GETACTORSTATERESPONSE']._serialized_end=6362 + _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_start=543 + _globals['_GETACTORSTATERESPONSE_METADATAENTRY']._serialized_end=590 + _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_start=6365 + _globals['_EXECUTEACTORSTATETRANSACTIONREQUEST']._serialized_end=6537 + _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_start=6540 + _globals['_TRANSACTIONALACTORSTATEOPERATION']._serialized_end=6785 + _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_start=543 + _globals['_TRANSACTIONALACTORSTATEOPERATION_METADATAENTRY']._serialized_end=590 + _globals['_INVOKEACTORREQUEST']._serialized_start=6788 + _globals['_INVOKEACTORREQUEST']._serialized_end=7020 + _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_INVOKEACTORREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_INVOKEACTORRESPONSE']._serialized_start=7022 + _globals['_INVOKEACTORRESPONSE']._serialized_end=7057 + _globals['_GETMETADATAREQUEST']._serialized_start=7059 + _globals['_GETMETADATAREQUEST']._serialized_end=7079 + _globals['_GETMETADATARESPONSE']._serialized_start=7082 + _globals['_GETMETADATARESPONSE']._serialized_end=7968 + _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_start=7899 + _globals['_GETMETADATARESPONSE_EXTENDEDMETADATAENTRY']._serialized_end=7954 + _globals['_METADATASCHEDULER']._serialized_start=7970 + _globals['_METADATASCHEDULER']._serialized_end=8018 + _globals['_ACTORRUNTIME']._serialized_start=8021 + _globals['_ACTORRUNTIME']._serialized_end=8337 + _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_start=8272 + _globals['_ACTORRUNTIME_ACTORRUNTIMESTATUS']._serialized_end=8337 + _globals['_ACTIVEACTORSCOUNT']._serialized_start=8339 + _globals['_ACTIVEACTORSCOUNT']._serialized_end=8387 + _globals['_REGISTEREDCOMPONENTS']._serialized_start=8389 + _globals['_REGISTEREDCOMPONENTS']._serialized_end=8478 + _globals['_METADATAHTTPENDPOINT']._serialized_start=8480 + _globals['_METADATAHTTPENDPOINT']._serialized_end=8522 + _globals['_APPCONNECTIONPROPERTIES']._serialized_start=8525 + _globals['_APPCONNECTIONPROPERTIES']._serialized_end=8734 + _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_start=8737 + _globals['_APPCONNECTIONHEALTHPROPERTIES']._serialized_end=8957 + _globals['_PUBSUBSUBSCRIPTION']._serialized_start=8960 + _globals['_PUBSUBSUBSCRIPTION']._serialized_end=9350 + _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_start=543 + _globals['_PUBSUBSUBSCRIPTION_METADATAENTRY']._serialized_end=590 + _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_start=9352 + _globals['_PUBSUBSUBSCRIPTIONRULES']._serialized_end=9439 + _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_start=9441 + _globals['_PUBSUBSUBSCRIPTIONRULE']._serialized_end=9494 + _globals['_SETMETADATAREQUEST']._serialized_start=9496 + _globals['_SETMETADATAREQUEST']._serialized_end=9544 + _globals['_GETCONFIGURATIONREQUEST']._serialized_start=9547 + _globals['_GETCONFIGURATIONREQUEST']._serialized_end=9735 + _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_GETCONFIGURATIONREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_GETCONFIGURATIONRESPONSE']._serialized_start=9738 + _globals['_GETCONFIGURATIONRESPONSE']._serialized_end=9926 + _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9841 + _globals['_GETCONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9926 + _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_start=9929 + _globals['_SUBSCRIBECONFIGURATIONREQUEST']._serialized_end=10129 + _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_SUBSCRIBECONFIGURATIONREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_start=10131 + _globals['_UNSUBSCRIBECONFIGURATIONREQUEST']._serialized_end=10196 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=10199 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10411 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_start=9841 + _globals['_SUBSCRIBECONFIGURATIONRESPONSE_ITEMSENTRY']._serialized_end=9926 + _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_start=10413 + _globals['_UNSUBSCRIBECONFIGURATIONRESPONSE']._serialized_end=10476 + _globals['_TRYLOCKREQUEST']._serialized_start=10479 + _globals['_TRYLOCKREQUEST']._serialized_end=10634 + _globals['_TRYLOCKRESPONSE']._serialized_start=10636 + _globals['_TRYLOCKRESPONSE']._serialized_end=10670 + _globals['_UNLOCKREQUEST']._serialized_start=10672 + _globals['_UNLOCKREQUEST']._serialized_end=10782 + _globals['_UNLOCKRESPONSE']._serialized_start=10785 + _globals['_UNLOCKRESPONSE']._serialized_end=10959 + _globals['_UNLOCKRESPONSE_STATUS']._serialized_start=10865 + _globals['_UNLOCKRESPONSE_STATUS']._serialized_end=10959 + _globals['_SUBTLEGETKEYREQUEST']._serialized_start=10962 + _globals['_SUBTLEGETKEYREQUEST']._serialized_end=11138 + _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_start=11108 + _globals['_SUBTLEGETKEYREQUEST_KEYFORMAT']._serialized_end=11138 + _globals['_SUBTLEGETKEYRESPONSE']._serialized_start=11140 + _globals['_SUBTLEGETKEYRESPONSE']._serialized_end=11207 + _globals['_SUBTLEENCRYPTREQUEST']._serialized_start=11210 + _globals['_SUBTLEENCRYPTREQUEST']._serialized_end=11392 + _globals['_SUBTLEENCRYPTRESPONSE']._serialized_start=11394 + _globals['_SUBTLEENCRYPTRESPONSE']._serialized_end=11450 + _globals['_SUBTLEDECRYPTREQUEST']._serialized_start=11453 + _globals['_SUBTLEDECRYPTREQUEST']._serialized_end=11649 + _globals['_SUBTLEDECRYPTRESPONSE']._serialized_start=11651 + _globals['_SUBTLEDECRYPTRESPONSE']._serialized_end=11693 + _globals['_SUBTLEWRAPKEYREQUEST']._serialized_start=11696 + _globals['_SUBTLEWRAPKEYREQUEST']._serialized_end=11896 + _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_start=11898 + _globals['_SUBTLEWRAPKEYRESPONSE']._serialized_end=11967 + _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_start=11970 + _globals['_SUBTLEUNWRAPKEYREQUEST']._serialized_end=12181 + _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_start=12183 + _globals['_SUBTLEUNWRAPKEYRESPONSE']._serialized_end=12245 + _globals['_SUBTLESIGNREQUEST']._serialized_start=12247 + _globals['_SUBTLESIGNREQUEST']._serialized_end=12367 + _globals['_SUBTLESIGNRESPONSE']._serialized_start=12369 + _globals['_SUBTLESIGNRESPONSE']._serialized_end=12408 + _globals['_SUBTLEVERIFYREQUEST']._serialized_start=12411 + _globals['_SUBTLEVERIFYREQUEST']._serialized_end=12552 + _globals['_SUBTLEVERIFYRESPONSE']._serialized_start=12554 + _globals['_SUBTLEVERIFYRESPONSE']._serialized_end=12591 + _globals['_ENCRYPTREQUEST']._serialized_start=12594 + _globals['_ENCRYPTREQUEST']._serialized_end=12727 + _globals['_ENCRYPTREQUESTOPTIONS']._serialized_start=12730 + _globals['_ENCRYPTREQUESTOPTIONS']._serialized_end=12984 + _globals['_ENCRYPTRESPONSE']._serialized_start=12986 + _globals['_ENCRYPTRESPONSE']._serialized_end=13057 + _globals['_DECRYPTREQUEST']._serialized_start=13060 + _globals['_DECRYPTREQUEST']._serialized_end=13193 + _globals['_DECRYPTREQUESTOPTIONS']._serialized_start=13195 + _globals['_DECRYPTREQUESTOPTIONS']._serialized_end=13284 + _globals['_DECRYPTRESPONSE']._serialized_start=13286 + _globals['_DECRYPTRESPONSE']._serialized_end=13357 + _globals['_GETWORKFLOWREQUEST']._serialized_start=13359 + _globals['_GETWORKFLOWREQUEST']._serialized_end=13459 + _globals['_GETWORKFLOWRESPONSE']._serialized_start=13462 + _globals['_GETWORKFLOWRESPONSE']._serialized_end=13850 + _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_start=13801 + _globals['_GETWORKFLOWRESPONSE_PROPERTIESENTRY']._serialized_end=13850 + _globals['_STARTWORKFLOWREQUEST']._serialized_start=13853 + _globals['_STARTWORKFLOWREQUEST']._serialized_end=14130 + _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_start=14084 + _globals['_STARTWORKFLOWREQUEST_OPTIONSENTRY']._serialized_end=14130 + _globals['_STARTWORKFLOWRESPONSE']._serialized_start=14132 + _globals['_STARTWORKFLOWRESPONSE']._serialized_end=14188 + _globals['_TERMINATEWORKFLOWREQUEST']._serialized_start=14190 + _globals['_TERMINATEWORKFLOWREQUEST']._serialized_end=14296 + _globals['_PAUSEWORKFLOWREQUEST']._serialized_start=14298 + _globals['_PAUSEWORKFLOWREQUEST']._serialized_end=14400 + _globals['_RESUMEWORKFLOWREQUEST']._serialized_start=14402 + _globals['_RESUMEWORKFLOWREQUEST']._serialized_end=14505 + _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_start=14508 + _globals['_RAISEEVENTWORKFLOWREQUEST']._serialized_end=14666 + _globals['_PURGEWORKFLOWREQUEST']._serialized_start=14668 + _globals['_PURGEWORKFLOWREQUEST']._serialized_end=14770 + _globals['_SHUTDOWNREQUEST']._serialized_start=14772 + _globals['_SHUTDOWNREQUEST']._serialized_end=14789 + _globals['_JOB']._serialized_start=14792 + _globals['_JOB']._serialized_end=15157 + _globals['_SCHEDULEJOBREQUEST']._serialized_start=15159 + _globals['_SCHEDULEJOBREQUEST']._serialized_end=15220 + _globals['_SCHEDULEJOBRESPONSE']._serialized_start=15222 + _globals['_SCHEDULEJOBRESPONSE']._serialized_end=15243 + _globals['_GETJOBREQUEST']._serialized_start=15245 + _globals['_GETJOBREQUEST']._serialized_end=15274 + _globals['_GETJOBRESPONSE']._serialized_start=15276 + _globals['_GETJOBRESPONSE']._serialized_end=15333 + _globals['_DELETEJOBREQUEST']._serialized_start=15335 + _globals['_DELETEJOBREQUEST']._serialized_end=15367 + _globals['_DELETEJOBRESPONSE']._serialized_start=15369 + _globals['_DELETEJOBRESPONSE']._serialized_end=15388 + _globals['_CONVERSATIONREQUEST']._serialized_start=15391 + _globals['_CONVERSATIONREQUEST']._serialized_end=15882 + _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_start=15715 + _globals['_CONVERSATIONREQUEST_PARAMETERSENTRY']._serialized_end=15786 + _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_start=543 + _globals['_CONVERSATIONREQUEST_METADATAENTRY']._serialized_end=590 + _globals['_CONVERSATIONREQUESTALPHA2']._serialized_start=15885 + _globals['_CONVERSATIONREQUESTALPHA2']._serialized_end=16499 + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_start=15715 + _globals['_CONVERSATIONREQUESTALPHA2_PARAMETERSENTRY']._serialized_end=15786 + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_start=543 + _globals['_CONVERSATIONREQUESTALPHA2_METADATAENTRY']._serialized_end=590 + _globals['_CONVERSATIONINPUT']._serialized_start=16501 + _globals['_CONVERSATIONINPUT']._serialized_end=16605 + _globals['_CONVERSATIONINPUTALPHA2']._serialized_start=16607 + _globals['_CONVERSATIONINPUTALPHA2']._serialized_end=16732 + _globals['_CONVERSATIONMESSAGE']._serialized_start=16735 + _globals['_CONVERSATIONMESSAGE']._serialized_end=17142 + _globals['_CONVERSATIONMESSAGEOFDEVELOPER']._serialized_start=17145 + _globals['_CONVERSATIONMESSAGEOFDEVELOPER']._serialized_end=17273 + _globals['_CONVERSATIONMESSAGEOFSYSTEM']._serialized_start=17275 + _globals['_CONVERSATIONMESSAGEOFSYSTEM']._serialized_end=17400 + _globals['_CONVERSATIONMESSAGEOFUSER']._serialized_start=17402 + _globals['_CONVERSATIONMESSAGEOFUSER']._serialized_end=17525 + _globals['_CONVERSATIONMESSAGEOFASSISTANT']._serialized_start=17528 + _globals['_CONVERSATIONMESSAGEOFASSISTANT']._serialized_end=17722 + _globals['_CONVERSATIONMESSAGEOFTOOL']._serialized_start=17725 + _globals['_CONVERSATIONMESSAGEOFTOOL']._serialized_end=17868 + _globals['_CONVERSATIONTOOLCALLS']._serialized_start=17871 + _globals['_CONVERSATIONTOOLCALLS']._serialized_end=18008 + _globals['_CONVERSATIONTOOLCALLSOFFUNCTION']._serialized_start=18010 + _globals['_CONVERSATIONTOOLCALLSOFFUNCTION']._serialized_end=18076 + _globals['_CONVERSATIONMESSAGECONTENT']._serialized_start=18078 + _globals['_CONVERSATIONMESSAGECONTENT']._serialized_end=18120 + _globals['_CONVERSATIONRESULT']._serialized_start=18123 + _globals['_CONVERSATIONRESULT']._serialized_end=18315 + _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_start=15715 + _globals['_CONVERSATIONRESULT_PARAMETERSENTRY']._serialized_end=15786 + _globals['_CONVERSATIONRESULTALPHA2']._serialized_start=18317 + _globals['_CONVERSATIONRESULTALPHA2']._serialized_end=18410 + _globals['_CONVERSATIONRESULTCHOICES']._serialized_start=18413 + _globals['_CONVERSATIONRESULTCHOICES']._serialized_end=18545 + _globals['_CONVERSATIONRESULTMESSAGE']._serialized_start=18547 + _globals['_CONVERSATIONRESULTMESSAGE']._serialized_end=18657 + _globals['_CONVERSATIONRESPONSE']._serialized_start=18659 + _globals['_CONVERSATIONRESPONSE']._serialized_end=18783 + _globals['_CONVERSATIONRESPONSEALPHA2']._serialized_start=18786 + _globals['_CONVERSATIONRESPONSEALPHA2']._serialized_end=18920 + _globals['_CONVERSATIONTOOLS']._serialized_start=18922 + _globals['_CONVERSATIONTOOLS']._serialized_end=19025 + _globals['_CONVERSATIONTOOLSFUNCTION']._serialized_start=19028 + _globals['_CONVERSATIONTOOLSFUNCTION']._serialized_end=19156 + _globals['_DAPR']._serialized_start=19248 + _globals['_DAPR']._serialized_end=25703 # @@protoc_insertion_point(module_scope) diff --git a/dapr/proto/runtime/v1/dapr_pb2.pyi b/dapr/proto/runtime/v1/dapr_pb2.pyi index ed0c2745..cdc0ba89 100644 --- a/dapr/proto/runtime/v1/dapr_pb2.pyi +++ b/dapr/proto/runtime/v1/dapr_pb2.pyi @@ -23,6 +23,7 @@ import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.internal.enum_type_wrapper import google.protobuf.message +import google.protobuf.struct_pb2 import google.protobuf.timestamp_pb2 import sys import typing @@ -4103,24 +4104,6 @@ class ConversationToolsFunction(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - @typing.final - class ParametersEntry(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - KEY_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - key: builtins.str - @property - def value(self) -> google.protobuf.any_pb2.Any: ... - def __init__( - self, - *, - key: builtins.str = ..., - value: google.protobuf.any_pb2.Any | None = ..., - ) -> None: ... - def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... - NAME_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int PARAMETERS_FIELD_NUMBER: builtins.int @@ -4131,7 +4114,7 @@ class ConversationToolsFunction(google.protobuf.message.Message): used by the model to choose when and how to call the function. """ @property - def parameters(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, google.protobuf.any_pb2.Any]: + def parameters(self) -> google.protobuf.struct_pb2.Struct: """The parameters the functions accepts, described as a JSON Schema object. See the [guide](https://platform.openai.com/docs/guides/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. @@ -4143,9 +4126,9 @@ class ConversationToolsFunction(google.protobuf.message.Message): *, name: builtins.str = ..., description: builtins.str | None = ..., - parameters: collections.abc.Mapping[builtins.str, google.protobuf.any_pb2.Any] | None = ..., + parameters: google.protobuf.struct_pb2.Struct | None = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description"]) -> builtins.bool: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "parameters", b"parameters"]) -> builtins.bool: ... def ClearField(self, field_name: typing.Literal["_description", b"_description", "description", b"description", "name", b"name", "parameters", b"parameters"]) -> None: ... def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... diff --git a/dapr/proto/runtime/v1/dapr_pb2_grpc.py b/dapr/proto/runtime/v1/dapr_pb2_grpc.py index bd32a083..c739588a 100644 --- a/dapr/proto/runtime/v1/dapr_pb2_grpc.py +++ b/dapr/proto/runtime/v1/dapr_pb2_grpc.py @@ -1,31 +1,11 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc -import warnings from dapr.proto.common.v1 import common_pb2 as dapr_dot_proto_dot_common_dot_v1_dot_common__pb2 from dapr.proto.runtime.v1 import dapr_pb2 as dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -GRPC_GENERATED_VERSION = '1.74.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in dapr/proto/runtime/v1/dapr_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - class DaprStub(object): """Dapr service provides APIs to user application to access Dapr building blocks. @@ -41,302 +21,302 @@ def __init__(self, channel): '/dapr.proto.runtime.v1.Dapr/InvokeService', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeServiceRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - _registered_method=True) + ) self.GetState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateResponse.FromString, - _registered_method=True) + ) self.GetBulkState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetBulkState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateResponse.FromString, - _registered_method=True) + ) self.SaveState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SaveState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SaveStateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.QueryStateAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/QueryStateAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateResponse.FromString, - _registered_method=True) + ) self.DeleteState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/DeleteState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteStateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.DeleteBulkState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/DeleteBulkState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteBulkStateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.ExecuteStateTransaction = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ExecuteStateTransaction', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteStateTransactionRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.PublishEvent = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PublishEvent', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PublishEventRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.BulkPublishEventAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/BulkPublishEventAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishResponse.FromString, - _registered_method=True) + ) self.SubscribeTopicEventsAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.FromString, - _registered_method=True) + ) self.InvokeBinding = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/InvokeBinding', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingResponse.FromString, - _registered_method=True) + ) self.GetSecret = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetSecret', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretResponse.FromString, - _registered_method=True) + ) self.GetBulkSecret = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetBulkSecret', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretResponse.FromString, - _registered_method=True) + ) self.RegisterActorTimer = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RegisterActorTimer', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorTimerRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.UnregisterActorTimer = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnregisterActorTimer', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorTimerRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.RegisterActorReminder = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RegisterActorReminder', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorReminderRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.UnregisterActorReminder = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnregisterActorReminder', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorReminderRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.GetActorState = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetActorState', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateResponse.FromString, - _registered_method=True) + ) self.ExecuteActorStateTransaction = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ExecuteActorStateTransaction', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteActorStateTransactionRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.InvokeActor = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/InvokeActor', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorResponse.FromString, - _registered_method=True) + ) self.GetConfigurationAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetConfigurationAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - _registered_method=True) + ) self.GetConfiguration = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetConfiguration', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - _registered_method=True) + ) self.SubscribeConfigurationAlpha1 = channel.unary_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeConfigurationAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - _registered_method=True) + ) self.SubscribeConfiguration = channel.unary_stream( '/dapr.proto.runtime.v1.Dapr/SubscribeConfiguration', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - _registered_method=True) + ) self.UnsubscribeConfigurationAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfigurationAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - _registered_method=True) + ) self.UnsubscribeConfiguration = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfiguration', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - _registered_method=True) + ) self.TryLockAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/TryLockAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockResponse.FromString, - _registered_method=True) + ) self.UnlockAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/UnlockAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockResponse.FromString, - _registered_method=True) + ) self.EncryptAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/EncryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptResponse.FromString, - _registered_method=True) + ) self.DecryptAlpha1 = channel.stream_stream( '/dapr.proto.runtime.v1.Dapr/DecryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptResponse.FromString, - _registered_method=True) + ) self.GetMetadata = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetMetadata', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataResponse.FromString, - _registered_method=True) + ) self.SetMetadata = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SetMetadata', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SetMetadataRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.SubtleGetKeyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleGetKeyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyResponse.FromString, - _registered_method=True) + ) self.SubtleEncryptAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleEncryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptResponse.FromString, - _registered_method=True) + ) self.SubtleDecryptAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleDecryptAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptResponse.FromString, - _registered_method=True) + ) self.SubtleWrapKeyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleWrapKeyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyResponse.FromString, - _registered_method=True) + ) self.SubtleUnwrapKeyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleUnwrapKeyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyResponse.FromString, - _registered_method=True) + ) self.SubtleSignAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleSignAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignResponse.FromString, - _registered_method=True) + ) self.SubtleVerifyAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/SubtleVerifyAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyResponse.FromString, - _registered_method=True) + ) self.StartWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/StartWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - _registered_method=True) + ) self.GetWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - _registered_method=True) + ) self.PurgeWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.TerminateWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.PauseWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PauseWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.ResumeWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.RaiseEventWorkflowAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.StartWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/StartWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - _registered_method=True) + ) self.GetWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - _registered_method=True) + ) self.PurgeWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.TerminateWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.PauseWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/PauseWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.ResumeWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.RaiseEventWorkflowBeta1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowBeta1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.Shutdown = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/Shutdown', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ShutdownRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - _registered_method=True) + ) self.ScheduleJobAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ScheduleJobAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobResponse.FromString, - _registered_method=True) + ) self.GetJobAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/GetJobAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobResponse.FromString, - _registered_method=True) + ) self.DeleteJobAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/DeleteJobAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobResponse.FromString, - _registered_method=True) + ) self.ConverseAlpha1 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ConverseAlpha1', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequest.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponse.FromString, - _registered_method=True) + ) self.ConverseAlpha2 = channel.unary_unary( '/dapr.proto.runtime.v1.Dapr/ConverseAlpha2', request_serializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequestAlpha2.SerializeToString, response_deserializer=dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponseAlpha2.FromString, - _registered_method=True) + ) class DaprServicer(object): @@ -1072,7 +1052,6 @@ def add_DaprServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'dapr.proto.runtime.v1.Dapr', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('dapr.proto.runtime.v1.Dapr', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -1091,21 +1070,11 @@ def InvokeService(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/InvokeService', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/InvokeService', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeServiceRequest.SerializeToString, dapr_dot_proto_dot_common_dot_v1_dot_common__pb2.InvokeResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetState(request, @@ -1118,21 +1087,11 @@ def GetState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetState', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetStateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetBulkState(request, @@ -1145,21 +1104,11 @@ def GetBulkState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetBulkState', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetBulkState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkStateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SaveState(request, @@ -1172,21 +1121,11 @@ def SaveState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SaveState', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SaveState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SaveStateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def QueryStateAlpha1(request, @@ -1199,21 +1138,11 @@ def QueryStateAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/QueryStateAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/QueryStateAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.QueryStateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def DeleteState(request, @@ -1226,21 +1155,11 @@ def DeleteState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/DeleteState', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/DeleteState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteStateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def DeleteBulkState(request, @@ -1253,21 +1172,11 @@ def DeleteBulkState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/DeleteBulkState', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/DeleteBulkState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteBulkStateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ExecuteStateTransaction(request, @@ -1280,21 +1189,11 @@ def ExecuteStateTransaction(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ExecuteStateTransaction', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ExecuteStateTransaction', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteStateTransactionRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def PublishEvent(request, @@ -1307,21 +1206,11 @@ def PublishEvent(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/PublishEvent', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PublishEvent', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PublishEventRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def BulkPublishEventAlpha1(request, @@ -1334,21 +1223,11 @@ def BulkPublishEventAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/BulkPublishEventAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/BulkPublishEventAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.BulkPublishResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubscribeTopicEventsAlpha1(request_iterator, @@ -1361,21 +1240,11 @@ def SubscribeTopicEventsAlpha1(request_iterator, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream( - request_iterator, - target, - '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', + return grpc.experimental.stream_stream(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/SubscribeTopicEventsAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsRequestAlpha1.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeTopicEventsResponseAlpha1.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def InvokeBinding(request, @@ -1388,21 +1257,11 @@ def InvokeBinding(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/InvokeBinding', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/InvokeBinding', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeBindingResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetSecret(request, @@ -1415,21 +1274,11 @@ def GetSecret(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetSecret', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetSecret', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetSecretResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetBulkSecret(request, @@ -1442,21 +1291,11 @@ def GetBulkSecret(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetBulkSecret', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetBulkSecret', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetBulkSecretResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def RegisterActorTimer(request, @@ -1469,21 +1308,11 @@ def RegisterActorTimer(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/RegisterActorTimer', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RegisterActorTimer', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorTimerRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def UnregisterActorTimer(request, @@ -1496,21 +1325,11 @@ def UnregisterActorTimer(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/UnregisterActorTimer', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnregisterActorTimer', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorTimerRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def RegisterActorReminder(request, @@ -1523,21 +1342,11 @@ def RegisterActorReminder(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/RegisterActorReminder', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RegisterActorReminder', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RegisterActorReminderRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def UnregisterActorReminder(request, @@ -1550,21 +1359,11 @@ def UnregisterActorReminder(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/UnregisterActorReminder', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnregisterActorReminder', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnregisterActorReminderRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetActorState(request, @@ -1577,21 +1376,11 @@ def GetActorState(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetActorState', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetActorState', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetActorStateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ExecuteActorStateTransaction(request, @@ -1604,21 +1393,11 @@ def ExecuteActorStateTransaction(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ExecuteActorStateTransaction', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ExecuteActorStateTransaction', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ExecuteActorStateTransactionRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def InvokeActor(request, @@ -1631,21 +1410,11 @@ def InvokeActor(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/InvokeActor', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/InvokeActor', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.InvokeActorResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetConfigurationAlpha1(request, @@ -1658,21 +1427,11 @@ def GetConfigurationAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetConfigurationAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetConfigurationAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetConfiguration(request, @@ -1685,21 +1444,11 @@ def GetConfiguration(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetConfiguration', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetConfiguration', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetConfigurationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubscribeConfigurationAlpha1(request, @@ -1712,21 +1461,11 @@ def SubscribeConfigurationAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_stream( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubscribeConfigurationAlpha1', + return grpc.experimental.unary_stream(request, target, '/dapr.proto.runtime.v1.Dapr/SubscribeConfigurationAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubscribeConfiguration(request, @@ -1739,21 +1478,11 @@ def SubscribeConfiguration(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_stream( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubscribeConfiguration', + return grpc.experimental.unary_stream(request, target, '/dapr.proto.runtime.v1.Dapr/SubscribeConfiguration', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubscribeConfigurationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def UnsubscribeConfigurationAlpha1(request, @@ -1766,21 +1495,11 @@ def UnsubscribeConfigurationAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfigurationAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfigurationAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def UnsubscribeConfiguration(request, @@ -1793,21 +1512,11 @@ def UnsubscribeConfiguration(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfiguration', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnsubscribeConfiguration', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnsubscribeConfigurationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def TryLockAlpha1(request, @@ -1820,21 +1529,11 @@ def TryLockAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/TryLockAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/TryLockAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TryLockResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def UnlockAlpha1(request, @@ -1847,21 +1546,11 @@ def UnlockAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/UnlockAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/UnlockAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.UnlockResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def EncryptAlpha1(request_iterator, @@ -1874,21 +1563,11 @@ def EncryptAlpha1(request_iterator, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream( - request_iterator, - target, - '/dapr.proto.runtime.v1.Dapr/EncryptAlpha1', + return grpc.experimental.stream_stream(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/EncryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.EncryptResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def DecryptAlpha1(request_iterator, @@ -1901,21 +1580,11 @@ def DecryptAlpha1(request_iterator, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.stream_stream( - request_iterator, - target, - '/dapr.proto.runtime.v1.Dapr/DecryptAlpha1', + return grpc.experimental.stream_stream(request_iterator, target, '/dapr.proto.runtime.v1.Dapr/DecryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DecryptResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetMetadata(request, @@ -1928,21 +1597,11 @@ def GetMetadata(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetMetadata', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetMetadata', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetMetadataResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SetMetadata(request, @@ -1955,21 +1614,11 @@ def SetMetadata(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SetMetadata', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SetMetadata', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SetMetadataRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleGetKeyAlpha1(request, @@ -1982,21 +1631,11 @@ def SubtleGetKeyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleGetKeyAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleGetKeyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleGetKeyResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleEncryptAlpha1(request, @@ -2009,21 +1648,11 @@ def SubtleEncryptAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleEncryptAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleEncryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleEncryptResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleDecryptAlpha1(request, @@ -2036,21 +1665,11 @@ def SubtleDecryptAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleDecryptAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleDecryptAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleDecryptResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleWrapKeyAlpha1(request, @@ -2063,21 +1682,11 @@ def SubtleWrapKeyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleWrapKeyAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleWrapKeyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleWrapKeyResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleUnwrapKeyAlpha1(request, @@ -2090,21 +1699,11 @@ def SubtleUnwrapKeyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleUnwrapKeyAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleUnwrapKeyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleUnwrapKeyResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleSignAlpha1(request, @@ -2117,21 +1716,11 @@ def SubtleSignAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleSignAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleSignAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleSignResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def SubtleVerifyAlpha1(request, @@ -2144,21 +1733,11 @@ def SubtleVerifyAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/SubtleVerifyAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/SubtleVerifyAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.SubtleVerifyResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def StartWorkflowAlpha1(request, @@ -2171,21 +1750,11 @@ def StartWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/StartWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/StartWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetWorkflowAlpha1(request, @@ -2198,21 +1767,11 @@ def GetWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def PurgeWorkflowAlpha1(request, @@ -2225,21 +1784,11 @@ def PurgeWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def TerminateWorkflowAlpha1(request, @@ -2252,21 +1801,11 @@ def TerminateWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def PauseWorkflowAlpha1(request, @@ -2279,21 +1818,11 @@ def PauseWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/PauseWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PauseWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ResumeWorkflowAlpha1(request, @@ -2306,21 +1835,11 @@ def ResumeWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def RaiseEventWorkflowAlpha1(request, @@ -2333,21 +1852,11 @@ def RaiseEventWorkflowAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def StartWorkflowBeta1(request, @@ -2360,21 +1869,11 @@ def StartWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/StartWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/StartWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.StartWorkflowResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetWorkflowBeta1(request, @@ -2387,21 +1886,11 @@ def GetWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetWorkflowResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def PurgeWorkflowBeta1(request, @@ -2414,21 +1903,11 @@ def PurgeWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PurgeWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PurgeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def TerminateWorkflowBeta1(request, @@ -2441,21 +1920,11 @@ def TerminateWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/TerminateWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.TerminateWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def PauseWorkflowBeta1(request, @@ -2468,21 +1937,11 @@ def PauseWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/PauseWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/PauseWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.PauseWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ResumeWorkflowBeta1(request, @@ -2495,21 +1954,11 @@ def ResumeWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ResumeWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ResumeWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def RaiseEventWorkflowBeta1(request, @@ -2522,21 +1971,11 @@ def RaiseEventWorkflowBeta1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowBeta1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/RaiseEventWorkflowBeta1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.RaiseEventWorkflowRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def Shutdown(request, @@ -2549,21 +1988,11 @@ def Shutdown(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/Shutdown', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/Shutdown', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ShutdownRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ScheduleJobAlpha1(request, @@ -2576,21 +2005,11 @@ def ScheduleJobAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ScheduleJobAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ScheduleJobAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ScheduleJobResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def GetJobAlpha1(request, @@ -2603,21 +2022,11 @@ def GetJobAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/GetJobAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/GetJobAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.GetJobResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def DeleteJobAlpha1(request, @@ -2630,21 +2039,11 @@ def DeleteJobAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/DeleteJobAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/DeleteJobAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.DeleteJobResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ConverseAlpha1(request, @@ -2657,21 +2056,11 @@ def ConverseAlpha1(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ConverseAlpha1', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ConverseAlpha1', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequest.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @staticmethod def ConverseAlpha2(request, @@ -2684,18 +2073,8 @@ def ConverseAlpha2(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/dapr.proto.runtime.v1.Dapr/ConverseAlpha2', + return grpc.experimental.unary_unary(request, target, '/dapr.proto.runtime.v1.Dapr/ConverseAlpha2', dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationRequestAlpha2.SerializeToString, dapr_dot_proto_dot_runtime_dot_v1_dot_dapr__pb2.ConversationResponseAlpha2.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/dev-requirements.txt b/dev-requirements.txt index ec6f841c..60daeae1 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -17,4 +17,8 @@ ruff===0.2.2 # needed for dapr-ext-workflow durabletask-dapr >= 0.2.0a7 # needed for .env file loading in examples -python-dotenv>=1.0.0 \ No newline at end of file +python-dotenv>=1.0.0 +# needed for testing enhanced schema generation features +pydantic>=2.0.0 +# needed for yaml file generation in examples +PyYAML>=6.0.2 \ No newline at end of file diff --git a/examples/conversation/.env.example b/examples/conversation/.env.example new file mode 100644 index 00000000..97956ab0 --- /dev/null +++ b/examples/conversation/.env.example @@ -0,0 +1,20 @@ +# LLM Provider API Keys +# Add your API keys for the providers you want to test + +# OpenAI +OPENAI_API_KEY=your_openai_api_key_here + +# Anthropic +ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# Mistral AI +MISTRAL_API_KEY=your_mistral_api_key_here + +# DeepSeek +DEEPSEEK_API_KEY=your_deepseek_api_key_here + +# Google AI (Gemini/Vertex) +GOOGLE_API_KEY=your_google_api_key_here + +# Optional: Default component to use if not specified +DAPR_LLM_COMPONENT_DEFAULT=openai diff --git a/examples/conversation/README.md b/examples/conversation/README.md index c793dd4b..2df56bf8 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -1,34 +1,666 @@ -# Example - Conversation API +# Dapr Python SDK - Conversation API Examples -## Step +This directory contains examples demonstrating how to use the Dapr Conversation API with the Python SDK, including real LLM provider integrations and advanced Alpha2 features. -### Prepare +## Real LLM Providers Support -- Dapr installed +The Conversation API supports real LLM providers including: -### Run Conversation Example +- **OpenAI** (GPT-4o-mini, GPT-4, etc.) +- **Anthropic** (Claude Sonnet 4, Claude Haiku, etc.) +- **Mistral** (Mistral Large, etc.) +- **DeepSeek** (DeepSeek V3, etc.) +- **Google AI** (Gemini 2.5 Flash, etc.) - +### Environment Setup +1. **Install dependencies:** + ```bash + pip install python-dotenv # For .env file support + ``` + +2. **Create .env file:** + ```bash + cp .env.example .env + ``` + +3. **Add your API keys to .env:** + ```bash + OPENAI_API_KEY=your_openai_key_here + ANTHROPIC_API_KEY=your_anthropic_key_here + MISTRAL_API_KEY=your_mistral_key_here + DEEPSEEK_API_KEY=your_deepseek_key_here + GOOGLE_API_KEY=your_google_ai_key_here + ``` + +4. **Run the comprehensive example:** + ```bash + python examples/conversation/real_llm_providers_example.py + ``` + +## Alpha2 API Features + +The Alpha2 API introduces sophisticated features: + +- **Advanced Message Types**: user, system, assistant, tool messages +- **Automatic Parameter Conversion**: Raw Python values → GrpcAny +- **Enhanced Tool Calling**: Multi-turn tool workflows +- **Function-to-Schema**: Ultimate DevEx for tool creation +- **Multi-turn Conversations**: Context accumulation across turns +- **Async Support**: Full async/await implementation + +## New Tool Creation Helpers (Alpha2) - Excellent DevEx! + +The Alpha2 API introduces powerful new helper functions that dramatically simplify tool creation and parameter handling. + +### Before (Manual GrpcAny Creation) ❌ +```python +from google.protobuf.any_pb2 import Any as GrpcAny +import json + +# Manual, error-prone approach +location_param = GrpcAny() +location_param.value = json.dumps({ + "type": "string", + "description": "City name" +}).encode() + +unit_param = GrpcAny() +unit_param.value = json.dumps({ + "type": "string", + "enum": ["celsius", "fahrenheit"] +}).encode() + +weather_tool = ConversationTools( + function=ConversationToolsFunction( + name="get_weather", + description="Get weather", + parameters={ + "location": location_param, # ✅ This part was correct + "unit": unit_param, # ✅ This part was correct + "required": ["location"] # ❌ This causes CopyFrom errors! + } + ) +) +``` + +### After (Helper Functions) ✅ +```python +from dapr.clients.grpc._helpers import create_tool + +# Clean, simple, intuitive approach +weather_tool = create_tool( + name="get_weather", + description="Get current weather", + parameters={ + "location": { + "type": "string", + "description": "City name" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + required=["location"] +) +``` + +## Understanding the Protobuf Structure + +The Dapr Conversation API uses a specific protobuf structure for tool parameters that follows the OpenAI function calling standard: + +```protobuf +// ConversationToolsFunction.parameters is a map +// The parameters map directly represents the JSON schema structure +parameters: { + "type": GrpcAny(StringValue("object")), + "properties": GrpcAny(Struct with parameter definitions), + "required": GrpcAny(ListValue(["location"])) +} +``` + +**Key insights:** +- ✅ The **parameters map IS the JSON schema** - direct field mapping +- ✅ Uses **proper protobuf types** for each schema field: + - `"type"`: `StringValue` for the schema type + - `"properties"`: `Struct` for parameter definitions + - `"required"`: `ListValue` for required field names +- ✅ **No wrapper keys** - each JSON schema field becomes a map entry +- ✅ This matches the **OpenAI function calling standard** exactly + +**Example JSON Schema:** +```json +{ + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"}, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + }, + "required": ["location"] +} +``` + +**Becomes protobuf map:** +```python +parameters = { + "type": GrpcAny(StringValue("object")), + "properties": GrpcAny(Struct({ + "location": {"type": "string", "description": "City name"}, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + })), + "required": GrpcAny(ListValue(["location"])) +} +``` + +**Resolved Issues:** +- ❌ **Old Issue**: `'type.googleapis.com/google.protobuf.StringValue' is not of type 'object'` +- ✅ **New Solution**: Direct schema field mapping with proper protobuf types + +### Automatic Parameter Conversion + +Parameters are now automatically converted from raw Python types: + +```python +# Before: Manual GrpcAny creation for every parameter ❌ +temp_param = GrpcAny() +temp_param.Pack(DoubleValue(value=0.7)) + +# After: Raw Python values automatically converted ✅ +response = client.converse_alpha2( + name="my-provider", + inputs=[input_alpha2], + parameters={ + 'temperature': 0.7, # float -> GrpcAny + 'max_tokens': 1000, # int -> GrpcAny + 'stream': False, # bool -> GrpcAny + 'model': 'gpt-4', # string -> GrpcAny + 'config': { # dict -> GrpcAny (JSON) + 'features': ['a', 'b'], # nested arrays supported + 'enabled': True # nested values converted + } + } +) +``` + +### Function-to-Schema Approach (Ultimate DevEx!) + +The most advanced approach: define typed Python functions and automatically generate tool schemas: + +```python +from typing import Optional, List +from enum import Enum +from dapr.clients.grpc._schema_helpers import function_to_json_schema +from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools + +class Units(Enum): + CELSIUS = "celsius" + FAHRENHEIT = "fahrenheit" + +def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: + '''Get current weather for a location. + + Args: + location: The city and state or country + unit: Temperature unit preference + ''' + return f"Weather in {location}" + +# Automatically generate schema from function +schema = function_to_json_schema(get_weather) +function = ConversationToolsFunction( + name="get_weather", + description="Get current weather for a location", + parameters=schema +) +weather_tool = ConversationTools(function=function) +``` + +**Benefits:** +- ✅ **Type Safety**: Full Python type hint support (str, int, List, Optional, Enum, etc.) +- ✅ **Auto-Documentation**: Docstring parsing for parameter descriptions +- ✅ **Ultimate DevEx**: Define functions, get tools automatically +- ✅ **90%+ less boilerplate** compared to manual schema creation + +### Multiple Tool Creation Approaches + +#### 1. Simple Properties (Recommended) +```python +create_tool( + name="get_weather", + description="Get weather", + parameters={ + "location": {"type": "string", "description": "City"}, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + }, + required=["location"] +) +``` + +#### 2. Full JSON Schema +```python +create_tool( + name="calculate", + description="Perform calculations", + parameters={ + "type": "object", + "properties": { + "expression": {"type": "string", "description": "Math expression"} + }, + "required": ["expression"] + } +) +``` + +#### 3. No Parameters +```python +create_tool( + name="get_time", + description="Get current time" +) +``` + +#### 4. Complex Schema with Arrays +```python +create_tool( + name="search", + description="Search the web", + parameters={ + "query": {"type": "string"}, + "domains": { + "type": "array", + "items": {"type": "string"} + } + }, + required=["query"] +) +``` + +## Advanced Message Types (Alpha2) + +Alpha2 supports sophisticated message structures for complex conversations: + +### User Messages +```python +from dapr.clients.grpc._request import ( + ConversationMessage, + ConversationMessageOfUser, + ConversationMessageContent +) + +user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="What's the weather in Paris?")] + ) +) +``` + +### System Messages +```python +system_message = ConversationMessage( + of_system=ConversationMessageOfSystem( + content=[ConversationMessageContent(text="You are a helpful AI assistant.")] + ) +) +``` + +### Assistant Messages +```python +assistant_message = ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=[ConversationMessageContent(text="I can help you with that!")], + tool_calls=[...] # Optional tool calls + ) +) +``` + +### Tool Messages (for tool responses) +```python +tool_message = ConversationMessage( + of_tool=ConversationMessageOfTool( + tool_id="call_123", + name="get_weather", + content=[ConversationMessageContent(text="Weather: 72°F, sunny")] + ) +) +``` + +## Multi-turn Conversations + +Alpha2 excels at multi-turn conversations with proper context accumulation: + +```python +from dapr.clients.grpc._request import ConversationInputAlpha2 + +# Build conversation history +conversation_history = [] + +# Turn 1: User asks question +user_message = create_user_message("What's the weather in San Francisco?") +conversation_history.append(user_message) + +# LLM responds (potentially with tool calls) +response1 = client.converse_alpha2( + name="openai", + inputs=[ConversationInputAlpha2(messages=conversation_history)], + tools=[weather_tool] +) + +# Add LLM response to history +assistant_message = convert_llm_response_to_conversation_message(response1.outputs[0].choices[0].message) +conversation_history.append(assistant_message) + +# Turn 2: Follow-up question with full context +user_message2 = create_user_message("Should I bring an umbrella?") +conversation_history.append(user_message2) + +response2 = client.converse_alpha2( + name="openai", + inputs=[ConversationInputAlpha2(messages=conversation_history)], + tools=[weather_tool] +) +``` + +## Async Support + +Full async/await support for non-blocking operations: + +```python +from dapr.aio.clients import DaprClient as AsyncDaprClient + +async def async_conversation(): + async with AsyncDaprClient() as client: + user_message = create_user_message("Tell me a joke about async programming.") + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = await client.converse_alpha2( + name="openai", + inputs=[input_alpha2], + parameters={'temperature': 0.7} + ) + + return response.outputs[0].choices[0].message.content + +# Run async function +result = asyncio.run(async_conversation()) +``` + +## Benefits + +- ✅ **80%+ less boilerplate code** +- ✅ **No more CopyFrom() errors** +- ✅ **Automatic type conversion** +- ✅ **Multiple input formats** +- ✅ **JSON Schema validation hints** +- ✅ **Clean, readable code** +- ✅ **Supports complex nested structures** +- ✅ **Real LLM provider integration** +- ✅ **Multi-turn conversation support** +- ✅ **Function-to-schema automation** +- ✅ **Full async/await support** + +## Dapr Component Configuration + +For real LLM providers, you need Dapr component configurations. The example automatically creates these: + +### OpenAI Component Example +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: openai +spec: + type: conversation.openai + version: v1 + metadata: + - name: key + value: "your_openai_api_key" + - name: model + value: "gpt-4o-mini" +``` + +### Running with Dapr Sidecar ```bash -dapr run --app-id conversation \ - --log-level debug \ - --resources-path ./config \ - -- python3 conversation.py +# The example creates temporary component configs and shows you the command: +dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path /tmp/dapr-llm-components-xyz/ ``` - +## Helper Functions + +Convert LLM responses for multi-turn conversations: -## Result +```python +from dapr.clients.grpc._response import ConversationResultMessage +def convert_llm_response_to_conversation_message(result_message: ConversationResultMessage) -> ConversationMessage: + """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input).""" + content = [] + if result_message.content: + content = [ConversationMessageContent(text=result_message.content)] + + tool_calls = result_message.tool_calls or [] + + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=content, + tool_calls=tool_calls + ) + ) + +# Usage in multi-turn conversations +response = client.converse_alpha2(name="openai", inputs=[input_alpha2], tools=[tool]) +choice = response.outputs[0].choices[0] +assistant_message = convert_llm_response_to_conversation_message(choice.message) +conversation_history.append(assistant_message) ``` - - '== APP == Result: What's Dapr?' - - '== APP == Result: Give a brief overview.' -``` \ No newline at end of file + +## Examples in This Directory + +- **`real_llm_providers_example.py`** - Comprehensive Alpha2 examples with real providers + - Real LLM provider setup (OpenAI, Anthropic, Mistral, DeepSeek, Google AI) + - Advanced tool calling workflows + - Multi-turn conversations with context accumulation + - Function-to-schema automatic tool generation + - Both sync and async implementations + - Parameter conversion demonstration + - Backward compatibility with Alpha1 + +- **`conversation.py`** - Basic conversation examples + - Simple Alpha1 conversation flow + - Basic tool calling setup + +- **Configuration files:** + - `.env.example` - Environment variables template + - `config/` directory - Provider-specific component configurations + +## Quick Start + +### Basic Alpha2 Conversation with Tool Calling + +```python +from dapr.clients import DaprClient +from dapr.clients.grpc._request import ( + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageOfUser, + ConversationMessageContent, + ConversationToolsFunction, + ConversationTools +) + +# Create a tool using the simple approach +function = ConversationToolsFunction( + name="get_weather", + description="Get current weather for a location", + parameters={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state or country" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "Temperature unit" + } + }, + "required": ["location"] + } +) +weather_tool = ConversationTools(function=function) + +# Create a user message +user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="What's the weather in Paris?")] + ) +) + +# Create input and make the request +input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + +with DaprClient() as client: + response = client.converse_alpha2( + name="openai", # or "anthropic", "mistral", etc. + inputs=[input_alpha2], + parameters={ + 'temperature': 0.7, # Auto-converted to GrpcAny! + 'max_tokens': 500, # Auto-converted to GrpcAny! + 'stream': False # Auto-converted to GrpcAny! + }, + tools=[weather_tool], + tool_choice='auto' + ) + + # Process the response + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: + print(f"LLM wants to call: {choice.message.tool_calls[0].function.name}") + print(f"With arguments: {choice.message.tool_calls[0].function.arguments}") + else: + print(f"LLM response: {choice.message.content}") +``` + +### Real Provider Setup + +1. Set up environment: + ```bash + export OPENAI_API_KEY="your_key_here" + ``` + +2. Create component configuration (`components/openai.yaml`): + ```yaml + apiVersion: dapr.io/v1alpha1 + kind: Component + metadata: + name: openai + spec: + type: conversation.openai + version: v1 + metadata: + - name: key + value: "your_openai_api_key" + - name: model + value: "gpt-4o-mini" + ``` + +3. Start Dapr sidecar: + ```bash + dapr run --app-id test-app --dapr-grpc-port 50001 --resources-path ./components/ + ``` + +4. Run your conversation code! + +For a complete working example with multiple providers, see `real_llm_providers_example.py`. + +## Troubleshooting + +### Common Issues + +1. **No LLM providers configured** + - Ensure API keys are set in environment variables or `.env` file + - Check that component configurations are correctly formatted + +2. **Tool calls not working** + - Verify tool schema is properly formatted (use examples as reference) + - Check that `tool_choice` is set to `'auto'` or specific tool name + - Ensure LLM provider supports function calling + +3. **Multi-turn context issues** + - Use `convert_llm_response_to_conversation_message()` helper function + - Maintain conversation history across turns + - Include all previous messages in subsequent requests + +4. **Parameter conversion errors** + - Alpha2 automatically converts raw Python values to GrpcAny + - No need to manually create GrpcAny objects for parameters + - Supported types: int, float, bool, str, dict, list + +### Environment Variables + +```bash +# Required for respective providers +OPENAI_API_KEY=sk-... +ANTHROPIC_API_KEY=sk-ant-... +MISTRAL_API_KEY=... +DEEPSEEK_API_KEY=... +GOOGLE_API_KEY=... + +# Optional: Use local development build +USE_LOCAL_DEV=true +BUILD_LOCAL_DAPR=true +``` + +## Migration from Alpha1 to Alpha2 + +Alpha2 provides significant improvements while maintaining backward compatibility: + +### Alpha1 (Legacy) +```python +from dapr.clients.grpc._request import ConversationInput + +inputs = [ConversationInput( + content="Hello!", + role="user" +)] + +response = client.converse_alpha1( + name="provider", + inputs=inputs, + parameters={'temperature': 0.7} +) +``` + +### Alpha2 (Recommended) +```python +from dapr.clients.grpc._request import ConversationInputAlpha2, ConversationMessage + +user_message = ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="Hello!")] + ) +) + +response = client.converse_alpha2( + name="provider", + inputs=[ConversationInputAlpha2(messages=[user_message])], + parameters={'temperature': 0.7} # Auto-converted! +) +``` + +## Features Overview + +| Feature | Alpha1 | Alpha2 | +|---------|--------|--------| +| Basic Conversations | ✅ | ✅ | +| Tool Calling | ✅ | ✅ Enhanced | +| Multi-turn Context | ❌ | ✅ | +| Advanced Message Types | ❌ | ✅ | +| Parameter Auto-conversion | ❌ | ✅ | +| Function-to-Schema | ❌ | ✅ | +| Async Support | ✅ | ✅ Enhanced | +| Real LLM Providers | ✅ | ✅ | + +**Recommendation:** Use Alpha2 for new projects and consider migrating existing Alpha1 code to benefit from enhanced features and improved developer experience. \ No newline at end of file diff --git a/examples/conversation/config/anthropic.yaml b/examples/conversation/config/anthropic.yaml new file mode 100644 index 00000000..a098425a --- /dev/null +++ b/examples/conversation/config/anthropic.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: anthropic +spec: + type: conversation.anthropic + version: v1 + metadata: + - name: key + value: ${ANTHROPIC_API_KEY} + - name: model + value: claude-sonnet-4-20250514 diff --git a/examples/conversation/config/deepseek.yaml b/examples/conversation/config/deepseek.yaml new file mode 100644 index 00000000..ab1d465d --- /dev/null +++ b/examples/conversation/config/deepseek.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: deepseek +spec: + type: conversation.deepseek + version: v1 + metadata: + - name: key + value: ${DEEPSEEK_API_KEY} + - name: model + value: deepseek-chat diff --git a/examples/conversation/config/google.yaml b/examples/conversation/config/google.yaml new file mode 100644 index 00000000..670f40a8 --- /dev/null +++ b/examples/conversation/config/google.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: google +spec: + type: conversation.googleai + version: v1 + metadata: + - name: key + value: ${GOOGLE_API_KEY} + - name: model + value: gemini-2.5-pro diff --git a/examples/conversation/config/mistral.yaml b/examples/conversation/config/mistral.yaml new file mode 100644 index 00000000..0e037ce6 --- /dev/null +++ b/examples/conversation/config/mistral.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: mistral +spec: + type: conversation.mistral + version: v1 + metadata: + - name: key + value: ${MISTRAL_API_KEY} + - name: model + value: mistral-large-latest diff --git a/examples/conversation/config/openai.yaml b/examples/conversation/config/openai.yaml new file mode 100644 index 00000000..edca7c99 --- /dev/null +++ b/examples/conversation/config/openai.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: openai +spec: + type: conversation.openai + version: v1 + metadata: + - name: key + value: ${OPENAI_API_KEY} + - name: model + value: gpt-4o-mini-2024-07-18 diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py new file mode 100644 index 00000000..1c6a7322 --- /dev/null +++ b/examples/conversation/real_llm_providers_example.py @@ -0,0 +1,1230 @@ +#!/usr/bin/env python3 + +""" +Real LLM Providers Example for Dapr Conversation API (Alpha2) + +This example demonstrates how to use real LLM providers (OpenAI, Anthropic, etc.) +with the Dapr Conversation API Alpha2. It showcases the latest features including: +- Advanced message types (user, system, assistant, developer, tool) +- Automatic parameter conversion (raw Python values) +- Enhanced tool calling capabilities +- Multi-turn conversations +- Both sync and async implementations + +Prerequisites: +1. Set up API keys in .env file (copy from .env.example) +2. For manual mode: Start Dapr sidecar manually + +Usage: + # Automatic mode (recommended) - manages Dapr sidecar automatically + python examples/conversation/real_llm_providers_example.py + + # Manual mode - requires manual Dapr sidecar setup + python examples/conversation/real_llm_providers_example.py + + # Show help + python examples/conversation/real_llm_providers_example.py --help + +Environment Variables: + OPENAI_API_KEY: OpenAI API key + ANTHROPIC_API_KEY: Anthropic API key + MISTRAL_API_KEY: Mistral API key + DEEPSEEK_API_KEY: DeepSeek API key + GOOGLE_API_KEY: Google AI (Gemini) API key +""" + +import asyncio +import json +import os +import sys +import tempfile +from pathlib import Path +from typing import Any, Dict, List, Optional + +import yaml + +# Add the parent directory to the path so we can import local dapr sdk +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +# Load environment variables from .env file if available +try: + from dotenv import load_dotenv + + DOTENV_AVAILABLE = True +except ImportError: + DOTENV_AVAILABLE = False + print('⚠️ python-dotenv not installed. Install with: pip install python-dotenv') + +from dapr.aio.clients import DaprClient as AsyncDaprClient +from dapr.clients import DaprClient +from dapr.clients.grpc._request import ( + ConversationInput, + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageContent, + ConversationMessageOfUser, + ConversationMessageOfSystem, + ConversationMessageOfAssistant, + ConversationMessageOfTool, + ConversationToolCalls, + ConversationToolCallsOfFunction, + ConversationTools, + ConversationToolsFunction, +) +from google.protobuf.wrappers_pb2 import DoubleValue, Int32Value, BoolValue, StringValue +from dapr.clients.grpc._response import ConversationResultMessage + + +def convert_llm_response_to_conversation_message( + result_message: ConversationResultMessage, +) -> ConversationMessage: + """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input). + + This standalone utility function makes it easy to append LLM responses to conversation history + and reuse them as input for subsequent conversation turns in multi-turn scenarios. + + Args: + result_message: ConversationResultMessage from LLM response (choice.message) + + Returns: + ConversationMessage suitable for input to next conversation turn + + Example: + >>> response = client.converse_alpha2(name="openai", inputs=[input_alpha2], tools=[tool]) + >>> choice = response.outputs[0].choices[0] + >>> + >>> # Convert LLM response to conversation message + >>> assistant_message = convert_llm_response_to_conversation_message(choice.message) + >>> conversation_history.append(assistant_message) + >>> + >>> # Use in next turn + >>> next_input = ConversationInputAlpha2(messages=conversation_history) + >>> next_response = client.converse_alpha2(name="openai", inputs=[next_input]) + """ + # Convert content string to ConversationMessageContent list + content = [] + if result_message.content: + content = [ConversationMessageContent(text=result_message.content)] + + # Convert tool_calls if present (they're already the right type) + tool_calls = result_message.tool_calls or [] + + # Create assistant message (since LLM responses are always assistant messages) + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant(content=content, tool_calls=tool_calls) + ) + + +class RealLLMProviderTester: + """Test real LLM providers with Dapr Conversation API Alpha2. + + This example demonstrates the new schema-based tool creation approach + using ConversationToolsFunction.from_schema() for proper JSON Schema handling. + + ## Tool Creation Examples + + ### 1. Simple Properties Approach (Recommended for most cases) + ```python + from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools + + function = ConversationToolsFunction.from_schema( + name="get_weather", + description="Get current weather", + json_schema={ + "properties": { + "location": { + "type": "string", + "description": "City name" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + ) + weather_tool = ConversationTools(function=function) + ``` + + ### 2. Function-to-Schema Approach (Ultimate DevEx - NEW!) + ```python + from typing import Optional, List + from enum import Enum + from dapr.clients.grpc._helpers import function_to_json_schema + + class Units(Enum): + CELSIUS = "celsius" + FAHRENHEIT = "fahrenheit" + + def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: + '''Get current weather for a location. + + Args: + location: The city and state or country + unit: Temperature unit preference + ''' + return f"Weather in {location}" + + # Automatically generate schema from function + schema = function_to_json_schema(get_weather) + function = ConversationToolsFunction.from_schema( + name="get_weather", + description="Get current weather for a location", + json_schema=schema + ) + weather_tool = ConversationTools(function=function) + + # Or use the complete helper (when available): + # from dapr.clients.grpc._helpers import create_tool_from_function + # weather_tool = create_tool_from_function(get_weather) + ``` + + ### 3. Full JSON Schema Approach (For complex schemas) + ```python + function = ConversationToolsFunction.from_schema( + name="calculate", + description="Perform calculations", + json_schema={ + "properties": { + "expression": { + "type": "string", + "description": "Math expression" + } + }, + "required": ["expression"] + } + ) + calc_tool = ConversationTools(function=function) + ``` + + ### 4. No Parameters Approach + ```python + function = ConversationToolsFunction.from_schema( + name="get_current_time", + description="Get current date and time", + json_schema={ + "properties": {}, + "required": [] + } + ) + time_tool = ConversationTools(function=function) + ``` + + ### 5. Complex Schema with Arrays and Nested Objects + ```python + function = ConversationToolsFunction.from_schema( + name="web_search", + description="Search the web", + json_schema={ + "properties": { + "query": {"type": "string"}, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "domains": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["query"] + } + ) + search_tool = ConversationTools(function=function) + ``` + + ### 6. Automatic Parameter Conversion + The client now automatically converts raw Python values to GrpcAny: + ```python + response = client.converse_alpha2( + name="my-provider", + inputs=[input_alpha2], + parameters={ + 'temperature': 0.7, + 'max_tokens': 100, + 'stream': False, + 'model': 'gpt-4', + }, + tools=[weather_tool] + ) + ``` + + ## Benefits + - ✅ **Function-to-Schema**: Ultimate DevEx - define typed functions, get tools automatically + - ✅ **Type Safety**: Full Python type hint support (str, int, List, Optional, Enum, etc.) + - ✅ **Auto-Documentation**: Docstring parsing for parameter descriptions + - ✅ **Proper JSON Schema**: ConversationToolsFunction.from_schema() handles protobuf correctly + - ✅ **Automatic Parameter Conversion**: Raw Python values → GrpcAny at gRPC call level + - ✅ **OpenAI Compatibility**: 100% compatible with OpenAI function calling standard + - ✅ **Multiple Approaches**: Choose the right level of abstraction for your needs + - ✅ **Clean Separation**: Tool creation separated from protobuf conversion + - ✅ **Reduced Complexity**: 90%+ less boilerplate compared to manual schema creation + """ + + def __init__(self, use_local_dev: bool = False): + self.available_providers = {} + self.component_configs = {} + self.components_dir = None + self.use_local_dev = use_local_dev + # Note: Local dev sidecar management not implemented in this example + self.sidecar_manager = None # DaprSidecarManager() if use_local_dev else None + + def load_environment(self) -> None: + """Load environment variables from .env file if available.""" + if DOTENV_AVAILABLE: + env_file = Path(__file__).parent / '.env' + if env_file.exists(): + load_dotenv(env_file) + print(f'📁 Loaded environment from {env_file}') + else: + print(f'⚠️ No .env file found at {env_file}') + print(' Copy .env.example to .env and add your API keys') + else: + print('⚠️ python-dotenv not available, using system environment variables') + + def detect_available_providers(self) -> Dict[str, Dict[str, Any]]: + """Detect which LLM providers are available based on API keys.""" + providers = {} + + # OpenAI + if os.getenv('OPENAI_API_KEY'): + providers['openai'] = { + 'display_name': 'OpenAI GPT-4o-mini', + 'component_type': 'conversation.openai', + 'api_key_env': 'OPENAI_API_KEY', + 'metadata': [ + {'name': 'key', 'value': os.getenv('OPENAI_API_KEY')}, + {'name': 'model', 'value': 'gpt-4o-mini'}, + ], + } + + # Anthropic + if os.getenv('ANTHROPIC_API_KEY'): + providers['anthropic'] = { + 'display_name': 'Anthropic Claude Sonnet 4', + 'component_type': 'conversation.anthropic', + 'api_key_env': 'ANTHROPIC_API_KEY', + 'metadata': [ + {'name': 'key', 'value': os.getenv('ANTHROPIC_API_KEY')}, + {'name': 'model', 'value': 'claude-sonnet-4-20250514'}, + ], + } + + # Mistral + if os.getenv('MISTRAL_API_KEY'): + providers['mistral'] = { + 'display_name': 'Mistral Large', + 'component_type': 'conversation.mistral', + 'api_key_env': 'MISTRAL_API_KEY', + 'metadata': [ + {'name': 'key', 'value': os.getenv('MISTRAL_API_KEY')}, + {'name': 'model', 'value': 'mistral-large-latest'}, + ], + } + + # DeepSeek + if os.getenv('DEEPSEEK_API_KEY'): + providers['deepseek'] = { + 'display_name': 'DeepSeek V3', + 'component_type': 'conversation.deepseek', + 'api_key_env': 'DEEPSEEK_API_KEY', + 'metadata': [ + {'name': 'key', 'value': os.getenv('DEEPSEEK_API_KEY')}, + {'name': 'model', 'value': 'deepseek-chat'}, + ], + } + + # Google AI (Gemini) + if os.getenv('GOOGLE_API_KEY'): + providers['google'] = { + 'display_name': 'Google Gemini 2.5 Flash', + 'component_type': 'conversation.googleai', + 'api_key_env': 'GOOGLE_API_KEY', + 'metadata': [ + {'name': 'key', 'value': os.getenv('GOOGLE_API_KEY')}, + {'name': 'model', 'value': 'gemini-2.5-flash'}, + ], + } + + return providers + + def create_component_configs(self, selected_providers: Optional[List[str]] = None) -> str: + """Create Dapr component configurations for available providers (those with API keys exposed).""" + # Create temporary directory for components + self.components_dir = tempfile.mkdtemp(prefix='dapr-llm-components-') + + # If no specific providers selected, use OpenAI as default (most reliable) + if not selected_providers: + selected_providers = ( + ['openai'] + if 'openai' in self.available_providers + else list(self.available_providers.keys())[:1] + ) + + for provider_id in selected_providers: + if provider_id not in self.available_providers: + continue + + config = self.available_providers[provider_id] + component_config = { + 'apiVersion': 'dapr.io/v1alpha1', + 'kind': 'Component', + 'metadata': {'name': provider_id}, + 'spec': { + 'type': config['component_type'], + 'version': 'v1', + 'metadata': config['metadata'], + }, + } + + # Write component file + component_file = Path(self.components_dir) / f'{provider_id}.yaml' + with open(component_file, 'w') as f: + yaml.dump(component_config, f, default_flow_style=False) + + print(f'📝 Created component: {component_file}') + + return self.components_dir + + def create_weather_tool(self) -> ConversationTools: + """Create a weather tool for testing Alpha2 tool calling.""" + # Using the new ConversationToolsFunction.from_schema approach + function = ConversationToolsFunction( + name='get_weather', + description='Get the current weather for a location', + parameters={ + 'type': 'object', + 'properties': { + 'location': {'type': 'string', 'description': 'The city and state or country'}, + 'unit': { + 'type': 'string', + 'enum': ['celsius', 'fahrenheit'], + 'description': 'Temperature unit', + }, + }, + 'required': ['location'], + }, + ) + return ConversationTools(function=function) + + def create_calculator_tool(self) -> ConversationTools: + """Create a calculator tool using full JSON schema approach.""" + function = ConversationToolsFunction( + name='calculate', + description='Perform mathematical calculations', + parameters={ + 'type': 'object', + 'properties': { + 'expression': { + 'type': 'string', + 'description': "Mathematical expression to evaluate (e.g., '2+2', 'sqrt(16)')", + } + }, + 'required': ['expression'], + }, + ) + return ConversationTools(function=function) + + def create_time_tool(self) -> ConversationTools: + """Create a simple tool with no parameters.""" + function = ConversationToolsFunction( + name='get_current_time', + description='Get the current date and time', + parameters={'type': 'object', 'properties': {}, 'required': []}, + ) + return ConversationTools(function=function) + + def create_search_tool(self) -> ConversationTools: + """Create a more complex tool with multiple parameter types.""" + function = ConversationToolsFunction( + name='web_search', + description='Search the web for information', + parameters={ + 'type': 'object', + 'properties': { + 'query': {'type': 'string', 'description': 'Search query'}, + 'limit': { + 'type': 'integer', + 'description': 'Maximum number of results', + 'minimum': 1, + 'maximum': 10, + 'default': 5, + }, + 'include_images': { + 'type': 'boolean', + 'description': 'Whether to include image results', + 'default': False, + }, + 'domains': { + 'type': 'array', + 'items': {'type': 'string'}, + 'description': 'Limit search to specific domains', + }, + }, + 'required': ['query'], + }, + ) + return ConversationTools(function=function) + + def create_tool_from_typed_function_example(self) -> ConversationTools: + """Demonstrate creating tools from typed Python functions - Ultimate DevEx! + + This shows the most advanced approach: define a typed function and automatically + generate the complete tool schema from type hints and docstrings. + """ + from typing import Optional, List + from enum import Enum + from dapr.clients.grpc._schema_helpers import function_to_json_schema + + # Define the tool behavior as a regular Python function with type hints + class PriceRange(Enum): + BUDGET = 'budget' + MODERATE = 'moderate' + EXPENSIVE = 'expensive' + + def find_restaurants( + location: str, + cuisine: str = 'any', + price_range: PriceRange = PriceRange.MODERATE, + max_results: int = 5, + dietary_restrictions: Optional[List[str]] = None, + ) -> str: + """Find restaurants in a specific location. + + Args: + location: The city or neighborhood to search + cuisine: Type of cuisine (italian, chinese, mexican, etc.) + price_range: Budget preference for dining + max_results: Maximum number of restaurant recommendations + dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) + """ + # This would contain actual implementation + return f'Found restaurants in {location} serving {cuisine} food' + + # Automatically generate JSON schema from the function + schema = function_to_json_schema(find_restaurants) + + # Create the tool using the generated schema + function = ConversationToolsFunction( + name='find_restaurants', + description='Find restaurants in a specific location', + parameters=schema, + ) + + return ConversationTools(function=function) + + def execute_weather_tool(self, location: str, unit: str = 'fahrenheit') -> str: + """Simulate weather tool execution.""" + temp = '72°F' if unit == 'fahrenheit' else '22°C' + return f'The weather in {location} is sunny with a temperature of {temp}.' + + def create_user_message(self, text: str) -> ConversationMessage: + """Helper to create a user message for Alpha2.""" + return ConversationMessage( + of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) + ) + + def create_system_message(self, text: str) -> ConversationMessage: + """Helper to create a system message for Alpha2.""" + return ConversationMessage( + of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) + ) + + def create_assistant_message(self, text: str) -> ConversationMessage: + """Helper to create an assistant message for Alpha2.""" + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=[ConversationMessageContent(text=text)] + ) + ) + + def create_tool_message(self, tool_id: str, name: str, content: str) -> ConversationMessage: + """Helper to create a tool message for Alpha2 responses (from client to LLM).""" + return ConversationMessage( + of_tool=ConversationMessageOfTool( + tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] + ) + ) + + def create_tool_call_message( + self, tool_id: str, name: str, arguments: str + ) -> ConversationMessage: + """Helper to create a tool call message for Alpha2 responses (from LLM to client).""" + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + tool_calls=[ + ConversationToolCalls( + id=tool_id, + function=ConversationToolCallsOfFunction(name=name, arguments=arguments), + ) + ] + ) + ) + + def convert_result_message_to_input_message( + self, result_message: ConversationResultMessage + ) -> ConversationMessage: + """Convert ConversationResultMessage to ConversationMessage for reuse in conversation history. + + This utility makes it easy to append LLM responses to conversation history + and use them as input for subsequent turns. + + Args: + result_message: ConversationResultMessage from LLM response + + Returns: + ConversationMessage suitable for input to next conversation turn + """ + # Delegate to standalone utility function + return convert_llm_response_to_conversation_message(result_message) + + def test_basic_conversation_alpha2(self, provider_id: str) -> None: + """Test basic Alpha2 conversation with a provider.""" + print( + f"\n💬 Testing Alpha2 basic conversation with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + # Create Alpha2 conversation input with sophisticated message structure + user_message = self.create_user_message( + "Hello! Please respond with exactly: 'Hello from Dapr Alpha2!'" + ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + # Use new parameter conversion (raw Python values automatically converted) + response = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + parameters={ + 'temperature': 0.7, + 'max_tokens': 100, + 'top_p': 0.9, + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + print(f'✅ Alpha2 Response: {choice.message.content}') + print(f'📊 Finish reason: {choice.finish_reason}') + else: + print('❌ No Alpha2 response received') + + except Exception as e: + print(f'❌ Alpha2 basic conversation error: {e}') + + def test_multi_turn_conversation_alpha2(self, provider_id: str) -> None: + """Test multi-turn Alpha2 conversation with different message types.""" + print( + f"\n🔄 Testing Alpha2 multi-turn conversation with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + # Create a multi-turn conversation with system, user, and assistant messages + system_message = self.create_system_message( + 'You are a helpful AI assistant. Be concise.' + ) + user_message1 = self.create_user_message('What is 2+2?') + assistant_message = self.create_assistant_message('2+2 equals 4.') + user_message2 = self.create_user_message('What about 3+3?') + + input_alpha2 = ConversationInputAlpha2( + messages=[system_message, user_message1, assistant_message, user_message2] + ) + + response = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + parameters={ + 'temperature': 0.5, + 'max_tokens': 150, + }, + ) + + if response.outputs and response.outputs[0].choices: + print( + f'✅ Multi-turn conversation processed {len(response.outputs[0].choices)} message(s)' + ) + for i, choice in enumerate(response.outputs[0].choices): + print(f' Response {i+1}: {choice.message.content[:100]}...') + else: + print('❌ No multi-turn response received') + + except Exception as e: + print(f'❌ Multi-turn conversation error: {e}') + + def test_tool_calling_alpha2(self, provider_id: str) -> None: + """Test Alpha2 tool calling with a provider.""" + print( + f"\n🔧 Testing Alpha2 tool calling with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + weather_tool = self.create_weather_tool() + user_message = self.create_user_message("What's the weather like in San Francisco?") + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + tools=[weather_tool], + tool_choice='auto', + parameters={ + 'temperature': 0.3, # Lower temperature for more consistent tool calling + 'max_tokens': 500, + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + print(f'📊 Finish reason: {choice.finish_reason}') + + if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: + print(f'🔧 Tool calls made: {len(choice.message.tool_calls)}') + for tool_call in choice.message.tool_calls: + print(f' Tool: {tool_call.function.name}') + print(f' Arguments: {tool_call.function.arguments}') + + # Execute the tool to show the workflow + try: + args = json.loads(tool_call.function.arguments) + weather_result = self.execute_weather_tool( + args.get('location', 'San Francisco'), + args.get('unit', 'fahrenheit'), + ) + print(f'🌤️ Tool executed: {weather_result}') + + # Demonstrate tool result message (for multi-turn tool workflows) + tool_result_message = self.create_tool_message( + tool_id=tool_call.id, + name=tool_call.function.name, + content=weather_result, + ) + print('✅ Alpha2 tool calling demonstration completed!') + + except json.JSONDecodeError: + print('⚠️ Could not parse tool arguments') + else: + print(f'💬 Regular response: {choice.message.content}') + else: + print('❌ No tool calling response received') + + except Exception as e: + print(f'❌ Alpha2 tool calling error: {e}') + + def test_parameter_conversion(self, provider_id: str) -> None: + """Test the new parameter conversion feature.""" + print( + f"\n🔄 Testing parameter conversion with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + user_message = self.create_user_message( + 'Tell me about the different tool creation approaches available.' + ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + # Demonstrate different tool creation approaches + weather_tool = self.create_weather_tool() # Simple properties approach + calc_tool = self.create_calculator_tool() # Full JSON schema approach + time_tool = self.create_time_tool() # No parameters approach + search_tool = self.create_search_tool() # Complex schema with arrays, etc. + + print( + f'✅ Created {len([weather_tool, calc_tool, time_tool, search_tool])} tools with different approaches!' + ) + + # Test various parameter types that are automatically converted + response = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + parameters={ + # Raw Python values - automatically converted to GrpcAny + 'temperature': 0.8, # float + 'max_tokens': 200, # int + 'top_p': 1.0, # float + 'frequency_penalty': 0.0, # float + 'presence_penalty': 0.0, # float + 'stream': False, # bool + 'tool_choice': 'none', # string + 'model': 'gpt-4o-mini', # string (provider-specific) + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + print(f'✅ Parameter conversion successful!') + print(f'✅ Tool creation helpers working perfectly!') + print(f' Response: {choice.message.content[:100]}...') + else: + print('❌ Parameter conversion test failed') + + except Exception as e: + print(f'❌ Parameter conversion error: {e}') + + def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: + """Test multi-turn Alpha2 tool calling with proper context accumulation.""" + print( + f"\n🔄🔧 Testing multi-turn tool calling with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + weather_tool = self.create_weather_tool() + conversation_history = [] + + # Turn 1: User asks about weather (include tools) + print('\n--- Turn 1: Initial weather query ---') + user_message1 = self.create_user_message( + "What's the weather like in San Francisco?" + ) + conversation_history.append(user_message1) + + print(f'📝 Request 1 context: {len(conversation_history)} messages + tools') + input_alpha2_turn1 = ConversationInputAlpha2(messages=conversation_history) + + response1 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn1], + tools=[weather_tool], # Tools included in turn 1 + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + # Check all outputs and choices for tool calls + tool_calls_found = [] + assistant_messages = [] + + for output_idx, output in enumerate(response1.outputs or []): + for choice_idx, choice in enumerate(output.choices or []): + print( + f'📋 Checking output {output_idx}, choice {choice_idx}: finish_reason={choice.finish_reason}, choice: {choice}' + ) + + # Convert and collect all assistant messages + assistant_message = self.convert_result_message_to_input_message( + choice.message + ) + assistant_messages.append(assistant_message) + + # Check for tool calls in this choice + if choice.message.tool_calls: + tool_calls_found.extend(choice.message.tool_calls) + print( + f'🔧 Found {len(choice.message.tool_calls)} tool call(s) in output {output_idx}, choice {choice_idx}' + ) + + # Use the first assistant message for conversation history (most providers return one) + if assistant_messages: + for assistant_message in assistant_messages: + conversation_history.append(assistant_message) + print( + f'✅ Added assistant message to history (from {len(assistant_messages)} total messages)' + ) + + if tool_calls_found: + # Use the first tool call for demonstration + tool_call = tool_calls_found[0] + print( + f'🔧 Processing tool call: {tool_call.function.name} (found {len(tool_calls_found)} total tool calls)' + ) + + # Execute the tool + args = json.loads(tool_call.function.arguments) + weather_result = self.execute_weather_tool( + args.get('location', 'San Francisco'), args.get('unit', 'fahrenheit') + ) + print(f'🌤️ Tool result: {weather_result}') + + # Add tool result to conversation history + tool_result_message = self.create_tool_message( + tool_id=tool_call.id, name=tool_call.function.name, content=weather_result + ) + conversation_history.append(tool_result_message) + + # Turn 2: LLM processes tool result (accumulate context + tools) + print('\n--- Turn 2: LLM processes tool result ---') + print(f'📝 Request 2 context: {len(conversation_history)} messages + tools') + input_alpha2_turn2 = ConversationInputAlpha2(messages=conversation_history) + + response2 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn2], + tools=[weather_tool], # Tools carried forward to turn 2 + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + if response2.outputs and response2.outputs[0].choices: + choice2 = response2.outputs[0].choices[0] + print(f'🤖 LLM response with tool context: {choice2.message.content}') + + # Add LLM's response to accumulated history using utility + assistant_message2 = self.convert_result_message_to_input_message( + choice2.message + ) + conversation_history.append(assistant_message2) + + # Turn 3: Follow-up question (full context + tools) + print('\n--- Turn 3: Follow-up question using accumulated context ---') + user_message2 = self.create_user_message( + 'Should I bring an umbrella? Also, what about the weather in New York?' + ) + conversation_history.append(user_message2) + + print(f'📝 Request 3 context: {len(conversation_history)} messages + tools') + print('📋 Accumulated context includes:') + print(' • Original user query about San Francisco') + print(" • Assistant's tool call intention") + print(' • Weather tool execution result') + print(" • Assistant's weather summary") + print(' • New user follow-up question') + + input_alpha2_turn3 = ConversationInputAlpha2(messages=conversation_history) + + response3 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn3], + tools=[weather_tool], # Tools still available in turn 3 + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + if response3.outputs and response3.outputs[0].choices: + choice3 = response3.outputs[0].choices[0] + + if choice3.finish_reason == 'tool_calls' and choice3.message.tool_calls: + print( + f'🔧 Follow-up tool call: {choice3.message.tool_calls[0].function.name}' + ) + + # Execute second tool call + tool_call3 = choice3.message.tool_calls[0] + args3 = json.loads(tool_call3.function.arguments) + weather_result3 = self.execute_weather_tool( + args3.get('location', 'New York'), + args3.get('unit', 'fahrenheit'), + ) + print(f'🌤️ Second tool result: {weather_result3}') + + # Could continue accumulating context for turn 4... + print( + '✅ Multi-turn tool calling with proper context accumulation successful!' + ) + print( + f'📊 Final context: {len(conversation_history)} messages + tools available for next turn' + ) + else: + print( + f'💬 Follow-up response using accumulated context: {choice3.message.content}' + ) + print( + '✅ Multi-turn conversation with proper context accumulation successful!' + ) + print(f'📊 Final context: {len(conversation_history)} messages') + else: + print( + '⚠️ No tool calls found in any output/choice - continuing with regular conversation flow' + ) + # Could continue with regular multi-turn conversation without tools + + if not assistant_messages: + print('❌ No assistant messages received in first turn') + + except Exception as e: + print(f'❌ Multi-turn tool calling error: {e}') + + def test_function_to_schema_approach(self, provider_id: str) -> None: + """Test the ultimate DevEx: function-to-JSON-schema automatic tool creation.""" + print( + f"\n🎯 Testing function-to-schema approach with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + # Create a tool using the typed function approach + restaurant_tool = self.create_tool_from_typed_function_example() + + user_message = self.create_user_message( + 'I want to find Italian restaurants in San Francisco with a moderate price range.' + ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + tools=[restaurant_tool], + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + print(f'📊 Finish reason: {choice.finish_reason}') + + if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: + print(f'🎯 Function-to-schema tool calling successful!') + for tool_call in choice.message.tool_calls: + print(f' Tool: {tool_call.function.name}') + print(f' Arguments: {tool_call.function.arguments}') + + # This demonstrates the complete workflow + print('✅ Auto-generated schema worked perfectly with real LLM!') + else: + print(f'💬 Response: {choice.message.content}') + else: + print('❌ No function-to-schema response received') + + except Exception as e: + print(f'❌ Function-to-schema approach error: {e}') + + async def test_async_conversation_alpha2(self, provider_id: str) -> None: + """Test async Alpha2 conversation with a provider.""" + print( + f"\n⚡ Testing async Alpha2 conversation with {self.available_providers[provider_id]['display_name']}" + ) + + try: + async with AsyncDaprClient() as client: + user_message = self.create_user_message( + 'Tell me a very short joke about async programming.' + ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = await client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + print(f'✅ Async Alpha2 response: {choice.message.content}') + else: + print('❌ No async Alpha2 response received') + + except Exception as e: + print(f'❌ Async Alpha2 error: {e}') + + async def test_async_tool_calling_alpha2(self, provider_id: str) -> None: + """Test async Alpha2 tool calling with a provider.""" + print( + f"\n🔧⚡ Testing async Alpha2 tool calling with {self.available_providers[provider_id]['display_name']}" + ) + + try: + async with AsyncDaprClient() as client: + weather_tool = self.create_weather_tool() + user_message = self.create_user_message("What's the weather in Tokyo?") + + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + + response = await client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + tools=[weather_tool], + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: + print('✅ Async tool calling successful!') + for tool_call in choice.message.tool_calls: + print(f' Tool: {tool_call.function.name}') + args = json.loads(tool_call.function.arguments) + weather_result = self.execute_weather_tool( + args.get('location', 'Tokyo'), args.get('unit', 'fahrenheit') + ) + print(f' Result: {weather_result}') + else: + print(f'💬 Async response: {choice.message.content}') + else: + print('❌ No async tool calling response received') + + except Exception as e: + print(f'❌ Async tool calling error: {e}') + + def run_comprehensive_test(self, provider_id: str) -> None: + """Run comprehensive Alpha2 tests for a provider.""" + provider_name = self.available_providers[provider_id]['display_name'] + print(f"\n{'='*60}") + print(f'🧪 Testing {provider_name} with Alpha2 API') + print(f"{'='*60}") + + # Alpha2 Sync tests + self.test_basic_conversation_alpha2(provider_id) + self.test_multi_turn_conversation_alpha2(provider_id) + self.test_tool_calling_alpha2(provider_id) + self.test_parameter_conversion(provider_id) + self.test_function_to_schema_approach(provider_id) + self.test_multi_turn_tool_calling_alpha2(provider_id) + + # Alpha2 Async tests + asyncio.run(self.test_async_conversation_alpha2(provider_id)) + asyncio.run(self.test_async_tool_calling_alpha2(provider_id)) + + # Legacy Alpha1 test for comparison + self.test_basic_conversation_alpha1_legacy(provider_id) + + def test_basic_conversation_alpha1_legacy(self, provider_id: str) -> None: + """Test legacy Alpha1 conversation for comparison.""" + print( + f"\n📚 Testing legacy Alpha1 for comparison with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + inputs = [ + ConversationInput( + content="Hello! Please respond with: 'Hello from Dapr Alpha1!'", role='user' + ) + ] + + response = client.converse_alpha1( + name=provider_id, + inputs=inputs, + parameters={ + 'temperature': 0.7, + 'max_tokens': 100, + }, + ) + + if response.outputs: + result = response.outputs[0].result + print(f'✅ Alpha1 Response: {result}') + else: + print('❌ No Alpha1 response received') + + except Exception as e: + print(f'❌ Alpha1 legacy conversation error: {e}') + + def cleanup(self) -> None: + """Clean up temporary component files and stop sidecar if needed.""" + # Stop sidecar if we started it + if self.sidecar_manager: + self.sidecar_manager.stop() + + # Clean up temporary components directory + if self.components_dir and Path(self.components_dir).exists(): + import shutil + + shutil.rmtree(self.components_dir) + print(f'🧹 Cleaned up components directory: {self.components_dir}') + + +def main(): + """Main function to run the real LLM providers test with Alpha2 API.""" + print('🚀 Real LLM Providers Example for Dapr Conversation API Alpha2') + print('=' * 60) + + # Check for help flag + if '--help' in sys.argv or '-h' in sys.argv: + print(__doc__) + return + + # Check if user wants to use local dev environment + use_local_dev = '--local-dev' in sys.argv or os.getenv('USE_LOCAL_DEV', '').lower() in ( + 'true', + '1', + 'yes', + ) + build_local_dapr = '--build-local-dapr' in sys.argv or os.getenv( + 'BUILD_LOCAL_DAPR', '' + ).lower() in ('true', '1', 'yes') + + if use_local_dev: + print('🔧 Using local development build (Alpha2 tool calling enabled)') + print(' This will automatically start and manage the Dapr sidecar') + else: + print('📋 Using manual Dapr sidecar setup') + print(" You'll need to start the Dapr sidecar manually") + + tester = RealLLMProviderTester(use_local_dev=use_local_dev) + + try: + # Load environment variables + tester.load_environment() + + # Detect available providers + print('\n🔍 Detecting available LLM providers...') + tester.available_providers = tester.detect_available_providers() + + if not tester.available_providers: + print('\n❌ No LLM providers configured!') + print('Please set up API keys in .env file (copy from .env.example)') + print('Available providers: OpenAI, Anthropic, Mistral, DeepSeek, Google AI') + return + + print(f'\n✅ Found {len(tester.available_providers)} configured provider(s)') + + # Create component configurations for all available providers + selected_providers = list(tester.available_providers.keys()) + components_dir = tester.create_component_configs(selected_providers) + + # Manual sidecar setup + print('\n⚠️ IMPORTANT: Make sure Dapr sidecar is running with components from:') + print(f' {components_dir}') + print('\nTo start the sidecar with these components:') + print( + f' dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path {components_dir}' + ) + + # Wait for user to confirm + input('\nPress Enter when Dapr sidecar is running with the component configurations...') + + # Test only the providers we created components for + for provider_id in selected_providers: + if provider_id in tester.available_providers: + tester.run_comprehensive_test(provider_id) + + print(f"\n{'='*60}") + print('🎉 All Alpha2 tests completed!') + print('✅ Real LLM provider integration with Alpha2 API is working correctly') + print('🔧 Features demonstrated:') + print(' • Alpha2 conversation API with sophisticated message types') + print(' • Automatic parameter conversion (raw Python values)') + print(' • Enhanced tool calling capabilities') + print(' • Multi-turn conversations') + print(' • Multi-turn tool calling with context expansion') + print(' • Function-to-schema automatic tool generation') + print(' • Both sync and async implementations') + print(' • Backward compatibility with Alpha1') + print(f"{'='*60}") + + except KeyboardInterrupt: + print('\n\n⏹️ Tests interrupted by user') + except Exception as e: + print(f'\n❌ Unexpected error: {e}') + import traceback + + traceback.print_exc() + finally: + tester.cleanup() + + +if __name__ == '__main__': + main() diff --git a/ext/dapr-ext-grpc/dapr/ext/grpc/_servicer.py b/ext/dapr-ext-grpc/dapr/ext/grpc/_servicer.py index c51df48b..996267fd 100644 --- a/ext/dapr-ext-grpc/dapr/ext/grpc/_servicer.py +++ b/ext/dapr-ext-grpc/dapr/ext/grpc/_servicer.py @@ -28,7 +28,7 @@ JobEventRequest, ) from dapr.proto.common.v1.common_pb2 import InvokeRequest -from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE +from dapr.clients._constants import DEFAULT_JSON_CONTENT_TYPE from dapr.clients.grpc._request import InvokeMethodRequest, BindingRequest, JobEvent from dapr.clients.grpc._response import InvokeMethodResponse, TopicEventResponse diff --git a/tests/clients/fake_dapr_server.py b/tests/clients/fake_dapr_server.py index 3ca7391c..5ff3b969 100644 --- a/tests/clients/fake_dapr_server.py +++ b/tests/clients/fake_dapr_server.py @@ -554,37 +554,45 @@ def ConverseAlpha2(self, request, context): # Process each message in the input for msg_idx, message in enumerate(input.messages): - response_content = "" + response_content = '' tool_calls = [] # Extract content based on message type if message.HasField('of_user'): if message.of_user.content: - response_content = f"Response to user: {message.of_user.content[0].text}" + response_content = f'Response to user: {message.of_user.content[0].text}' elif message.HasField('of_system'): if message.of_system.content: - response_content = f"System acknowledged: {message.of_system.content[0].text}" + response_content = ( + f'System acknowledged: {message.of_system.content[0].text}' + ) elif message.HasField('of_assistant'): if message.of_assistant.content: - response_content = f"Assistant continued: {message.of_assistant.content[0].text}" + response_content = ( + f'Assistant continued: {message.of_assistant.content[0].text}' + ) elif message.HasField('of_developer'): if message.of_developer.content: - response_content = f"Developer note processed: {message.of_developer.content[0].text}" + response_content = ( + f'Developer note processed: {message.of_developer.content[0].text}' + ) elif message.HasField('of_tool'): if message.of_tool.content: - response_content = f"Tool result processed: {message.of_tool.content[0].text}" + response_content = ( + f'Tool result processed: {message.of_tool.content[0].text}' + ) # Check if tools are available and simulate tool calling - if request.tools and response_content and "weather" in response_content.lower(): + if request.tools and response_content and 'weather' in response_content.lower(): # Simulate a tool call for weather requests for tool in request.tools: - if tool.function and "weather" in tool.function.name.lower(): + if tool.function and 'weather' in tool.function.name.lower(): tool_call = ConversationToolCalls( - id=f"call_{input_idx}_{msg_idx}", + id=f'call_{input_idx}_{msg_idx}', function=ConversationToolCallsOfFunction( name=tool.function.name, - arguments='{"location": "San Francisco", "unit": "celsius"}' - ) + arguments='{"location": "San Francisco", "unit": "celsius"}', + ), ) tool_calls.append(tool_call) response_content = "I'll check the weather for you." @@ -592,16 +600,13 @@ def ConverseAlpha2(self, request, context): # Create result message result_message = ConversationResultMessage( - content=response_content, - tool_calls=tool_calls + content=response_content, tool_calls=tool_calls ) # Create choice - finish_reason = "tool_calls" if tool_calls else "stop" + finish_reason = 'tool_calls' if tool_calls else 'stop' choice = ConversationResultChoices( - finish_reason=finish_reason, - index=msg_idx, - message=result_message + finish_reason=finish_reason, index=msg_idx, message=result_message ) choices.append(choice) @@ -611,7 +616,7 @@ def ConverseAlpha2(self, request, context): return ConversationResponseAlpha2( context_id=request.context_id if request.HasField('context_id') else None, - outputs=outputs + outputs=outputs, ) def ScheduleJobAlpha1(self, request, context): diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index 49029af7..eb52a1f8 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -14,15 +14,14 @@ """ import asyncio -import json import unittest -from google.protobuf.any_pb2 import Any as GrpcAny from google.rpc import code_pb2, status_pb2 from dapr.aio.clients import DaprClient as AsyncDaprClient from dapr.clients import DaprClient from dapr.clients.exceptions import DaprGrpcError +from dapr.conf import settings from dapr.clients.grpc._request import ( ConversationInput, ConversationInputAlpha2, @@ -31,12 +30,9 @@ ConversationMessageOfUser, ConversationMessageOfSystem, ConversationMessageOfAssistant, - ConversationMessageOfDeveloper, ConversationMessageOfTool, ConversationTools, ConversationToolsFunction, - ConversationToolCalls, - ConversationToolCallsOfFunction, ) from tests.clients.fake_dapr_server import FakeDaprSidecar @@ -52,6 +48,9 @@ class ConversationTestBase: def setUpClass(cls): cls._fake_dapr_server = FakeDaprSidecar(grpc_port=cls.grpc_port, http_port=cls.http_port) cls._fake_dapr_server.start() + # Configure health check to use fake server's HTTP port + settings.DAPR_HTTP_PORT = cls.http_port + settings.DAPR_HTTP_ENDPOINT = f'http://127.0.0.1:{cls.http_port}' @classmethod def tearDownClass(cls): @@ -64,15 +63,19 @@ def create_weather_tool(self): name='get_weather', description='Get weather information for a location', parameters={ - 'location': GrpcAny(value=json.dumps({ - 'type': 'string', - 'description': 'The city and state, e.g. San Francisco, CA' - }).encode()), - 'unit': GrpcAny(value=json.dumps({ - 'type': 'string', - 'enum': ['celsius', 'fahrenheit'], - 'description': 'Temperature unit' - }).encode()) + 'type': 'object', + 'properties': { + 'location': { + 'type': 'string', + 'description': 'The city and state, e.g. San Francisco, CA', + }, + 'unit': { + 'type': 'string', + 'enum': ['celsius', 'fahrenheit'], + 'description': 'Temperature unit', + }, + }, + 'required': ['location'], } ) ) @@ -84,10 +87,14 @@ def create_calculate_tool(self): name='calculate', description='Perform mathematical calculations', parameters={ - 'expression': GrpcAny(value=json.dumps({ - 'type': 'string', - 'description': 'Mathematical expression to evaluate' - }).encode()) + 'type': 'object', + 'properties': { + 'expression': { + 'type': 'string', + 'description': 'Mathematical expression to evaluate', + } + }, + 'required': ['expression'], } ) ) @@ -95,25 +102,20 @@ def create_calculate_tool(self): def create_user_message(self, text): """Helper to create a user message for Alpha2.""" return ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text=text)] - ) + of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) ) def create_system_message(self, text): """Helper to create a system message for Alpha2.""" return ConversationMessage( - of_system=ConversationMessageOfSystem( - content=[ConversationMessageContent(text=text)] - ) + of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) ) def create_assistant_message(self, text, tool_calls=None): """Helper to create an assistant message for Alpha2.""" return ConversationMessage( of_assistant=ConversationMessageOfAssistant( - content=[ConversationMessageContent(text=text)], - tool_calls=tool_calls or [] + content=[ConversationMessageContent(text=text)], tool_calls=tool_calls or [] ) ) @@ -121,9 +123,7 @@ def create_tool_message(self, tool_id, name, content): """Helper to create a tool message for Alpha2.""" return ConversationMessage( of_tool=ConversationMessageOfTool( - tool_id=tool_id, - name=name, - content=[ConversationMessageContent(text=content)] + tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] ) ) @@ -157,7 +157,7 @@ def test_conversation_alpha1_with_options(self): context_id='test-context-123', temperature=0.7, scrub_pii=True, - metadata={'test_key': 'test_value'} + metadata={'test_key': 'test_value'}, ) self.assertIsNotNone(response) @@ -179,7 +179,7 @@ def test_alpha1_parameter_conversion(self): 'top_p': 0.9, 'frequency_penalty': 0.0, 'presence_penalty': 0.0, - } + }, ) self.assertIsNotNone(response) @@ -226,8 +226,7 @@ def test_conversation_alpha2_with_system_message(self): user_message = self.create_user_message('Hello!') input_alpha2 = ConversationInputAlpha2( - messages=[system_message, user_message], - scrub_pii=False + messages=[system_message, user_message], scrub_pii=False ) response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -247,10 +246,7 @@ def test_conversation_alpha2_with_options(self): """Test Alpha2 conversation with various options.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: user_message = self.create_user_message('Alpha2 with options') - input_alpha2 = ConversationInputAlpha2( - messages=[user_message], - scrub_pii=True - ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=True) response = client.converse_alpha2( name='test-llm', @@ -259,7 +255,7 @@ def test_conversation_alpha2_with_options(self): temperature=0.8, scrub_pii=True, metadata={'alpha2_test': 'true'}, - tool_choice='none' + tool_choice='none', ) self.assertIsNotNone(response) @@ -282,7 +278,7 @@ def test_alpha2_parameter_conversion(self): 'frequency_penalty': 0.0, 'presence_penalty': 0.0, 'stream': False, - } + }, ) self.assertIsNotNone(response) @@ -314,10 +310,7 @@ def test_tool_calling_weather(self): input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool], - tool_choice='auto' + name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto' ) self.assertIsNotNone(response) @@ -338,9 +331,7 @@ def test_tool_calling_calculate(self): input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[calc_tool] + name='test-llm', inputs=[input_alpha2], tools=[calc_tool] ) # Note: Our fake server only triggers weather tools, so this won't return tool calls @@ -362,7 +353,7 @@ def test_multiple_tools(self): name='test-llm', inputs=[input_alpha2], tools=[weather_tool, calc_tool], - tool_choice='auto' + tool_choice='auto', ) self.assertIsNotNone(response) @@ -379,10 +370,7 @@ def test_tool_choice_none(self): input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool], - tool_choice='none' + name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='none' ) self.assertIsNotNone(response) @@ -404,7 +392,7 @@ def test_tool_choice_specific(self): name='test-llm', inputs=[input_alpha2], tools=[weather_tool, calc_tool], - tool_choice='get_weather' + tool_choice='get_weather', ) self.assertIsNotNone(response) @@ -452,9 +440,7 @@ def test_tool_calling_workflow(self): input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response1 = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool] + name='test-llm', inputs=[input_alpha2], tools=[weather_tool] ) # Should get tool call @@ -467,7 +453,7 @@ def test_tool_calling_workflow(self): tool_result_message = self.create_tool_message( tool_id=tool_call.id, name='get_weather', - content='{"temperature": 18, "condition": "cloudy", "humidity": 75}' + content='{"temperature": 18, "condition": "cloudy", "humidity": 75}', ) result_input = ConversationInputAlpha2(messages=[tool_result_message]) @@ -489,9 +475,7 @@ def test_conversation_context_continuity(self): input1 = ConversationInputAlpha2(messages=[user_message1]) response1 = client.converse_alpha2( - name='test-llm', - inputs=[input1], - context_id=context_id + name='test-llm', inputs=[input1], context_id=context_id ) self.assertEqual(response1.context_id, context_id) @@ -501,9 +485,7 @@ def test_conversation_context_continuity(self): input2 = ConversationInputAlpha2(messages=[user_message2]) response2 = client.converse_alpha2( - name='test-llm', - inputs=[input2], - context_id=context_id + name='test-llm', inputs=[input2], context_id=context_id ) self.assertEqual(response2.context_id, context_id) @@ -548,9 +530,7 @@ async def test_async_tool_calling(self): input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = await client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool] + name='test-llm', inputs=[input_alpha2], tools=[weather_tool] ) self.assertIsNotNone(response) @@ -562,12 +542,11 @@ async def test_async_tool_calling(self): async def test_concurrent_async_conversations(self): """Test multiple concurrent async conversations.""" async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: + async def run_alpha1_conversation(message, session_id): inputs = [ConversationInput(content=message, role='user')] response = await client.converse_alpha1( - name='test-llm', - inputs=inputs, - context_id=session_id + name='test-llm', inputs=inputs, context_id=session_id ) return response.outputs[0].result @@ -575,9 +554,7 @@ async def run_alpha2_conversation(message, session_id): user_message = self.create_user_message(message) input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = await client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - context_id=session_id + name='test-llm', inputs=[input_alpha2], context_id=session_id ) return response.outputs[0].choices[0].message.content @@ -608,7 +585,7 @@ async def test_async_multi_turn_with_tools(self): name='test-llm', inputs=[input1], tools=[weather_tool], - context_id='async-multi-turn' + context_id='async-multi-turn', ) # Should get tool call @@ -619,14 +596,12 @@ async def test_async_multi_turn_with_tools(self): tool_result_message = self.create_tool_message( tool_id=tool_call.id, name='get_weather', - content='{"temperature": 22, "condition": "sunny"}' + content='{"temperature": 22, "condition": "sunny"}', ) input2 = ConversationInputAlpha2(messages=[tool_result_message]) response2 = await client.converse_alpha2( - name='test-llm', - inputs=[input2], - context_id='async-multi-turn' + name='test-llm', inputs=[input2], context_id='async-multi-turn' ) self.assertIsNotNone(response2) @@ -649,30 +624,6 @@ async def test_async_error_handling(self): class ConversationParameterTests(ConversationTestBase, unittest.TestCase): """Tests for parameter handling and conversion.""" - def test_parameter_backward_compatibility(self): - """Test that pre-wrapped protobuf parameters still work.""" - from google.protobuf.wrappers_pb2 import StringValue - - # Create pre-wrapped parameter (old way) - pre_wrapped_any = GrpcAny() - pre_wrapped_any.Pack(StringValue(value="auto")) - - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ConversationInput(content='Backward compatibility test', role='user')] - - # Mix of old (pre-wrapped) and new (raw) parameters - response = client.converse_alpha1( - name='test-llm', - inputs=inputs, - parameters={ - 'tool_choice': pre_wrapped_any, # Old way (pre-wrapped) - 'temperature': 0.8, # New way (raw value) - 'max_tokens': 500, # New way (raw value) - } - ) - - self.assertIsNotNone(response) - def test_parameter_edge_cases(self): """Test parameter conversion with edge cases.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: @@ -683,14 +634,14 @@ def test_parameter_edge_cases(self): name='test-llm', inputs=[input_alpha2], parameters={ - 'int32_max': 2147483647, # Int32 maximum + 'int32_max': 2147483647, # Int32 maximum 'int64_large': 9999999999, # Requires Int64 - 'negative_temp': -0.5, # Negative float - 'zero_value': 0, # Zero integer - 'false_flag': False, # Boolean false - 'true_flag': True, # Boolean true - 'empty_string': '', # Empty string - } + 'negative_temp': -0.5, # Negative float + 'zero_value': 0, # Zero integer + 'false_flag': False, # Boolean false + 'true_flag': True, # Boolean true + 'empty_string': '', # Empty string + }, ) self.assertIsNotNone(response) @@ -714,7 +665,7 @@ def test_realistic_provider_parameters(self): 'presence_penalty': 0.0, 'stream': False, 'tool_choice': 'auto', - } + }, ) # Anthropic-style parameters @@ -728,7 +679,7 @@ def test_realistic_provider_parameters(self): 'top_p': 0.9, 'top_k': 250, 'stream': False, - } + }, ) self.assertIsNotNone(response1) @@ -780,4 +731,4 @@ def test_mixed_alpha1_alpha2_compatibility(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 6750bb6a..ef328f1f 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -47,8 +47,6 @@ ConversationMessageOfTool, ConversationTools, ConversationToolsFunction, - ConversationToolCalls, - ConversationToolCallsOfFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem @@ -1254,16 +1252,12 @@ def test_converse_alpha2_basic_user_message(self): # Create user message user_message = ConversationMessage( of_user=ConversationMessageOfUser( - name="TestUser", - content=[ConversationMessageContent(text="Hello, how are you?")] + name='TestUser', content=[ConversationMessageContent(text='Hello, how are you?')] ) ) # Create Alpha2 input - input_alpha2 = ConversationInputAlpha2( - messages=[user_message], - scrub_pii=False - ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=False) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1287,9 +1281,15 @@ def test_converse_alpha2_with_tools_weather_request(self): # Create weather tool weather_tool = ConversationTools( function=ConversationToolsFunction( - name="get_weather", - description="Get current weather information", - parameters={"location": GrpcAny(value=b'{"type": "string"}')} + name='get_weather', + description='Get current weather information', + parameters={ + 'type': 'object', + 'properties': { + 'location': {'type': 'string', 'description': 'Location for weather info'} + }, + 'required': ['location'] + }, ) ) @@ -1303,10 +1303,7 @@ def test_converse_alpha2_with_tools_weather_request(self): input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = dapr.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool], - tool_choice='auto' + name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto' ) # Check response structure with tool call @@ -1322,7 +1319,9 @@ def test_converse_alpha2_with_tools_weather_request(self): tool_call = choice.message.tool_calls[0] self.assertEqual(tool_call.function.name, 'get_weather') - self.assertEqual(tool_call.function.arguments, '{"location": "San Francisco", "unit": "celsius"}') + self.assertEqual( + tool_call.function.arguments, '{"location": "San Francisco", "unit": "celsius"}' + ) self.assertTrue(tool_call.id.startswith('call_')) def test_converse_alpha2_system_message(self): @@ -1332,7 +1331,7 @@ def test_converse_alpha2_system_message(self): # Create system message system_message = ConversationMessage( of_system=ConversationMessageOfSystem( - content=[ConversationMessageContent(text="You are a helpful assistant.")] + content=[ConversationMessageContent(text='You are a helpful assistant.')] ) ) @@ -1343,7 +1342,9 @@ def test_converse_alpha2_system_message(self): # Check response self.assertIsNotNone(response) choice = response.outputs[0].choices[0] - self.assertEqual(choice.message.content, 'System acknowledged: You are a helpful assistant.') + self.assertEqual( + choice.message.content, 'System acknowledged: You are a helpful assistant.' + ) def test_converse_alpha2_developer_message(self): """Test Alpha2 conversation with developer messages.""" @@ -1352,8 +1353,8 @@ def test_converse_alpha2_developer_message(self): # Create developer message developer_message = ConversationMessage( of_developer=ConversationMessageOfDeveloper( - name="DevTeam", - content=[ConversationMessageContent(text="Debug: Processing user input")] + name='DevTeam', + content=[ConversationMessageContent(text='Debug: Processing user input')], ) ) @@ -1364,7 +1365,9 @@ def test_converse_alpha2_developer_message(self): # Check response self.assertIsNotNone(response) choice = response.outputs[0].choices[0] - self.assertEqual(choice.message.content, 'Developer note processed: Debug: Processing user input') + self.assertEqual( + choice.message.content, 'Developer note processed: Debug: Processing user input' + ) def test_converse_alpha2_tool_message(self): """Test Alpha2 conversation with tool messages.""" @@ -1373,9 +1376,11 @@ def test_converse_alpha2_tool_message(self): # Create tool message tool_message = ConversationMessage( of_tool=ConversationMessageOfTool( - tool_id="call_123", - name="get_weather", - content=[ConversationMessageContent(text='{"temperature": 22, "condition": "sunny"}')] + tool_id='call_123', + name='get_weather', + content=[ + ConversationMessageContent(text='{"temperature": 22, "condition": "sunny"}') + ], ) ) @@ -1386,7 +1391,10 @@ def test_converse_alpha2_tool_message(self): # Check response self.assertIsNotNone(response) choice = response.outputs[0].choices[0] - self.assertEqual(choice.message.content, 'Tool result processed: {"temperature": 22, "condition": "sunny"}') + self.assertEqual( + choice.message.content, + 'Tool result processed: {"temperature": 22, "condition": "sunny"}', + ) def test_converse_alpha2_assistant_message(self): """Test Alpha2 conversation with assistant messages.""" @@ -1395,7 +1403,7 @@ def test_converse_alpha2_assistant_message(self): # Create assistant message assistant_message = ConversationMessage( of_assistant=ConversationMessageOfAssistant( - content=[ConversationMessageContent(text="I understand your request.")] + content=[ConversationMessageContent(text='I understand your request.')] ) ) @@ -1415,14 +1423,12 @@ def test_converse_alpha2_multiple_messages(self): # Create multiple messages system_message = ConversationMessage( of_system=ConversationMessageOfSystem( - content=[ConversationMessageContent(text="You are helpful.")] + content=[ConversationMessageContent(text='You are helpful.')] ) ) user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="Hello!")] - ) + of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text='Hello!')]) ) input_alpha2 = ConversationInputAlpha2(messages=[system_message, user_message]) @@ -1435,7 +1441,9 @@ def test_converse_alpha2_multiple_messages(self): self.assertEqual(len(response.outputs[0].choices), 2) # Check individual responses - self.assertEqual(response.outputs[0].choices[0].message.content, 'System acknowledged: You are helpful.') + self.assertEqual( + response.outputs[0].choices[0].message.content, 'System acknowledged: You are helpful.' + ) self.assertEqual(response.outputs[0].choices[1].message.content, 'Response to user: Hello!') def test_converse_alpha2_with_context_and_options(self): @@ -1446,17 +1454,14 @@ def test_converse_alpha2_with_context_and_options(self): user_message = ConversationMessage( of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="Continue our conversation")] + content=[ConversationMessageContent(text='Continue our conversation')] ) ) - input_alpha2 = ConversationInputAlpha2( - messages=[user_message], - scrub_pii=True - ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=True) # Create custom parameters - params = {"custom_param": GrpcAny(value=b'{"setting": "value"}')} + params = {'custom_param': GrpcAny(value=b'{"setting": "value"}')} response = dapr.converse_alpha2( name='test-llm', @@ -1466,7 +1471,7 @@ def test_converse_alpha2_with_context_and_options(self): metadata={'env': 'test'}, scrub_pii=True, temperature=0.7, - tool_choice='none' + tool_choice='none', ) # Check response @@ -1486,7 +1491,7 @@ def test_converse_alpha2_error_handling(self): user_message = ConversationMessage( of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="Test error")] + content=[ConversationMessageContent(text='Test error')] ) ) @@ -1498,23 +1503,18 @@ def test_converse_alpha2_error_handling(self): def test_converse_alpha2_tool_choice_specific(self): """Test Alpha2 conversation with specific tool choice.""" - from google.protobuf.any_pb2 import Any as GrpcAny dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create multiple tools weather_tool = ConversationTools( function=ConversationToolsFunction( - name="get_weather", - description="Get weather information" + name='get_weather', description='Get weather information' ) ) calculator_tool = ConversationTools( - function=ConversationToolsFunction( - name="calculate", - description="Perform calculations" - ) + function=ConversationToolsFunction(name='calculate', description='Perform calculations') ) user_message = ConversationMessage( @@ -1529,7 +1529,7 @@ def test_converse_alpha2_tool_choice_specific(self): name='test-llm', inputs=[input_alpha2], tools=[weather_tool, calculator_tool], - tool_choice='get_weather' # Force specific tool + tool_choice='get_weather', # Force specific tool ) # Even though we specified a specific tool, our mock will still trigger diff --git a/tests/clients/test_grpc_helpers.py b/tests/clients/test_grpc_helpers.py new file mode 100644 index 00000000..32ba3c07 --- /dev/null +++ b/tests/clients/test_grpc_helpers.py @@ -0,0 +1,861 @@ +#!/usr/bin/env python3 + +""" +Tests for dapr.clients.grpc._helpers module. + +This module tests the function-to-JSON-schema helpers that provide +automatic tool creation from typed Python functions. +""" + +import unittest +import sys +import os +from typing import Optional, List, Dict, Union +from enum import Enum +from dataclasses import dataclass + +# Add the project root to sys.path to import helpers directly +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +# Import the helper functions from the new module to avoid circular imports +try: + from dapr.clients.grpc._schema_helpers import ( + python_type_to_json_schema, + extract_docstring_info, + function_to_json_schema, + create_tool_from_function, + ) + + HELPERS_AVAILABLE = True +except ImportError as e: + HELPERS_AVAILABLE = False + print(f'Warning: Could not import schema helpers: {e}') + + +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestPythonTypeToJsonSchema(unittest.TestCase): + """Test the python_type_to_json_schema function.""" + + def test_basic_types(self): + """Test conversion of basic Python types.""" + test_cases = [ + (str, {'type': 'string'}), + (int, {'type': 'integer'}), + (float, {'type': 'number'}), + (bool, {'type': 'boolean'}), + (bytes, {'type': 'string', 'format': 'byte'}), + ] + + for python_type, expected in test_cases: + with self.subTest(python_type=python_type): + result = python_type_to_json_schema(python_type) + self.assertEqual(result['type'], expected['type']) + if 'format' in expected: + self.assertEqual(result['format'], expected['format']) + + def test_optional_types(self): + """Test Optional[T] types (Union[T, None]).""" + # Optional[str] should resolve to string + result = python_type_to_json_schema(Optional[str]) + self.assertEqual(result['type'], 'string') + + # Optional[int] should resolve to integer + result = python_type_to_json_schema(Optional[int]) + self.assertEqual(result['type'], 'integer') + + def test_list_types(self): + """Test List[T] types.""" + # List[str] + result = python_type_to_json_schema(List[str]) + expected = {'type': 'array', 'items': {'type': 'string'}} + self.assertEqual(result, expected) + + # List[int] + result = python_type_to_json_schema(List[int]) + expected = {'type': 'array', 'items': {'type': 'integer'}} + self.assertEqual(result, expected) + + def test_dict_types(self): + """Test Dict[str, T] types.""" + result = python_type_to_json_schema(Dict[str, int]) + expected = {'type': 'object', 'additionalProperties': {'type': 'integer'}} + self.assertEqual(result, expected) + + def test_enum_types(self): + """Test Enum types.""" + + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + result = python_type_to_json_schema(Color) + expected = {'type': 'string', 'enum': ['red', 'green', 'blue']} + self.assertEqual(result['type'], expected['type']) + self.assertEqual(set(result['enum']), set(expected['enum'])) + + def test_union_types(self): + """Test Union types.""" + result = python_type_to_json_schema(Union[str, int]) + self.assertIn('anyOf', result) + self.assertEqual(len(result['anyOf']), 2) + + # Should contain both string and integer schemas + types = [schema['type'] for schema in result['anyOf']] + self.assertIn('string', types) + self.assertIn('integer', types) + + def test_dataclass_types(self): + """Test dataclass types.""" + + @dataclass + class Person: + name: str + age: int = 25 + + result = python_type_to_json_schema(Person) + + self.assertEqual(result['type'], 'object') + self.assertIn('properties', result) + self.assertIn('required', result) + + # Check properties + self.assertIn('name', result['properties']) + self.assertIn('age', result['properties']) + self.assertEqual(result['properties']['name']['type'], 'string') + self.assertEqual(result['properties']['age']['type'], 'integer') + + # Check required fields (name is required, age has default) + self.assertIn('name', result['required']) + self.assertNotIn('age', result['required']) + + def test_pydantic_models(self): + """Test Pydantic model types.""" + try: + from pydantic import BaseModel + + class SearchParams(BaseModel): + query: str + limit: int = 10 + include_images: bool = False + tags: Optional[List[str]] = None + + result = python_type_to_json_schema(SearchParams) + + # Pydantic models should generate their own schema + self.assertIn('type', result) + # The exact structure depends on Pydantic version, but it should have properties + if 'properties' in result: + self.assertIn('query', result['properties']) + except ImportError: + self.skipTest('Pydantic not available for testing') + + def test_nested_types(self): + """Test complex nested type combinations.""" + # Optional[List[str]] + result = python_type_to_json_schema(Optional[List[str]]) + self.assertEqual(result['type'], 'array') + self.assertEqual(result['items']['type'], 'string') + + # List[Optional[int]] + result = python_type_to_json_schema(List[Optional[int]]) + self.assertEqual(result['type'], 'array') + self.assertEqual(result['items']['type'], 'integer') + + # Dict[str, List[int]] + result = python_type_to_json_schema(Dict[str, List[int]]) + self.assertEqual(result['type'], 'object') + self.assertEqual(result['additionalProperties']['type'], 'array') + self.assertEqual(result['additionalProperties']['items']['type'], 'integer') + + def test_complex_dataclass_with_nested_types(self): + """Test dataclass with complex nested types.""" + + @dataclass + class Address: + street: str + city: str + zipcode: Optional[str] = None + + @dataclass + class Person: + name: str + addresses: List[Address] + metadata: Dict[str, str] + tags: Optional[List[str]] = None + + result = python_type_to_json_schema(Person) + + self.assertEqual(result['type'], 'object') + self.assertIn('name', result['properties']) + self.assertIn('addresses', result['properties']) + self.assertIn('metadata', result['properties']) + self.assertIn('tags', result['properties']) + + # Check nested structures + self.assertEqual(result['properties']['addresses']['type'], 'array') + self.assertEqual(result['properties']['metadata']['type'], 'object') + self.assertEqual(result['properties']['tags']['type'], 'array') + + # Required fields + self.assertIn('name', result['required']) + self.assertIn('addresses', result['required']) + self.assertIn('metadata', result['required']) + self.assertNotIn('tags', result['required']) + + def test_enum_with_different_types(self): + """Test enums with different value types.""" + + class Status(Enum): + ACTIVE = 1 + INACTIVE = 0 + PENDING = 2 + + class Priority(Enum): + LOW = 'low' + MEDIUM = 'medium' + HIGH = 'high' + + # String enum + result = python_type_to_json_schema(Priority) + self.assertEqual(result['type'], 'string') + self.assertEqual(set(result['enum']), {'low', 'medium', 'high'}) + + # Integer enum + result = python_type_to_json_schema(Status) + self.assertEqual(result['type'], 'string') + self.assertEqual(set(result['enum']), {1, 0, 2}) + + def test_none_type(self): + """Test None type handling.""" + result = python_type_to_json_schema(type(None)) + self.assertEqual(result['type'], 'null') + + def test_unknown_type_fallback(self): + """Test fallback for unknown types.""" + + class CustomClass: + pass + + result = python_type_to_json_schema(CustomClass) + self.assertEqual(result['type'], 'string') + self.assertIn('Unknown type', result['description']) + + def test_realistic_function_types(self): + """Test types from realistic function signatures.""" + # Weather function parameters + result = python_type_to_json_schema(str) # location + self.assertEqual(result['type'], 'string') + + # Optional unit with enum + class TemperatureUnit(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + + result = python_type_to_json_schema(Optional[TemperatureUnit]) + self.assertEqual(result['type'], 'string') + self.assertEqual(set(result['enum']), {'celsius', 'fahrenheit'}) + + # Search function with complex params + @dataclass + class SearchOptions: + max_results: int = 10 + include_metadata: bool = True + filters: Optional[Dict[str, str]] = None + + result = python_type_to_json_schema(SearchOptions) + self.assertEqual(result['type'], 'object') + self.assertIn('max_results', result['properties']) + self.assertIn('include_metadata', result['properties']) + self.assertIn('filters', result['properties']) + + def test_list_without_type_args(self): + """Test bare List type without type arguments.""" + result = python_type_to_json_schema(list) + self.assertEqual(result['type'], 'array') + self.assertNotIn('items', result) + + def test_dict_without_type_args(self): + """Test bare Dict type without type arguments.""" + result = python_type_to_json_schema(dict) + self.assertEqual(result['type'], 'object') + self.assertNotIn('additionalProperties', result) + + +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestExtractDocstringInfo(unittest.TestCase): + """Test the extract_docstring_info function.""" + + def test_google_style_docstring(self): + """Test Google-style docstring parsing.""" + + def sample_function(name: str, age: int) -> str: + """A sample function. + + Args: + name: The person's name + age: The person's age in years + """ + return f'{name} is {age}' + + result = extract_docstring_info(sample_function) + expected = {'name': "The person's name", 'age': "The person's age in years"} + self.assertEqual(result, expected) + + def test_no_docstring(self): + """Test function with no docstring.""" + + def no_doc_function(param): + pass + + result = extract_docstring_info(no_doc_function) + self.assertEqual(result, {}) + + def test_docstring_without_args(self): + """Test docstring without Args section.""" + + def simple_function(param): + """Just a simple function.""" + pass + + result = extract_docstring_info(simple_function) + self.assertEqual(result, {}) + + def test_multiline_param_description(self): + """Test parameter descriptions that span multiple lines.""" + + def complex_function(param1: str) -> str: + """A complex function. + + Args: + param1: This is a long description + that spans multiple lines + for testing purposes + """ + return param1 + + result = extract_docstring_info(complex_function) + expected = { + 'param1': 'This is a long description that spans multiple lines for testing purposes' + } + self.assertEqual(result, expected) + + +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestFunctionToJsonSchema(unittest.TestCase): + """Test the function_to_json_schema function.""" + + def test_simple_function(self): + """Test a simple function with basic types.""" + + def get_weather(location: str, unit: str = 'fahrenheit') -> str: + """Get weather for a location. + + Args: + location: The city name + unit: Temperature unit + """ + return f'Weather in {location}' + + result = function_to_json_schema(get_weather) + + # Check structure + self.assertEqual(result['type'], 'object') + self.assertIn('properties', result) + self.assertIn('required', result) + + # Check properties + self.assertIn('location', result['properties']) + self.assertIn('unit', result['properties']) + self.assertEqual(result['properties']['location']['type'], 'string') + self.assertEqual(result['properties']['unit']['type'], 'string') + + # Check descriptions + self.assertEqual(result['properties']['location']['description'], 'The city name') + self.assertEqual(result['properties']['unit']['description'], 'Temperature unit') + + # Check required (location is required, unit has default) + self.assertIn('location', result['required']) + self.assertNotIn('unit', result['required']) + + def test_function_with_complex_types(self): + """Test function with complex type hints.""" + + def search_data( + query: str, + limit: int = 10, + filters: Optional[List[str]] = None, + metadata: Dict[str, str] = None, + ) -> Dict[str, any]: + """Search for data. + + Args: + query: Search query + limit: Maximum results + filters: Optional search filters + metadata: Additional metadata + """ + return {} + + result = function_to_json_schema(search_data) + + # Check all parameters are present + props = result['properties'] + self.assertIn('query', props) + self.assertIn('limit', props) + self.assertIn('filters', props) + self.assertIn('metadata', props) + + # Check types + self.assertEqual(props['query']['type'], 'string') + self.assertEqual(props['limit']['type'], 'integer') + self.assertEqual(props['filters']['type'], 'array') + self.assertEqual(props['filters']['items']['type'], 'string') + self.assertEqual(props['metadata']['type'], 'object') + + # Check required (only query is required) + self.assertEqual(result['required'], ['query']) + + def test_function_with_enum(self): + """Test function with Enum parameter.""" + + class Priority(Enum): + LOW = 'low' + HIGH = 'high' + + def create_task(name: str, priority: Priority = Priority.LOW) -> str: + """Create a task. + + Args: + name: Task name + priority: Task priority level + """ + return f'Task: {name}' + + result = function_to_json_schema(create_task) + + # Check enum handling + priority_prop = result['properties']['priority'] + self.assertEqual(priority_prop['type'], 'string') + self.assertIn('enum', priority_prop) + self.assertEqual(set(priority_prop['enum']), {'low', 'high'}) + + def test_function_no_parameters(self): + """Test function with no parameters.""" + + def get_time() -> str: + """Get current time.""" + return '12:00' + + result = function_to_json_schema(get_time) + + self.assertEqual(result['type'], 'object') + self.assertEqual(result['properties'], {}) + self.assertEqual(result['required'], []) + + def test_function_with_args_kwargs(self): + """Test function with *args and **kwargs (should be ignored).""" + + def flexible_function(name: str, *args, **kwargs) -> str: + """A flexible function.""" + return name + + result = function_to_json_schema(flexible_function) + + # Should only include 'name', not *args or **kwargs + self.assertEqual(list(result['properties'].keys()), ['name']) + self.assertEqual(result['required'], ['name']) + + def test_realistic_weather_function(self): + """Test realistic weather API function.""" + + class Units(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + + def get_weather( + location: str, unit: Units = Units.FAHRENHEIT, include_forecast: bool = False + ) -> str: + """Get current weather for a location. + + Args: + location: The city and state or country + unit: Temperature unit preference + include_forecast: Whether to include 5-day forecast + """ + return f'Weather in {location}' + + result = function_to_json_schema(get_weather) + + # Check structure + self.assertEqual(result['type'], 'object') + props = result['properties'] + + # Check location (required string) + self.assertEqual(props['location']['type'], 'string') + self.assertEqual(props['location']['description'], 'The city and state or country') + self.assertIn('location', result['required']) + + # Check unit (optional enum) + self.assertEqual(props['unit']['type'], 'string') + self.assertEqual(set(props['unit']['enum']), {'celsius', 'fahrenheit'}) + self.assertNotIn('unit', result['required']) + + # Check forecast flag (optional boolean) + self.assertEqual(props['include_forecast']['type'], 'boolean') + self.assertNotIn('include_forecast', result['required']) + + def test_realistic_search_function(self): + """Test realistic search function with complex parameters.""" + + @dataclass + class SearchFilters: + category: Optional[str] = None + price_min: Optional[float] = None + price_max: Optional[float] = None + + def search_products( + query: str, + max_results: int = 20, + sort_by: str = 'relevance', + filters: Optional[SearchFilters] = None, + include_metadata: bool = True, + ) -> List[Dict[str, str]]: + """Search for products in catalog. + + Args: + query: Search query string + max_results: Maximum number of results to return + sort_by: Sort order (relevance, price, rating) + filters: Optional search filters + include_metadata: Whether to include product metadata + """ + return [] + + result = function_to_json_schema(search_products) + + props = result['properties'] + + # Check required query + self.assertEqual(props['query']['type'], 'string') + self.assertIn('query', result['required']) + + # Check optional integer with default + self.assertEqual(props['max_results']['type'], 'integer') + self.assertNotIn('max_results', result['required']) + + # Check string with default + self.assertEqual(props['sort_by']['type'], 'string') + self.assertNotIn('sort_by', result['required']) + + # Check optional dataclass + self.assertEqual(props['filters']['type'], 'object') + self.assertNotIn('filters', result['required']) + + # Check boolean with default + self.assertEqual(props['include_metadata']['type'], 'boolean') + self.assertNotIn('include_metadata', result['required']) + + def test_realistic_database_function(self): + """Test realistic database query function.""" + + def query_users( + filter_conditions: Dict[str, str], + limit: int = 100, + offset: int = 0, + order_by: Optional[str] = None, + include_inactive: bool = False, + ) -> List[Dict[str, any]]: + """Query users from database. + + Args: + filter_conditions: Key-value pairs for filtering + limit: Maximum number of users to return + offset: Number of records to skip + order_by: Field to sort by + include_inactive: Whether to include inactive users + """ + return [] + + result = function_to_json_schema(query_users) + + props = result['properties'] + + # Check required dict parameter + self.assertEqual(props['filter_conditions']['type'], 'object') + self.assertIn('filter_conditions', result['required']) + + # Check integer parameters with defaults + for param in ['limit', 'offset']: + self.assertEqual(props[param]['type'], 'integer') + self.assertNotIn(param, result['required']) + + # Check optional string + self.assertEqual(props['order_by']['type'], 'string') + self.assertNotIn('order_by', result['required']) + + # Check boolean flag + self.assertEqual(props['include_inactive']['type'], 'boolean') + self.assertNotIn('include_inactive', result['required']) + + def test_pydantic_function_parameter(self): + """Test function with Pydantic model parameter.""" + try: + from pydantic import BaseModel + + class UserProfile(BaseModel): + name: str + email: str + age: Optional[int] = None + preferences: Dict[str, bool] = {} + + def update_user(user_id: str, profile: UserProfile, notify: bool = True) -> str: + """Update user profile. + + Args: + user_id: Unique user identifier + profile: User profile data + notify: Whether to send notification + """ + return 'updated' + + result = function_to_json_schema(update_user) + + props = result['properties'] + + # Check required string + self.assertEqual(props['user_id']['type'], 'string') + self.assertIn('user_id', result['required']) + + # Check Pydantic model (should have proper schema) + self.assertIn('profile', props) + self.assertIn('profile', result['required']) + + # Check boolean flag + self.assertEqual(props['notify']['type'], 'boolean') + self.assertNotIn('notify', result['required']) + + except ImportError: + self.skipTest('Pydantic not available for testing') + + +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestCreateToolFromFunction(unittest.TestCase): + """Test the create_tool_from_function function.""" + + def test_create_tool_basic(self): + """Test creating a tool from a basic function.""" + + def get_weather(location: str, unit: str = 'celsius') -> str: + """Get weather information. + + Args: + location: City name + unit: Temperature unit + """ + return f'Weather in {location}' + + # Test that the function works without errors + tool = create_tool_from_function(get_weather) + + # Basic validation that we got a tool object + self.assertIsNotNone(tool) + self.assertIsNotNone(tool.function) + self.assertEqual(tool.function.name, 'get_weather') + self.assertEqual(tool.function.description, 'Get weather information.') + + def test_create_tool_with_overrides(self): + """Test creating a tool with name and description overrides.""" + + def simple_func() -> str: + """Simple function.""" + return 'result' + + tool = create_tool_from_function( + simple_func, name='custom_name', description='Custom description' + ) + self.assertEqual(tool.function.name, 'custom_name') + self.assertEqual(tool.function.description, 'Custom description') + + def test_create_tool_complex_function(self): + """Test creating a tool from a complex function with multiple types.""" + from typing import Optional, List + from enum import Enum + + class Status(Enum): + ACTIVE = 'active' + INACTIVE = 'inactive' + + def manage_user( + user_id: str, + status: Status = Status.ACTIVE, + tags: Optional[List[str]] = None, + metadata: Dict[str, any] = None, + ) -> str: + """Manage user account. + + Args: + user_id: Unique user identifier + status: Account status + tags: User tags for categorization + metadata: Additional user metadata + """ + return f'Managed user {user_id}' + + tool = create_tool_from_function(manage_user) + self.assertIsNotNone(tool) + self.assertEqual(tool.function.name, 'manage_user') + + # Verify the schema was generated correctly by unpacking it + schema = tool.function.schema_as_dict() + self.assertIsNotNone(schema) + + # Check that all parameters are present + props = schema['properties'] + self.assertIn('user_id', props) + self.assertIn('status', props) + self.assertIn('tags', props) + self.assertIn('metadata', props) + + # Verify enum handling + self.assertEqual(props['status']['type'], 'string') + self.assertIn('enum', props['status']) + + +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestIntegrationScenarios(unittest.TestCase): + """Test real-world integration scenarios.""" + + def test_restaurant_finder_scenario(self): + """Test the restaurant finder example from the documentation.""" + from typing import Optional, List + from enum import Enum + + class PriceRange(Enum): + BUDGET = 'budget' + MODERATE = 'moderate' + EXPENSIVE = 'expensive' + + def find_restaurants( + location: str, + cuisine: str = 'any', + price_range: PriceRange = PriceRange.MODERATE, + max_results: int = 5, + dietary_restrictions: Optional[List[str]] = None, + ) -> str: + """Find restaurants in a specific location. + + Args: + location: The city or neighborhood to search + cuisine: Type of cuisine (italian, chinese, mexican, etc.) + price_range: Budget preference for dining + max_results: Maximum number of restaurant recommendations + dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) + """ + return f'Found restaurants in {location}' + + schema = function_to_json_schema(find_restaurants) + + # Comprehensive validation + self.assertEqual(schema['type'], 'object') + + # Check all properties exist + props = schema['properties'] + self.assertIn('location', props) + self.assertIn('cuisine', props) + self.assertIn('price_range', props) + self.assertIn('max_results', props) + self.assertIn('dietary_restrictions', props) + + # Check types + self.assertEqual(props['location']['type'], 'string') + self.assertEqual(props['cuisine']['type'], 'string') + self.assertEqual(props['price_range']['type'], 'string') + self.assertEqual(props['max_results']['type'], 'integer') + self.assertEqual(props['dietary_restrictions']['type'], 'array') + self.assertEqual(props['dietary_restrictions']['items']['type'], 'string') + + # Check enum values + self.assertEqual(set(props['price_range']['enum']), {'budget', 'moderate', 'expensive'}) + + # Check descriptions + self.assertIn('description', props['location']) + self.assertIn('description', props['cuisine']) + self.assertIn('description', props['price_range']) + + # Check required (only location is required) + self.assertEqual(schema['required'], ['location']) + + def test_weather_api_scenario(self): + """Test a weather API scenario with validation.""" + from typing import Optional + from enum import Enum + + class Units(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + KELVIN = 'kelvin' + + def get_weather_forecast( + latitude: float, + longitude: float, + units: Units = Units.CELSIUS, + days: int = 7, + include_hourly: bool = False, + api_key: Optional[str] = None, + ) -> Dict[str, any]: + """Get weather forecast for coordinates. + + Args: + latitude: Latitude coordinate + longitude: Longitude coordinate + units: Temperature units for response + days: Number of forecast days + include_hourly: Whether to include hourly forecasts + api_key: Optional API key override + """ + return {'forecast': []} + + schema = function_to_json_schema(get_weather_forecast) + + # Check numeric types + self.assertEqual(schema['properties']['latitude']['type'], 'number') + self.assertEqual(schema['properties']['longitude']['type'], 'number') + self.assertEqual(schema['properties']['days']['type'], 'integer') + self.assertEqual(schema['properties']['include_hourly']['type'], 'boolean') + + # Check enum + self.assertEqual(schema['properties']['units']['type'], 'string') + self.assertEqual( + set(schema['properties']['units']['enum']), {'celsius', 'fahrenheit', 'kelvin'} + ) + + # Check required fields + self.assertEqual(set(schema['required']), {'latitude', 'longitude'}) + + +class TestFallbackWhenImportsUnavailable(unittest.TestCase): + """Test fallback behavior when imports are not available.""" + + def test_imports_status(self): + """Test and report the status of helper imports.""" + if HELPERS_AVAILABLE: + print('✅ Helper functions successfully imported') + print('✅ All function-to-schema features are testable') + else: + print('⚠️ Helper functions not available due to circular imports') + print("⚠️ This is expected during development and doesn't affect functionality") + print('✅ Tests are properly skipped when imports fail') + + # This test always passes - it's just for reporting + self.assertTrue(True) + + +if __name__ == '__main__': + # Print import status + if HELPERS_AVAILABLE: + print('🧪 Running comprehensive tests for function-to-JSON-schema helpers') + else: + print('⚠️ Running limited tests due to import issues (this is expected)') + + unittest.main(verbosity=2) From dbf6f56a10454a200135f7acc9dbd7b8f89f7704 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 31 Jul 2025 20:09:30 -0500 Subject: [PATCH 03/29] minor name changes and cleanup unused function Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 4 +-- dapr/clients/grpc/_helpers.py | 50 ++++++--------------------------- dapr/clients/grpc/client.py | 6 ++-- 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index c669bc13..63e99237 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -63,7 +63,7 @@ to_bytes, validateNotNone, validateNotBlankString, - convert_parameters, + convert_dict_to_grpc_dict_of_any, convert_value_to_struct, ) from dapr.aio.clients.grpc._request import ( @@ -1907,7 +1907,7 @@ def _convert_message(message: ConversationMessage): tools_pb.append(proto_tool) # Convert raw Python parameters to GrpcAny objects - converted_parameters = convert_parameters(parameters) + converted_parameters = convert_dict_to_grpc_dict_of_any(parameters) # Build the request request = api_v1.ConversationRequestAlpha2( diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index 35997f93..b47da064 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -170,7 +170,7 @@ def convert_value_to_struct(value: Dict[str, Any]) -> Struct: ) from e -def convert_parameters_to_grpc_any(value: Any) -> GrpcAny: +def convert_value_to_grpc_any(value: Any) -> GrpcAny: """Convert a raw Python value to a GrpcAny protobuf message. This function automatically detects the type of the input value and wraps it in the appropriate protobuf wrapper type before packing it into GrpcAny. @@ -181,10 +181,10 @@ def convert_parameters_to_grpc_any(value: Any) -> GrpcAny: Raises: ValueError: If the value type is not supported Examples: - >>> convert_parameter_value("hello") # -> GrpcAny containing StringValue - >>> convert_parameter_value(42) # -> GrpcAny containing Int64Value - >>> convert_parameter_value(3.14) # -> GrpcAny containing DoubleValue - >>> convert_parameter_value(True) # -> GrpcAny containing BoolValue + >>> convert_value_to_grpc_any("hello") # -> GrpcAny containing StringValue + >>> convert_value_to_grpc_any(42) # -> GrpcAny containing Int64Value + >>> convert_value_to_grpc_any(3.14) # -> GrpcAny containing DoubleValue + >>> convert_value_to_grpc_any(True) # -> GrpcAny containing BoolValue """ # If it's already a GrpcAny, return as-is (backward compatibility) if isinstance(value, GrpcAny): @@ -218,7 +218,7 @@ def convert_parameters_to_grpc_any(value: Any) -> GrpcAny: return any_pb -def convert_parameters(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAny]: +def convert_dict_to_grpc_dict_of_any(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAny]: """Convert a dictionary of raw Python values to GrpcAny parameters. This function takes a dictionary with raw Python values and converts each value to the appropriate GrpcAny protobuf message for use in Dapr API calls. @@ -227,7 +227,7 @@ def convert_parameters(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAn Returns: Dictionary of parameter names to GrpcAny values Examples: - >>> convert_parameters({"temperature": 0.7, "max_tokens": 1000, "stream": False}) + >>> convert_dict_to_grpc_dict_of_any({"temperature": 0.7, "max_tokens": 1000, "stream": False}) >>> # Returns: {"temperature": GrpcAny, "max_tokens": GrpcAny, "stream": GrpcAny} """ if not parameters: @@ -235,45 +235,11 @@ def convert_parameters(parameters: Optional[Dict[str, Any]]) -> Dict[str, GrpcAn converted = {} for key, value in parameters.items(): - converted[key] = convert_parameters_to_grpc_any(value) + converted[key] = convert_value_to_grpc_any(value) return converted -def convert_json_schema_to_grpc_any(schema: Dict[str, Any]) -> GrpcAny: - """Convert a parameter schema dictionary to a GrpcAny object. - - This helper converts JSON schema objects to GrpcAny objects using - google.protobuf.struct_pb2.Struct for proper JSON representation. - - Args: - schema: Parameter schema dictionary (JSON schema object) - - Returns: - GrpcAny object containing the JSON schema as a Struct - - Examples: - >>> schema = { - ... "type": "object", - ... "properties": { - ... "location": {"type": "string", "description": "City name"} - ... }, - ... "required": ["location"] - ... } - >>> convert_json_schema_to_grpc_any(schema) - """ - if not isinstance(schema, dict): - raise ValueError(f'Schema must be a dictionary, got {type(schema)}') - - # Use Struct to represent the JSON schema object - struct = Struct() - struct.update(schema) - - any_pb = GrpcAny() - any_pb.Pack(struct) - return any_pb - - def create_tool_function( name: str, description: str, diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 33402fe3..72430cbd 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -55,7 +55,7 @@ from dapr.clients.grpc._helpers import ( MetadataTuple, to_bytes, - convert_parameters, + convert_dict_to_grpc_dict_of_any, convert_value_to_struct, ) from dapr.conf.helpers import GrpcEndpoint @@ -1764,7 +1764,7 @@ def converse_alpha1( ] # Convert raw Python parameters to GrpcAny objects - converted_parameters = convert_parameters(parameters) + converted_parameters = convert_dict_to_grpc_dict_of_any(parameters) request = api_v1.ConversationRequest( name=name, @@ -1910,7 +1910,7 @@ def _convert_message(message: ConversationMessage): tools_pb.append(proto_tool) # Convert raw Python parameters to GrpcAny objects - converted_parameters = convert_parameters(parameters) + converted_parameters = convert_dict_to_grpc_dict_of_any(parameters) # Build the request request = api_v1.ConversationRequestAlpha2( From 7a9a6cf4ac249731d17241cf11f3ba9e4551f55c Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:33:03 -0500 Subject: [PATCH 04/29] refactors, updates to readme, linting Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/_helpers.py | 5 +- dapr/clients/grpc/_request.py | 34 +- dapr/clients/grpc/_schema_helpers.py | 280 ++++++++- dapr/clients/grpc/client.py | 8 +- dev-requirements.txt | 4 +- examples/conversation/README.md | 276 +++++---- ...conversation.py => conversation_alpha1.py} | 0 examples/conversation/conversation_alpha2.py | 54 ++ .../real_llm_providers_example.py | 203 +------ tests/clients/test_conversation.py | 4 +- tests/clients/test_dapr_grpc_client.py | 4 +- tests/clients/test_grpc_helpers.py | 553 +++++++++++++++++- 12 files changed, 1057 insertions(+), 368 deletions(-) rename examples/conversation/{conversation.py => conversation_alpha1.py} (100%) create mode 100644 examples/conversation/conversation_alpha2.py diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index b47da064..1b967bdb 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -13,7 +13,10 @@ limitations under the License. """ from enum import Enum -from typing import Any, Dict, List, Optional, Union, Tuple +from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage diff --git a/dapr/clients/grpc/_request.py b/dapr/clients/grpc/_request.py index c479812c..5e13070a 100644 --- a/dapr/clients/grpc/_request.py +++ b/dapr/clients/grpc/_request.py @@ -14,24 +14,25 @@ """ import io +from dataclasses import dataclass, field from enum import Enum -from dataclasses import dataclass -from typing import Dict, Optional, Union, List +from typing import Callable, Dict, List, Optional, Union from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage -from dapr.proto import api_v1, common_v1 from dapr.clients._constants import DEFAULT_JSON_CONTENT_TYPE -from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions +from dapr.clients.grpc._crypto import DecryptOptions, EncryptOptions from dapr.clients.grpc._helpers import ( MetadataDict, MetadataTuple, - tuple_to_dict, to_bytes, to_str, + tuple_to_dict, unpack, ) +from dapr.clients.grpc._schema_helpers import function_to_json_schema, extract_docstring_summary +from dapr.proto import api_v1, common_v1 class DaprRequest: @@ -449,7 +450,7 @@ class ConversationMessageOfDeveloper: """Developer message content.""" name: Optional[str] = None - content: List[ConversationMessageContent] = None + content: List[ConversationMessageContent] = field(default_factory=list) @dataclass @@ -457,7 +458,7 @@ class ConversationMessageOfSystem: """System message content.""" name: Optional[str] = None - content: List[ConversationMessageContent] = None + content: List[ConversationMessageContent] = field(default_factory=list) @dataclass @@ -465,7 +466,7 @@ class ConversationMessageOfUser: """User message content.""" name: Optional[str] = None - content: List[ConversationMessageContent] = None + content: List[ConversationMessageContent] = field(default_factory=list) @dataclass @@ -489,8 +490,8 @@ class ConversationMessageOfAssistant: """Assistant message content.""" name: Optional[str] = None - content: List[ConversationMessageContent] = None - tool_calls: List[ConversationToolCalls] = None + content: List[ConversationMessageContent] = field(default_factory=list) + tool_calls: List[ConversationToolCalls] = field(default_factory=list) @dataclass @@ -499,7 +500,7 @@ class ConversationMessageOfTool: tool_id: Optional[str] = None name: str = '' - content: List[ConversationMessageContent] = None + content: List[ConversationMessageContent] = field(default_factory=list) @dataclass @@ -531,12 +532,21 @@ class ConversationToolsFunction: def schema_as_dict(self) -> Dict: """Return the function's schema as a dictionary. - + Returns: Dict: The JSON schema for the function parameters. """ return self.parameters or {} + @classmethod + def from_function(cls, func: Callable) -> 'ConversationToolsFunction': + """Create a ConversationToolsFunction from a function.""" + return cls( + name=func.__name__, + description=extract_docstring_summary(func), + parameters=function_to_json_schema(func), + ) + @dataclass class ConversationTools: diff --git a/dapr/clients/grpc/_schema_helpers.py b/dapr/clients/grpc/_schema_helpers.py index dfcf5d47..75ba4647 100644 --- a/dapr/clients/grpc/_schema_helpers.py +++ b/dapr/clients/grpc/_schema_helpers.py @@ -17,7 +17,10 @@ import inspect from dataclasses import fields, is_dataclass from enum import Enum -from typing import Any, Dict, Optional, Union, get_args, get_origin, get_type_hints +from typing import Any, Dict, List, Optional, Union, get_args, get_origin, get_type_hints, TYPE_CHECKING + +if TYPE_CHECKING: + from dapr.clients.grpc._request import ConversationTools """ Schema Helpers for Dapr Conversation API. @@ -76,7 +79,7 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s # Handle Dict types if origin is dict or python_type is dict: - schema = {'type': 'object'} + schema: Dict[str, Any] = {'type': 'object'} if args and len(args) == 2: # Dict[str, ValueType] - add additionalProperties key_type, value_type = args @@ -118,23 +121,23 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s if is_dataclass(python_type): from dataclasses import MISSING - schema = {'type': 'object', 'properties': {}, 'required': []} + dataclass_schema: Dict[str, Any] = {'type': 'object', 'properties': {}, 'required': []} for field in fields(python_type): field_schema = python_type_to_json_schema(field.type, field.name) - schema['properties'][field.name] = field_schema + dataclass_schema['properties'][field.name] = field_schema # Check if field has no default (required) - use MISSING for dataclasses if field.default is MISSING: - schema['required'].append(field.name) + dataclass_schema['required'].append(field.name) - return schema + return dataclass_schema # Fallback for unknown types return {'type': 'string', 'description': f'Unknown type: {python_type}'} -def extract_docstring_info(func) -> Dict[str, str]: +def extract_docstring_args(func) -> Dict[str, str]: """Extract parameter descriptions from function docstring. Supports Google-style, NumPy-style, and Sphinx-style docstrings. @@ -144,54 +147,287 @@ def extract_docstring_info(func) -> Dict[str, str]: Returns: Dict mapping parameter names to their descriptions + + Raises: + ValueError: If docstring contains parameter info but doesn't match supported formats """ docstring = inspect.getdoc(func) if not docstring: return {} param_descriptions = {} - - # Simple regex-based extraction for common docstring formats lines = docstring.split('\n') + + # First, try to extract Sphinx-style parameters (:param name: description) + param_descriptions.update(_extract_sphinx_params(lines)) + + # If no Sphinx-style params found, try Google/NumPy style + if not param_descriptions: + param_descriptions.update(_extract_google_numpy_params(lines)) + + # If still no parameters found, check if docstring might have parameter info + # in an unsupported format + if not param_descriptions and _has_potential_param_info(lines): + func_name = getattr(func, '__name__', 'unknown') + import warnings + + warnings.warn( + f"Function '{func_name}' has a docstring that appears to contain parameter " + f"information, but it doesn't match any supported format (Google, NumPy, or Sphinx style). " + f'Consider reformatting the docstring to use one of the supported styles for ' + f'automatic parameter extraction or create the tool manually.', + UserWarning, + stacklevel=2, + ) + + return param_descriptions + + +def _has_potential_param_info(lines: List[str]) -> bool: + """Check if docstring lines might contain parameter information in unsupported format. + + This is a heuristic to detect when a docstring might have parameter info + but doesn't match our supported formats (Google, NumPy, Sphinx). + """ + text = ' '.join(line.strip().lower() for line in lines) + + # Look for specific parameter documentation patterns that suggest + # an attempt to document parameters in an unsupported format + import re + + # Look for informal parameter descriptions like: + # "The filename parameter should be..." or "parameter_name is used for..." + has_param_descriptions = bool( + re.search(r'the\s+\w+\s+parameter\s+(should|is|controls|specifies)', text) + ) + + # Look for patterns where parameters are mentioned with descriptions + # "filename parameter", "mode parameter", etc. + has_param_mentions = bool( + re.search(r'\w+\s+parameter\s+(should|is|controls|specifies|contains)', text) + ) + + # Look for informal patterns like "takes param1 which is", "param2 is an integer" + has_informal_param_descriptions = bool( + re.search(r'takes\s+\w+\s+which\s+(is|are)', text) or + re.search(r'\w+\s+(is|are)\s+(a|an)\s+\w+\s+(input|argument)', text) + ) + + # Look for multiple parameter mentions suggesting documentation attempt + param_count = text.count(' parameter ') + has_multiple_param_mentions = param_count >= 2 + + # Exclude common phrases that don't indicate parameter documentation attempts + exclude_phrases = [ + 'no parameters', + 'without parameters', + 'parameters documented', + 'parameter information', + 'parameter extraction', + 'function parameter', + 'optional parameter', + 'required parameter', # These are often in general descriptions + ] + has_excluded_phrases = any(phrase in text for phrase in exclude_phrases) + + return ( + (has_param_descriptions or has_param_mentions or has_informal_param_descriptions or has_multiple_param_mentions) + and not has_excluded_phrases + ) + + +def _extract_sphinx_params(lines: List[str]) -> Dict[str, str]: + """Extract parameters from Sphinx-style docstring. + + Looks for patterns like: + :param name: description + :parameter name: description + """ + import re + + param_descriptions = {} + + for original_line in lines: + line = original_line.strip() + + # Match Sphinx-style parameter documentation + # Patterns: :param name: description or :parameter name: description + param_match = re.match(r':param(?:eter)?\s+(\w+)\s*:\s*(.*)', line) + if param_match: + param_name = param_match.group(1) + description = param_match.group(2).strip() + param_descriptions[param_name] = description + continue + + # Handle multi-line descriptions for Sphinx style + # If line is indented and we have existing params, it might be a continuation + if ( + original_line.startswith(' ') or original_line.startswith('\t') + ) and param_descriptions: + # Check if this could be a continuation of the last parameter + last_param = list(param_descriptions.keys())[-1] + # Don't treat section headers or other directive-like content as continuations + # Also don't treat content that looks like parameter definitions from other styles + if (param_descriptions[last_param] + and not any(line.startswith(prefix) for prefix in [':param', ':type', ':return', ':raises']) + and not line.lower().endswith(':') + and not line.lower() in ('args', 'arguments', 'parameters', 'params') + and ':' not in line): # Avoid treating "param1: description" as continuation + param_descriptions[last_param] += ' ' + line.strip() + + return param_descriptions + + +def _extract_google_numpy_params(lines: List[str]) -> Dict[str, str]: + """Extract parameters from Google/NumPy-style docstring.""" + param_descriptions = {} in_args_section = False current_param = None - for line in lines: - line = line.strip() + for i, original_line in enumerate(lines): + line = original_line.strip() # Detect Args/Parameters section if line.lower() in ('args:', 'arguments:', 'parameters:', 'params:'): in_args_section = True continue + # Handle NumPy style section headers with dashes + if line.lower() in ('parameters', 'arguments') and in_args_section is False: + in_args_section = True + continue + + # Skip NumPy-style separator lines (dashes) but also check if this signals section end + if in_args_section and line and all(c in '-=' for c in line): + # Check if next line starts a new section + next_line_idx = i + 1 + if next_line_idx < len(lines): + next_line = lines[next_line_idx].strip().lower() + if next_line in ('returns', 'return', 'yields', 'yield', 'raises', 'raise', 'notes', 'note', 'examples', 'example'): + in_args_section = False + continue + # Exit args section on new section - if in_args_section and line.endswith(':') and not line.startswith(' '): + if in_args_section and (line.endswith(':') and not line.startswith(' ')): + in_args_section = False + continue + + # Also exit on direct section headers without separators + if in_args_section and line.lower() in ('returns', 'return', 'yields', 'yield', 'raises', 'raise', 'notes', 'note', 'examples', 'example'): in_args_section = False continue if in_args_section and line: - # Look for parameter definitions (contains colon and doesn't look like a continuation) - if ':' in line and not line.startswith(' '): + # Look for parameter definitions (contains colon) + if ':' in line: parts = line.split(':', 1) if len(parts) == 2: param_name = parts[0].strip() description = parts[1].strip() + # Handle type annotations like "param_name (type): description" if '(' in param_name and ')' in param_name: param_name = param_name.split('(')[0].strip() - param_descriptions[param_name] = description + # Handle NumPy style "param_name : type" format where description is on next line + if ' ' in param_name: + param_name = param_name.split()[0] + + # Check if this looks like a real description vs just a type annotation + # For NumPy style: "param : type" vs Google style: "param: description" + # Type annotations are usually single words like "str", "int", "float" + # Descriptions have multiple words or punctuation + if description: + if description.replace(' ', '').isalnum() and len(description.split()) == 1: + # Likely just a type annotation (single alphanumeric word), wait for real description + param_descriptions[param_name] = '' + else: + # Contains multiple words or punctuation, likely a real description + param_descriptions[param_name] = description + else: + param_descriptions[param_name] = '' current_param = param_name - elif current_param: - # Continuation of previous parameter description - param_descriptions[current_param] += ' ' + line.strip() + elif current_param and ( + original_line.startswith(' ') or original_line.startswith('\t') + ) and in_args_section: + # Indented continuation line for current parameter (only if still in args section) + if not param_descriptions[current_param]: + # First description line for this parameter (for cases where description is on next line) + param_descriptions[current_param] = line + else: + # Additional description lines + param_descriptions[current_param] += ' ' + line return param_descriptions +def extract_docstring_summary(func) -> Optional[str]: + """Extract only the summary from a function's docstring. + + Args: + func: The function to extract the summary from + + Returns: + The summary portion of the docstring, or None if no docstring exists + """ + docstring = inspect.getdoc(func) + if not docstring: + return None + + lines = docstring.strip().split('\n') + if not lines: + return None + + # Extract all lines before the first section header + summary_lines = [] + + for line in lines: + line = line.strip() + + # Skip empty lines + if not line: + continue + + # Check if this line starts a Google/NumPy-style section + google_numpy_headers = ( + 'args:', + 'arguments:', + 'parameters:', + 'params:', + 'returns:', + 'return:', + 'yields:', + 'yield:', + 'raises:', + 'raise:', + 'note:', + 'notes:', + 'example:', + 'examples:', + 'see also:', + 'references:', + 'attributes:', + ) + if line.lower().endswith(':') and line.lower() in google_numpy_headers: + break + + # Check if this line starts a Sphinx-style section + # Look for patterns like :param name:, :returns:, :raises:, etc. + import re + + sphinx_pattern = r'^:(?:param|parameter|type|returns?|return|yields?|yield|raises?|raise|note|notes|example|examples|see|seealso|references?|attributes?)(?:\s+\w+)?:' + if re.match(sphinx_pattern, line.lower()): + break + + summary_lines.append(line) + + return ' '.join(summary_lines) if summary_lines else None + + def function_to_json_schema( func, name: Optional[str] = None, description: Optional[str] = None ) -> Dict[str, Any]: """Convert a Python function to a JSON schema for tool calling. + All parameters without default values are set as required. Args: func: The Python function to convert @@ -219,7 +455,7 @@ def function_to_json_schema( type_hints = get_type_hints(func) # Extract parameter descriptions from docstring - param_descriptions = extract_docstring_info(func) + param_descriptions = extract_docstring_args(func) # Get function description if description is None: @@ -231,7 +467,7 @@ def function_to_json_schema( description = f'Function {func.__name__}' # Build JSON schema - schema = {'type': 'object', 'properties': {}, 'required': []} + schema: Dict[str, Any] = {'type': 'object', 'properties': {}, 'required': []} for param_name, param in sig.parameters.items(): # Skip *args and **kwargs @@ -257,7 +493,9 @@ def function_to_json_schema( return schema -def create_tool_from_function(func, name: Optional[str] = None, description: Optional[str] = None): +def create_tool_from_function( + func, name: Optional[str] = None, description: Optional[str] = None +) -> 'ConversationTools': """Create a ConversationTools from a Python function with type hints. This provides the ultimate developer experience - just define a typed function diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 72430cbd..041fb716 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -40,7 +40,11 @@ from dapr.clients.exceptions import DaprInternalError, DaprGrpcError from dapr.clients.grpc._state import StateOptions, StateItem -from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus, validateNotBlankString, validateNotNone +from dapr.clients.grpc._helpers import ( + getWorkflowRuntimeStatus, + validateNotBlankString, + validateNotNone, +) from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc.subscription import Subscription, StreamInactiveError from dapr.clients.grpc.interceptors import DaprClientInterceptor, DaprClientTimeoutInterceptor @@ -1794,7 +1798,7 @@ def converse_alpha2( inputs: List[ConversationInputAlpha2], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, GrpcAny | Any]] = None, + parameters: Optional[Dict[str, Union[GrpcAny, Any]]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, diff --git a/dev-requirements.txt b/dev-requirements.txt index 60daeae1..cbd71985 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -18,7 +18,7 @@ ruff===0.2.2 durabletask-dapr >= 0.2.0a7 # needed for .env file loading in examples python-dotenv>=1.0.0 -# needed for testing enhanced schema generation features +# needed for enhanced schema generation from function features pydantic>=2.0.0 # needed for yaml file generation in examples -PyYAML>=6.0.2 \ No newline at end of file +PyYAML>=6.0.2 diff --git a/examples/conversation/README.md b/examples/conversation/README.md index 2df56bf8..3be9fe24 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -38,128 +38,117 @@ The Conversation API supports real LLM providers including: python examples/conversation/real_llm_providers_example.py ``` + Depending on what API key you have, this will run and print the result of each test function in the example file. + + Before running the example, you need to start the Dapr sidecar with the component configurations as shown below in our run output. + Here we have a temporary directory with the component configurations with the API keys setup in the .env file. + + ```bash + dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path + ``` + + For example if we have openai, anthropic, mistral, deepseek and google ai, we will have a temporary directory with the component configurations for each provider: + + The example will run and print the result of each test function in the example file. + ```bash + 🚀 Real LLM Providers Example for Dapr Conversation API Alpha2 + ============================================================ + 📁 Loaded environment from /Users/filinto/diagrid/python-sdk/examples/conversation/.env + + 🔍 Detecting available LLM providers... + + ✅ Found 5 configured provider(s) + 📝 Created component: /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3/openai.yaml + 📝 Created component: /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3/anthropic.yaml + 📝 Created component: /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3/mistral.yaml + 📝 Created component: /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3/deepseek.yaml + 📝 Created component: /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3/google.yaml + + ⚠️ IMPORTANT: Make sure Dapr sidecar is running with components from: + /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3 + + To start the sidecar with these components: + dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path /var/folders/3t/b6jkjnv970l6dd1sp81b19hw0000gn/T/dapr-llm-components-9mcpb1a3 + + Press Enter when Dapr sidecar is running with the component configurations... + ``` + + At this point, you can press Enter to continue if you have the Dapr sidecar running with the component configurations. + ## Alpha2 API Features The Alpha2 API introduces sophisticated features: -- **Advanced Message Types**: user, system, assistant, tool messages +- **Advanced Message Types**: user, system, assistant, developer, tool messages - **Automatic Parameter Conversion**: Raw Python values → GrpcAny -- **Enhanced Tool Calling**: Multi-turn tool workflows +- **Tool Calling**: Function calling with JSON schema definition - **Function-to-Schema**: Ultimate DevEx for tool creation - **Multi-turn Conversations**: Context accumulation across turns - **Async Support**: Full async/await implementation -## New Tool Creation Helpers (Alpha2) - Excellent DevEx! +## Current Limitations -The Alpha2 API introduces powerful new helper functions that dramatically simplify tool creation and parameter handling. +- **Streaming**: Response streaming is not yet supported in Alpha2. All responses are returned as complete messages. -### Before (Manual GrpcAny Creation) ❌ -```python -from google.protobuf.any_pb2 import Any as GrpcAny -import json - -# Manual, error-prone approach -location_param = GrpcAny() -location_param.value = json.dumps({ - "type": "string", - "description": "City name" -}).encode() - -unit_param = GrpcAny() -unit_param.value = json.dumps({ - "type": "string", - "enum": ["celsius", "fahrenheit"] -}).encode() - -weather_tool = ConversationTools( - function=ConversationToolsFunction( - name="get_weather", - description="Get weather", - parameters={ - "location": location_param, # ✅ This part was correct - "unit": unit_param, # ✅ This part was correct - "required": ["location"] # ❌ This causes CopyFrom errors! - } - ) -) -``` +## Tool Creation (Alpha2) - Excellent DevEx! -### After (Helper Functions) ✅ -```python -from dapr.clients.grpc._helpers import create_tool +The Alpha2 API provides powerful functions for tool creation and parameter handling with clean JSON schema support. -# Clean, simple, intuitive approach -weather_tool = create_tool( +### Simple JSON Schema Approach +```python +# Clean, simple, intuitive approach using standard JSON schema +function = ConversationToolsFunction( name="get_weather", description="Get current weather", parameters={ - "location": { - "type": "string", - "description": "City name" + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"] - } - }, - required=["location"] + "required": ["location"] + } ) +weather_tool = ConversationTools(function=function) ``` ## Understanding the Protobuf Structure -The Dapr Conversation API uses a specific protobuf structure for tool parameters that follows the OpenAI function calling standard: - -```protobuf -// ConversationToolsFunction.parameters is a map -// The parameters map directly represents the JSON schema structure -parameters: { - "type": GrpcAny(StringValue("object")), - "properties": GrpcAny(Struct with parameter definitions), - "required": GrpcAny(ListValue(["location"])) -} -``` +The Dapr Conversation API uses a protobuf Struct to represent JSON schema for tool parameters, which directly maps to JSON: -**Key insights:** -- ✅ The **parameters map IS the JSON schema** - direct field mapping -- ✅ Uses **proper protobuf types** for each schema field: - - `"type"`: `StringValue` for the schema type - - `"properties"`: `Struct` for parameter definitions - - `"required"`: `ListValue` for required field names -- ✅ **No wrapper keys** - each JSON schema field becomes a map entry -- ✅ This matches the **OpenAI function calling standard** exactly - -**Example JSON Schema:** -```json -{ - "type": "object", - "properties": { - "location": {"type": "string", "description": "City name"}, - "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} - }, - "required": ["location"] -} -``` - -**Becomes protobuf map:** ```python +# ConversationToolsFunction.parameters is a protobuf Struct (Dict[str, Any]) +# The parameters directly represent the JSON schema structure parameters = { - "type": GrpcAny(StringValue("object")), - "properties": GrpcAny(Struct({ + "type": "object", + "properties": { "location": {"type": "string", "description": "City name"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} - })), - "required": GrpcAny(ListValue(["location"])) + }, + "required": ["location"] } ``` -**Resolved Issues:** -- ❌ **Old Issue**: `'type.googleapis.com/google.protobuf.StringValue' is not of type 'object'` -- ✅ **New Solution**: Direct schema field mapping with proper protobuf types +**Key insights:** +- ✅ **Direct JSON representation** - parameters is a standard Python dict that gets converted to protobuf Struct +- ✅ **Clean developer experience** - write standard JSON schema as Python dict +- ✅ **OpenAI function calling standard** - follows OpenAI's JSON schema format + +**Benefits of Struct-based approach:** + +- Direct JSON schema representation +- Automatic type conversion +- Better error messages and debugging ### Automatic Parameter Conversion -Parameters are now automatically converted from raw Python types: +Parameters argument in the converse method for alpha2 are now automatically converted from raw Python types: ```python # Before: Manual GrpcAny creation for every parameter ❌ @@ -183,14 +172,14 @@ response = client.converse_alpha2( ) ``` + ### Function-to-Schema Approach (Ultimate DevEx!) -The most advanced approach: define typed Python functions and automatically generate tool schemas: +We provide a helper function to automatically generate the tool schema from a Python function. ```python from typing import Optional, List from enum import Enum -from dapr.clients.grpc._schema_helpers import function_to_json_schema from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools class Units(Enum): @@ -206,13 +195,8 @@ def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: ''' return f"Weather in {location}" -# Automatically generate schema from function -schema = function_to_json_schema(get_weather) -function = ConversationToolsFunction( - name="get_weather", - description="Get current weather for a location", - parameters=schema -) +# Use the from_function class method for automatic schema generation +function = ConversationToolsFunction.from_function(get_weather) weather_tool = ConversationTools(function=function) ``` @@ -224,22 +208,26 @@ weather_tool = ConversationTools(function=function) ### Multiple Tool Creation Approaches -#### 1. Simple Properties (Recommended) +#### 1. Simple JSON Schema (Recommended) ```python -create_tool( +function = ConversationToolsFunction( name="get_weather", description="Get weather", parameters={ - "location": {"type": "string", "description": "City"}, - "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} - }, - required=["location"] + "type": "object", + "properties": { + "location": {"type": "string", "description": "City"}, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + }, + "required": ["location"] + } ) +weather_tool = ConversationTools(function=function) ``` -#### 2. Full JSON Schema +#### 2. Complete JSON Schema ```python -create_tool( +function = ConversationToolsFunction( name="calculate", description="Perform calculations", parameters={ @@ -250,30 +238,37 @@ create_tool( "required": ["expression"] } ) +calc_tool = ConversationTools(function=function) ``` #### 3. No Parameters ```python -create_tool( +function = ConversationToolsFunction( name="get_time", - description="Get current time" + description="Get current time", + parameters={"type": "object", "properties": {}, "required": []} ) +time_tool = ConversationTools(function=function) ``` #### 4. Complex Schema with Arrays ```python -create_tool( +function = ConversationToolsFunction( name="search", description="Search the web", parameters={ - "query": {"type": "string"}, - "domains": { - "type": "array", - "items": {"type": "string"} - } - }, - required=["query"] + "type": "object", + "properties": { + "query": {"type": "string"}, + "domains": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["query"] + } ) +search_tool = ConversationTools(function=function) ``` ## Advanced Message Types (Alpha2) @@ -285,6 +280,10 @@ Alpha2 supports sophisticated message structures for complex conversations: from dapr.clients.grpc._request import ( ConversationMessage, ConversationMessageOfUser, + ConversationMessageOfSystem, + ConversationMessageOfDeveloper, + ConversationMessageOfAssistant, + ConversationMessageOfTool, ConversationMessageContent ) @@ -304,6 +303,16 @@ system_message = ConversationMessage( ) ``` +### Developer Messages +```python +developer_message = ConversationMessage( + of_developer=ConversationMessageOfDeveloper( + name="developer", + content=[ConversationMessageContent(text="System configuration update.")] + ) +) +``` + ### Assistant Messages ```python assistant_message = ConversationMessage( @@ -387,11 +396,10 @@ result = asyncio.run(async_conversation()) ## Benefits -- ✅ **80%+ less boilerplate code** -- ✅ **No more CopyFrom() errors** +- ✅ **Clean JSON schema definition** - ✅ **Automatic type conversion** -- ✅ **Multiple input formats** -- ✅ **JSON Schema validation hints** +- ✅ **Multiple input formats supported** +- ✅ **Direct Python dict to protobuf Struct conversion** - ✅ **Clean, readable code** - ✅ **Supports complex nested structures** - ✅ **Real LLM provider integration** @@ -458,16 +466,19 @@ conversation_history.append(assistant_message) - **`real_llm_providers_example.py`** - Comprehensive Alpha2 examples with real providers - Real LLM provider setup (OpenAI, Anthropic, Mistral, DeepSeek, Google AI) - - Advanced tool calling workflows - - Multi-turn conversations with context accumulation + - Basic conversation testing (Alpha2) + - Multi-turn conversation testing + - Tool calling with real LLMs + - Parameter conversion demonstration + - Multi-turn tool calling with context accumulation - Function-to-schema automatic tool generation - - Both sync and async implementations - - Parameter conversion demonstration + - Async conversation and tool calling support - Backward compatibility with Alpha1 -- **`conversation.py`** - Basic conversation examples +- **`conversation_alpha1.py`** - Basic conversation examples (Alpha1) - Simple Alpha1 conversation flow - - Basic tool calling setup +- **`conversation_alpha2.py`** - Basic conversation examples (Alpha2) + - Simple Alpha2 conversation flow - **Configuration files:** - `.env.example` - Environment variables template @@ -527,7 +538,7 @@ with DaprClient() as client: parameters={ 'temperature': 0.7, # Auto-converted to GrpcAny! 'max_tokens': 500, # Auto-converted to GrpcAny! - 'stream': False # Auto-converted to GrpcAny! + 'stream': False # Streaming not supported in Alpha2 }, tools=[weather_tool], tool_choice='auto' @@ -598,12 +609,17 @@ For a complete working example with multiple providers, see `real_llm_providers_ - No need to manually create GrpcAny objects for parameters - Supported types: int, float, bool, str, dict, list +5. **Streaming not available** + - Response streaming is not yet supported in Alpha2 + - Set `stream: False` in parameters (this is the default) + - All responses are returned as complete, non-streaming messages + ### Environment Variables ```bash # Required for respective providers -OPENAI_API_KEY=sk-... -ANTHROPIC_API_KEY=sk-ant-... +OPENAI_API_KEY=... +ANTHROPIC_API_KEY=... MISTRAL_API_KEY=... DEEPSEEK_API_KEY=... GOOGLE_API_KEY=... @@ -655,7 +671,7 @@ response = client.converse_alpha2( | Feature | Alpha1 | Alpha2 | |---------|--------|--------| | Basic Conversations | ✅ | ✅ | -| Tool Calling | ✅ | ✅ Enhanced | +| Tool Calling | ❌ | ✅ | | Multi-turn Context | ❌ | ✅ | | Advanced Message Types | ❌ | ✅ | | Parameter Auto-conversion | ❌ | ✅ | diff --git a/examples/conversation/conversation.py b/examples/conversation/conversation_alpha1.py similarity index 100% rename from examples/conversation/conversation.py rename to examples/conversation/conversation_alpha1.py diff --git a/examples/conversation/conversation_alpha2.py b/examples/conversation/conversation_alpha2.py new file mode 100644 index 00000000..dae72b11 --- /dev/null +++ b/examples/conversation/conversation_alpha2.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------ +# 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. +# ------------------------------------------------------------ +from dapr.clients import DaprClient +from dapr.clients.grpc._request import ( + ConversationInputAlpha2, + ConversationMessage, + ConversationMessageContent, + ConversationMessageOfUser, +) + +with DaprClient() as d: + inputs = [ + ConversationInputAlpha2( + messages=[ + ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text="What's Dapr?")] + ) + ) + ] + ), + ConversationInputAlpha2( + messages=[ + ConversationMessage( + of_user=ConversationMessageOfUser( + content=[ConversationMessageContent(text='Give a brief overview.')] + ) + ) + ] + ), + ] + + metadata = { + 'model': 'foo', + 'key': 'authKey', + 'cacheTTL': '10m', + } + + response = d.converse_alpha2( + name='echo', inputs=inputs, temperature=0.7, context_id='chat-123', metadata=metadata + ) + + for output in response.outputs: + print(f'Result: {output.choices[0].message.content}') diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index 1c6a7322..f3753023 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -116,160 +116,12 @@ def convert_llm_response_to_conversation_message( class RealLLMProviderTester: - """Test real LLM providers with Dapr Conversation API Alpha2. + """Test real LLM providers with Dapr Conversation API Alpha2.""" - This example demonstrates the new schema-based tool creation approach - using ConversationToolsFunction.from_schema() for proper JSON Schema handling. - - ## Tool Creation Examples - - ### 1. Simple Properties Approach (Recommended for most cases) - ```python - from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools - - function = ConversationToolsFunction.from_schema( - name="get_weather", - description="Get current weather", - json_schema={ - "properties": { - "location": { - "type": "string", - "description": "City name" - }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"] - } - }, - "required": ["location"] - } - ) - weather_tool = ConversationTools(function=function) - ``` - - ### 2. Function-to-Schema Approach (Ultimate DevEx - NEW!) - ```python - from typing import Optional, List - from enum import Enum - from dapr.clients.grpc._helpers import function_to_json_schema - - class Units(Enum): - CELSIUS = "celsius" - FAHRENHEIT = "fahrenheit" - - def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: - '''Get current weather for a location. - - Args: - location: The city and state or country - unit: Temperature unit preference - ''' - return f"Weather in {location}" - - # Automatically generate schema from function - schema = function_to_json_schema(get_weather) - function = ConversationToolsFunction.from_schema( - name="get_weather", - description="Get current weather for a location", - json_schema=schema - ) - weather_tool = ConversationTools(function=function) - - # Or use the complete helper (when available): - # from dapr.clients.grpc._helpers import create_tool_from_function - # weather_tool = create_tool_from_function(get_weather) - ``` - - ### 3. Full JSON Schema Approach (For complex schemas) - ```python - function = ConversationToolsFunction.from_schema( - name="calculate", - description="Perform calculations", - json_schema={ - "properties": { - "expression": { - "type": "string", - "description": "Math expression" - } - }, - "required": ["expression"] - } - ) - calc_tool = ConversationTools(function=function) - ``` - - ### 4. No Parameters Approach - ```python - function = ConversationToolsFunction.from_schema( - name="get_current_time", - description="Get current date and time", - json_schema={ - "properties": {}, - "required": [] - } - ) - time_tool = ConversationTools(function=function) - ``` - - ### 5. Complex Schema with Arrays and Nested Objects - ```python - function = ConversationToolsFunction.from_schema( - name="web_search", - description="Search the web", - json_schema={ - "properties": { - "query": {"type": "string"}, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 10 - }, - "domains": { - "type": "array", - "items": {"type": "string"} - } - }, - "required": ["query"] - } - ) - search_tool = ConversationTools(function=function) - ``` - - ### 6. Automatic Parameter Conversion - The client now automatically converts raw Python values to GrpcAny: - ```python - response = client.converse_alpha2( - name="my-provider", - inputs=[input_alpha2], - parameters={ - 'temperature': 0.7, - 'max_tokens': 100, - 'stream': False, - 'model': 'gpt-4', - }, - tools=[weather_tool] - ) - ``` - - ## Benefits - - ✅ **Function-to-Schema**: Ultimate DevEx - define typed functions, get tools automatically - - ✅ **Type Safety**: Full Python type hint support (str, int, List, Optional, Enum, etc.) - - ✅ **Auto-Documentation**: Docstring parsing for parameter descriptions - - ✅ **Proper JSON Schema**: ConversationToolsFunction.from_schema() handles protobuf correctly - - ✅ **Automatic Parameter Conversion**: Raw Python values → GrpcAny at gRPC call level - - ✅ **OpenAI Compatibility**: 100% compatible with OpenAI function calling standard - - ✅ **Multiple Approaches**: Choose the right level of abstraction for your needs - - ✅ **Clean Separation**: Tool creation separated from protobuf conversion - - ✅ **Reduced Complexity**: 90%+ less boilerplate compared to manual schema creation - """ - - def __init__(self, use_local_dev: bool = False): + def __init__(self): self.available_providers = {} self.component_configs = {} self.components_dir = None - self.use_local_dev = use_local_dev - # Note: Local dev sidecar management not implemented in this example - self.sidecar_manager = None # DaprSidecarManager() if use_local_dev else None def load_environment(self) -> None: """Load environment variables from .env file if available.""" @@ -389,8 +241,7 @@ def create_component_configs(self, selected_providers: Optional[List[str]] = Non return self.components_dir def create_weather_tool(self) -> ConversationTools: - """Create a weather tool for testing Alpha2 tool calling.""" - # Using the new ConversationToolsFunction.from_schema approach + """Create a weather tool for testing Alpha2 tool calling using full JSON schema in parameters approach.""" function = ConversationToolsFunction( name='get_weather', description='Get the current weather for a location', @@ -410,7 +261,7 @@ def create_weather_tool(self) -> ConversationTools: return ConversationTools(function=function) def create_calculator_tool(self) -> ConversationTools: - """Create a calculator tool using full JSON schema approach.""" + """Create a calculator tool using full JSON schema in parameters approach.""" function = ConversationToolsFunction( name='calculate', description='Perform mathematical calculations', @@ -428,7 +279,7 @@ def create_calculator_tool(self) -> ConversationTools: return ConversationTools(function=function) def create_time_tool(self) -> ConversationTools: - """Create a simple tool with no parameters.""" + """Create a simple tool with no parameters using full JSON schema in parameters approach.""" function = ConversationToolsFunction( name='get_current_time', description='Get the current date and time', @@ -437,7 +288,7 @@ def create_time_tool(self) -> ConversationTools: return ConversationTools(function=function) def create_search_tool(self) -> ConversationTools: - """Create a more complex tool with multiple parameter types.""" + """Create a more complex tool with multiple parameter types and constraints using full JSON schema in parameters approach.""" function = ConversationToolsFunction( name='web_search', description='Search the web for information', @@ -469,14 +320,13 @@ def create_search_tool(self) -> ConversationTools: return ConversationTools(function=function) def create_tool_from_typed_function_example(self) -> ConversationTools: - """Demonstrate creating tools from typed Python functions - Ultimate DevEx! + """Demonstrate creating tools from typed Python functions - Best DevEx for most cases. This shows the most advanced approach: define a typed function and automatically generate the complete tool schema from type hints and docstrings. """ from typing import Optional, List from enum import Enum - from dapr.clients.grpc._schema_helpers import function_to_json_schema # Define the tool behavior as a regular Python function with type hints class PriceRange(Enum): @@ -503,15 +353,8 @@ def find_restaurants( # This would contain actual implementation return f'Found restaurants in {location} serving {cuisine} food' - # Automatically generate JSON schema from the function - schema = function_to_json_schema(find_restaurants) - - # Create the tool using the generated schema - function = ConversationToolsFunction( - name='find_restaurants', - description='Find restaurants in a specific location', - parameters=schema, - ) + # Create the tool using the from_function class method + function = ConversationToolsFunction.from_function(find_restaurants) return ConversationTools(function=function) @@ -705,7 +548,10 @@ def test_tool_calling_alpha2(self, provider_id: str) -> None: name=tool_call.function.name, content=weather_result, ) - print('✅ Alpha2 tool calling demonstration completed!') + print( + '✅ Alpha2 tool calling demonstration completed! Tool Result Message:' + ) + print(tool_result_message) except json.JSONDecodeError: print('⚠️ Could not parse tool arguments') @@ -949,7 +795,7 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: print(f'❌ Multi-turn tool calling error: {e}') def test_function_to_schema_approach(self, provider_id: str) -> None: - """Test the ultimate DevEx: function-to-JSON-schema automatic tool creation.""" + """Test the best DevEx for most cases: function-to-JSON-schema automatic tool creation.""" print( f"\n🎯 Testing function-to-schema approach with {self.available_providers[provider_id]['display_name']}" ) @@ -980,7 +826,7 @@ def test_function_to_schema_approach(self, provider_id: str) -> None: print(f'📊 Finish reason: {choice.finish_reason}') if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: - print(f'🎯 Function-to-schema tool calling successful!') + print('🎯 Function-to-schema tool calling successful!') for tool_call in choice.message.tool_calls: print(f' Tool: {tool_call.function.name}') print(f' Arguments: {tool_call.function.arguments}') @@ -1146,24 +992,7 @@ def main(): print(__doc__) return - # Check if user wants to use local dev environment - use_local_dev = '--local-dev' in sys.argv or os.getenv('USE_LOCAL_DEV', '').lower() in ( - 'true', - '1', - 'yes', - ) - build_local_dapr = '--build-local-dapr' in sys.argv or os.getenv( - 'BUILD_LOCAL_DAPR', '' - ).lower() in ('true', '1', 'yes') - - if use_local_dev: - print('🔧 Using local development build (Alpha2 tool calling enabled)') - print(' This will automatically start and manage the Dapr sidecar') - else: - print('📋 Using manual Dapr sidecar setup') - print(" You'll need to start the Dapr sidecar manually") - - tester = RealLLMProviderTester(use_local_dev=use_local_dev) + tester = RealLLMProviderTester() try: # Load environment variables diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index eb52a1f8..d73d67bc 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -76,7 +76,7 @@ def create_weather_tool(self): }, }, 'required': ['location'], - } + }, ) ) @@ -95,7 +95,7 @@ def create_calculate_tool(self): } }, 'required': ['expression'], - } + }, ) ) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index ef328f1f..b992aa4c 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -1274,8 +1274,6 @@ def test_converse_alpha2_basic_user_message(self): def test_converse_alpha2_with_tools_weather_request(self): """Test Alpha2 conversation with tool calling for weather requests.""" - from google.protobuf.any_pb2 import Any as GrpcAny - dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create weather tool @@ -1288,7 +1286,7 @@ def test_converse_alpha2_with_tools_weather_request(self): 'properties': { 'location': {'type': 'string', 'description': 'Location for weather info'} }, - 'required': ['location'] + 'required': ['location'], }, ) ) diff --git a/tests/clients/test_grpc_helpers.py b/tests/clients/test_grpc_helpers.py index 32ba3c07..fbde03ae 100644 --- a/tests/clients/test_grpc_helpers.py +++ b/tests/clients/test_grpc_helpers.py @@ -10,6 +10,7 @@ import unittest import sys import os +import warnings from typing import Optional, List, Dict, Union from enum import Enum from dataclasses import dataclass @@ -21,9 +22,13 @@ try: from dapr.clients.grpc._schema_helpers import ( python_type_to_json_schema, - extract_docstring_info, + extract_docstring_args, function_to_json_schema, create_tool_from_function, + extract_docstring_summary, + ) + from dapr.clients.grpc._request import ( + ConversationToolsFunction, ) HELPERS_AVAILABLE = True @@ -298,7 +303,7 @@ def sample_function(name: str, age: int) -> str: """ return f'{name} is {age}' - result = extract_docstring_info(sample_function) + result = extract_docstring_args(sample_function) expected = {'name': "The person's name", 'age': "The person's age in years"} self.assertEqual(result, expected) @@ -308,7 +313,7 @@ def test_no_docstring(self): def no_doc_function(param): pass - result = extract_docstring_info(no_doc_function) + result = extract_docstring_args(no_doc_function) self.assertEqual(result, {}) def test_docstring_without_args(self): @@ -318,7 +323,7 @@ def simple_function(param): """Just a simple function.""" pass - result = extract_docstring_info(simple_function) + result = extract_docstring_args(simple_function) self.assertEqual(result, {}) def test_multiline_param_description(self): @@ -334,12 +339,185 @@ def complex_function(param1: str) -> str: """ return param1 - result = extract_docstring_info(complex_function) + result = extract_docstring_args(complex_function) expected = { 'param1': 'This is a long description that spans multiple lines for testing purposes' } self.assertEqual(result, expected) + def test_sphinx_style_docstring(self): + """Test Sphinx-style docstring parsing.""" + + def sphinx_function(location: str, unit: str) -> str: + """Get weather information. + + :param location: The city or location name + :param unit: Temperature unit (celsius or fahrenheit) + :type location: str + :type unit: str + :returns: Weather information string + :rtype: str + """ + return f'Weather in {location}' + + result = extract_docstring_args(sphinx_function) + expected = { + 'location': 'The city or location name', + 'unit': 'Temperature unit (celsius or fahrenheit)', + } + self.assertEqual(result, expected) + + def test_sphinx_style_with_parameter_keyword(self): + """Test Sphinx-style with :parameter: instead of :param:.""" + + def sphinx_function2(query: str, limit: int) -> str: + """Search for data. + + :parameter query: The search query string + :parameter limit: Maximum number of results + """ + return f'Results for {query}' + + result = extract_docstring_args(sphinx_function2) + expected = {'query': 'The search query string', 'limit': 'Maximum number of results'} + self.assertEqual(result, expected) + + def test_sphinx_style_multiline_descriptions(self): + """Test Sphinx-style with multi-line parameter descriptions.""" + + def sphinx_multiline_function(data: str) -> str: + """Process complex data. + + :param data: The input data to process, which can be + quite complex and may require special handling + for optimal results + :returns: Processed data + """ + return data + + result = extract_docstring_args(sphinx_multiline_function) + expected = { + 'data': 'The input data to process, which can be quite complex and may require special handling for optimal results' + } + self.assertEqual(result, expected) + + def test_numpy_style_docstring(self): + """Test NumPy-style docstring parsing.""" + + def numpy_function(x: float, y: float) -> float: + """Calculate distance. + + Parameters + ---------- + x : float + The x coordinate + y : float + The y coordinate + + Returns + ------- + float + The calculated distance + """ + return (x**2 + y**2) ** 0.5 + + result = extract_docstring_args(numpy_function) + expected = {'x': 'The x coordinate', 'y': 'The y coordinate'} + self.assertEqual(result, expected) + + def test_mixed_style_preference(self): + """Test that Sphinx-style takes precedence when both styles are present.""" + + def mixed_function(param1: str, param2: int) -> str: + """Function with mixed documentation styles. + + :param param1: Sphinx-style description for param1 + :param param2: Sphinx-style description for param2 + + Args: + param1: Google-style description for param1 + param2: Google-style description for param2 + """ + return f'{param1}: {param2}' + + result = extract_docstring_args(mixed_function) + expected = { + 'param1': 'Sphinx-style description for param1', + 'param2': 'Sphinx-style description for param2', + } + self.assertEqual(result, expected) + + def test_unsupported_format_warning(self): + """Test that unsupported docstring formats trigger a warning.""" + + def unsupported_function(param1: str, param2: int) -> str: + """Function with unsupported parameter documentation format. + + This function takes param1 which is a string input, + and param2 which is an integer argument. + """ + return f'{param1}: {param2}' + + with self.assertWarns(UserWarning) as warning_context: + result = extract_docstring_args(unsupported_function) + + # Should return empty dict since no supported format found + self.assertEqual(result, {}) + + # Check warning message content + warning_message = str(warning_context.warning) + self.assertIn('unsupported_function', warning_message) + self.assertIn('supported format', warning_message) + self.assertIn('Google, NumPy, or Sphinx style', warning_message) + + def test_informal_style_warning(self): + """Test that informal parameter documentation triggers a warning.""" + + def informal_function(filename: str, mode: str) -> str: + """Open and read a file. + + The filename parameter should be the path to the file. + The mode parameter controls how the file is opened. + """ + return f'Reading {filename} in {mode} mode' + + with self.assertWarns(UserWarning): + result = extract_docstring_args(informal_function) + + self.assertEqual(result, {}) + + def test_no_warning_for_no_params(self): + """Test that functions without parameter docs don't trigger warnings.""" + + def simple_function() -> str: + """Simple function with no parameters documented.""" + return 'hello' + + # Should not raise any warnings + with warnings.catch_warnings(): + warnings.simplefilter('error') # Turn warnings into errors + result = extract_docstring_args(simple_function) + + self.assertEqual(result, {}) + + def test_no_warning_for_valid_formats(self): + """Test that valid formats don't trigger warnings.""" + + def google_function(param: str) -> str: + """Function with Google-style docs. + + Args: + param: A parameter description + """ + return param + + # Should not raise any warnings + with warnings.catch_warnings(): + warnings.simplefilter('error') # Turn warnings into errors + result = extract_docstring_args(google_function) + + self.assertEqual(result, {'param': 'A parameter description'}) + @unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') class TestFunctionToJsonSchema(unittest.TestCase): @@ -639,6 +817,365 @@ def update_user(user_id: str, profile: UserProfile, notify: bool = True) -> str: self.skipTest('Pydantic not available for testing') +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestExtractDocstringSummary(unittest.TestCase): + """Test the extract_docstring_summary function.""" + + def test_simple_docstring(self): + """Test function with simple one-line docstring.""" + + def simple_function(): + """Simple one-line description.""" + pass + + result = extract_docstring_summary(simple_function) + self.assertEqual(result, 'Simple one-line description.') + + def test_full_docstring_with_extended_summary(self): + """Test function with full docstring including extended summary.""" + + def complex_function(): + """Get weather information for a specific location. + + This function retrieves current weather data including temperature, + humidity, and precipitation for the given location. + + Args: + location: The city or location to get weather for + unit: Temperature unit (celsius or fahrenheit) + + Returns: + Weather information as a string + + Raises: + ValueError: If location is invalid + """ + pass + + result = extract_docstring_summary(complex_function) + expected = ( + 'Get weather information for a specific location. ' + 'This function retrieves current weather data including temperature, ' + 'humidity, and precipitation for the given location.' + ) + self.assertEqual(result, expected) + + def test_multiline_summary_before_args(self): + """Test function with multiline summary that stops at Args section.""" + + def multiline_summary_function(): + """Complex function that does many things. + + This is an extended description that spans multiple lines + and provides more context about what the function does. + + Args: + param1: First parameter + """ + pass + + result = extract_docstring_summary(multiline_summary_function) + expected = ( + 'Complex function that does many things. ' + 'This is an extended description that spans multiple lines ' + 'and provides more context about what the function does.' + ) + self.assertEqual(result, expected) + + def test_no_docstring(self): + """Test function without docstring.""" + + def no_docstring_function(): + pass + + result = extract_docstring_summary(no_docstring_function) + self.assertIsNone(result) + + def test_empty_docstring(self): + """Test function with empty docstring.""" + + def empty_docstring_function(): + """""" + pass + + result = extract_docstring_summary(empty_docstring_function) + self.assertIsNone(result) + + def test_docstring_with_only_whitespace(self): + """Test function with docstring containing only whitespace.""" + + def whitespace_docstring_function(): + """ """ + pass + + result = extract_docstring_summary(whitespace_docstring_function) + self.assertIsNone(result) + + def test_docstring_stops_at_various_sections(self): + """Test that summary extraction stops at various section headers.""" + + def function_with_returns(): + """Function description. + + Returns: + Something useful + """ + pass + + def function_with_raises(): + """Function description. + + Raises: + ValueError: If something goes wrong + """ + pass + + def function_with_note(): + """Function description. + + Note: + This is important to remember + """ + pass + + # Test each section header + for func in [function_with_returns, function_with_raises, function_with_note]: + result = extract_docstring_summary(func) + self.assertEqual(result, 'Function description.') + + def test_docstring_with_parameters_section(self): + """Test docstring with Parameters section (alternative to Args).""" + + def function_with_parameters(): + """Process data efficiently. + + Parameters: + data: Input data to process + options: Processing options + """ + pass + + result = extract_docstring_summary(function_with_parameters) + self.assertEqual(result, 'Process data efficiently.') + + def test_docstring_with_example_section(self): + """Test docstring with Example section.""" + + def function_with_example(): + """Calculate the area of a circle. + + Example: + >>> calculate_area(5) + 78.54 + """ + pass + + result = extract_docstring_summary(function_with_example) + self.assertEqual(result, 'Calculate the area of a circle.') + + def test_case_insensitive_section_headers(self): + """Test that section header matching is case insensitive.""" + + def function_with_uppercase_args(): + """Function with uppercase section. + + ARGS: + param: A parameter + """ + pass + + result = extract_docstring_summary(function_with_uppercase_args) + self.assertEqual(result, 'Function with uppercase section.') + + def test_sphinx_style_summary_extraction(self): + """Test that Sphinx-style docstrings stop at :param: sections.""" + + def sphinx_function(): + """Calculate mathematical operations. + + This function performs various mathematical calculations + with high precision and error handling. + + :param x: First number + :param y: Second number + :returns: Calculation result + """ + pass + + result = extract_docstring_summary(sphinx_function) + expected = ( + 'Calculate mathematical operations. ' + 'This function performs various mathematical calculations ' + 'with high precision and error handling.' + ) + self.assertEqual(result, expected) + + def test_mixed_sphinx_google_summary(self): + """Test summary extraction stops at first section marker (Sphinx or Google).""" + + def mixed_function(): + """Process data with multiple algorithms. + + This is an extended description that provides + more context about the processing methods. + + :param data: Input data + + Args: + additional: More parameters + """ + pass + + result = extract_docstring_summary(mixed_function) + expected = ( + 'Process data with multiple algorithms. ' + 'This is an extended description that provides ' + 'more context about the processing methods.' + ) + self.assertEqual(result, expected) + + +@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') +class TestConversationToolsFunctionFromFunction(unittest.TestCase): + """Test the ConversationToolsFunction.from_function method.""" + + def test_from_function_basic(self): + """Test creating ConversationToolsFunction from a basic function.""" + + def test_function(param1: str, param2: int = 10): + """Test function for conversion. + + Args: + param1: First parameter + param2: Second parameter with default + """ + return f'{param1}: {param2}' + + result = ConversationToolsFunction.from_function(test_function) + + # Check basic properties + self.assertEqual(result.name, 'test_function') + self.assertEqual(result.description, 'Test function for conversion.') + self.assertIsInstance(result.parameters, dict) + + # Check that parameters schema was generated + self.assertEqual(result.parameters['type'], 'object') + self.assertIn('properties', result.parameters) + self.assertIn('required', result.parameters) + + def test_from_function_with_complex_docstring(self): + """Test from_function with complex docstring extracts only summary.""" + + def complex_function(location: str): + """Get weather information for a location. + + This function provides comprehensive weather data including + current conditions and forecasts. + + Args: + location: The location to get weather for + + Returns: + str: Weather information + + Raises: + ValueError: If location is invalid + + Example: + >>> get_weather("New York") + "Sunny, 72°F" + """ + return f'Weather for {location}' + + result = ConversationToolsFunction.from_function(complex_function) + + expected_description = ( + 'Get weather information for a location. ' + 'This function provides comprehensive weather data including ' + 'current conditions and forecasts.' + ) + self.assertEqual(result.description, expected_description) + + def test_from_function_no_docstring(self): + """Test from_function with function that has no docstring.""" + + def no_doc_function(param): + return param + + result = ConversationToolsFunction.from_function(no_doc_function) + + self.assertEqual(result.name, 'no_doc_function') + self.assertIsNone(result.description) + self.assertIsInstance(result.parameters, dict) + + def test_from_function_simple_docstring(self): + """Test from_function with simple one-line docstring.""" + + def simple_function(): + """Simple function description.""" + pass + + result = ConversationToolsFunction.from_function(simple_function) + + self.assertEqual(result.name, 'simple_function') + self.assertEqual(result.description, 'Simple function description.') + + def test_from_function_sphinx_style_summary(self): + """Test from_function extracts only summary from Sphinx-style docstring.""" + + def sphinx_function(location: str): + """Get weather information for a location. + + This function provides comprehensive weather data including + current conditions and forecasts using various APIs. + + :param location: The location to get weather for + :type location: str + :returns: Weather information string + :rtype: str + :raises ValueError: If location is invalid + """ + return f'Weather for {location}' + + result = ConversationToolsFunction.from_function(sphinx_function) + + expected_description = ( + 'Get weather information for a location. ' + 'This function provides comprehensive weather data including ' + 'current conditions and forecasts using various APIs.' + ) + self.assertEqual(result.description, expected_description) + + def test_from_function_google_style_summary(self): + """Test from_function extracts only summary from Google-style docstring.""" + + def google_function(data: str): + """Process input data efficiently. + + This function handles various data formats and applies + multiple processing algorithms for optimal results. + + Args: + data: The input data to process + + Returns: + str: Processed data string + + Raises: + ValueError: If data format is invalid + """ + return f'Processed {data}' + + result = ConversationToolsFunction.from_function(google_function) + + expected_description = ( + 'Process input data efficiently. ' + 'This function handles various data formats and applies ' + 'multiple processing algorithms for optimal results.' + ) + self.assertEqual(result.description, expected_description) + + @unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') class TestCreateToolFromFunction(unittest.TestCase): """Test the create_tool_from_function function.""" @@ -679,8 +1216,8 @@ def simple_func() -> str: def test_create_tool_complex_function(self): """Test creating a tool from a complex function with multiple types.""" - from typing import Optional, List from enum import Enum + from typing import List, Optional class Status(Enum): ACTIVE = 'active' @@ -728,8 +1265,8 @@ class TestIntegrationScenarios(unittest.TestCase): def test_restaurant_finder_scenario(self): """Test the restaurant finder example from the documentation.""" - from typing import Optional, List from enum import Enum + from typing import List, Optional class PriceRange(Enum): BUDGET = 'budget' @@ -788,8 +1325,8 @@ def find_restaurants( def test_weather_api_scenario(self): """Test a weather API scenario with validation.""" - from typing import Optional from enum import Enum + from typing import Optional class Units(Enum): CELSIUS = 'celsius' From 287d4b7c586b7ff6500cf2e198c9be68711f805a Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Tue, 12 Aug 2025 08:26:47 -0500 Subject: [PATCH 05/29] feedback Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 6 +- ...ma_helpers.py => _conversation_helpers.py} | 0 dapr/clients/grpc/_helpers.py | 170 +----------------- pyproject.toml | 9 +- 4 files changed, 5 insertions(+), 180 deletions(-) rename dapr/clients/grpc/{_schema_helpers.py => _conversation_helpers.py} (100%) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index a2b1ebb6..65ce1ef4 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -1763,14 +1763,14 @@ async def converse_alpha1( for inp in inputs ] - # # Convert raw Python parameters to GrpcAny objects - # converted_parameters = convert_parameters(parameters) + # Convert raw Python parameters to GrpcAny objects + converted_parameters = convert_dict_to_grpc_dict_of_any(parameters) request = api_v1.ConversationRequest( name=name, inputs=inputs_pb, contextID=context_id, - parameters=parameters, + parameters=converted_parameters, metadata=metadata or {}, scrubPII=scrub_pii, temperature=temperature, diff --git a/dapr/clients/grpc/_schema_helpers.py b/dapr/clients/grpc/_conversation_helpers.py similarity index 100% rename from dapr/clients/grpc/_schema_helpers.py rename to dapr/clients/grpc/_conversation_helpers.py diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index 1b967bdb..103e1394 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -29,6 +29,7 @@ BytesValue, ) from google.protobuf.struct_pb2 import Struct +from google.protobuf import json_format MetadataDict = Dict[str, List[Union[bytes, str]]] MetadataTuple = Tuple[Tuple[str, Union[bytes, str]], ...] @@ -149,8 +150,6 @@ def convert_value_to_struct(value: Dict[str, Any]) -> Struct: if not isinstance(value, dict) and not isinstance(value, bytes): raise ValueError(f'Value must be a dictionary, got {type(value)}') - from google.protobuf import json_format - # Convert the value to a JSON-serializable format first # Handle bytes by converting to base64 string for JSON compatibility if isinstance(value, bytes): @@ -241,170 +240,3 @@ def convert_dict_to_grpc_dict_of_any(parameters: Optional[Dict[str, Any]]) -> Di converted[key] = convert_value_to_grpc_any(value) return converted - - -def create_tool_function( - name: str, - description: str, - parameters: Optional[Dict[str, Any]] = None, - required: Optional[List[str]] = None, -) -> 'ConversationToolsFunction': - """Create a tool function with automatic parameter conversion. - - This helper automatically converts Python dictionaries to the proper JSON Schema - format required by the Dapr Conversation API. - - The parameters map directly represents the JSON schema structure using proper protobuf types. - - Args: - name: Function name - description: Human-readable description of what the function does - parameters: Parameter definitions (raw Python dict) - required: List of required parameter names - - Returns: - ConversationToolsFunction ready to use with Alpha2 API - - Examples: - # Simple approach - properties become JSON schema - >>> create_tool_function( - ... name="get_weather", - ... description="Get current weather", - ... parameters={ - ... "location": {"type": "string", "description": "City name"}, - ... "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} - ... }, - ... required=["location"] - ... ) - # Creates: parameters = { - # "type": StringValue("object"), - # "properties": Struct({...}), - # "required": ListValue([...]) - # } - - # Full JSON Schema approach (already complete) - >>> create_tool_function( - ... name="calculate", - ... description="Perform calculations", - ... parameters={ - ... "type": "object", - ... "properties": { - ... "expression": {"type": "string", "description": "Math expression"} - ... }, - ... "required": ["expression"] - ... } - ... ) - # Maps schema fields directly to protobuf map - - # No parameters - >>> create_tool_function( - ... name="get_time", - ... description="Get current time" - ... ) - # Creates: parameters = {} - """ - from dapr.clients.grpc._request import ConversationToolsFunction - - if parameters is None: - # No parameters - simple case - return ConversationToolsFunction(name=name, description=description, parameters={}) - - # Build the complete JSON Schema object - if isinstance(parameters, dict): - # Check if it's already a complete JSON schema - if 'type' in parameters and parameters['type'] == 'object': - # Complete JSON schema provided - use as-is - json_schema = parameters.copy() - # Add required field if provided separately and not already present - if required and 'required' not in json_schema: - json_schema['required'] = required - elif 'properties' in parameters: - # Properties provided directly - wrap in JSON schema - json_schema = {'type': 'object', 'properties': parameters['properties']} - if required: - json_schema['required'] = required - elif 'required' in parameters: - json_schema['required'] = parameters['required'] - else: - # Assume it's a direct mapping of parameter names to schemas - json_schema = {'type': 'object', 'properties': parameters} - if required: - json_schema['required'] = required - else: - raise ValueError(f'Parameters must be a dictionary, got {type(parameters)}') - - # Map JSON schema fields directly to protobuf map entries - # The parameters map directly represents the JSON schema structure - converted_params = {} - if json_schema: - for key, value in json_schema.items(): - converted_params[key] = convert_value_to_struct(value) - - return ConversationToolsFunction( - name=name, description=description, parameters=converted_params - ) - - -def create_tool( - name: str, - description: str, - parameters: Optional[Dict[str, Any]] = None, - required: Optional[List[str]] = None, -) -> 'ConversationTools': - """Create a complete tool with automatic parameter conversion. - - Args: - name: Function name - description: Human-readable description of what the function does - parameters: JSON schema for function parameters (raw Python dict) - required: List of required parameter names - - Returns: - ConversationTools ready to use with converse_alpha2() - - Examples: - # Weather tool - >>> weather_tool = create_tool( - ... name="get_weather", - ... description="Get current weather for a location", - ... parameters={ - ... "location": { - ... "type": "string", - ... "description": "The city and state or country" - ... }, - ... "unit": { - ... "type": "string", - ... "enum": ["celsius", "fahrenheit"], - ... "description": "Temperature unit" - ... } - ... }, - ... required=["location"] - ... ) - - # Calculator tool with full schema - >>> calc_tool = create_tool( - ... name="calculate", - ... description="Perform mathematical calculations", - ... parameters={ - ... "type": "object", - ... "properties": { - ... "expression": { - ... "type": "string", - ... "description": "Mathematical expression to evaluate" - ... } - ... }, - ... "required": ["expression"] - ... } - ... ) - - # Simple tool with no parameters - >>> time_tool = create_tool( - ... name="get_current_time", - ... description="Get the current date and time" - ... ) - """ - from dapr.clients.grpc._request import ConversationTools - - function = create_tool_function(name, description, parameters, required) - - return ConversationTools(function=function) diff --git a/pyproject.toml b/pyproject.toml index d7a62aea..2b8ddf72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,7 @@ target-version = "py38" line-length = 100 fix = true -extend-exclude = [ - ".github", - "dapr/proto", - "tools/dapr/proto", - "**/*_pb2.py", - "**/*_pb2_grpc.py", - "**/*_pb2.pyi" -] +extend-exclude = [".github", "dapr/proto"] [tool.ruff.lint] select = [ "E", # pycodestyle errors From b548c0016501a21fc150e176398c2b0f4fd61aa6 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:08:30 -0500 Subject: [PATCH 06/29] feedback, updates Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- README.md | 2 + dapr/__init__.py | 0 dapr/aio/clients/grpc/client.py | 89 +-- dapr/clients/grpc/_conversation_helpers.py | 224 +++--- dapr/clients/grpc/_helpers.py | 4 +- dapr/clients/grpc/_request.py | 130 +--- dapr/clients/grpc/_response.py | 45 +- dapr/clients/grpc/client.py | 86 +-- dapr/clients/grpc/conversation.py | 398 ++++++++++ .../en/python-sdk-docs/python-client.md | 2 +- examples/conversation/README.md | 103 ++- examples/conversation/config/anthropic.yaml | 12 - examples/conversation/config/deepseek.yaml | 12 - examples/conversation/config/google.yaml | 12 - examples/conversation/config/mistral.yaml | 12 - examples/conversation/config/openai.yaml | 12 - examples/conversation/conversation_alpha1.py | 5 +- examples/conversation/conversation_alpha2.py | 9 +- .../real_llm_providers_example.py | 715 +++++++++++------- tests/clients/test_conversation.py | 232 +++--- tests/clients/test_dapr_grpc_client.py | 116 ++- tests/clients/test_dapr_grpc_client_async.py | 3 +- tests/clients/test_grpc_helpers.py | 88 +-- tox.ini | 19 + 24 files changed, 1331 insertions(+), 999 deletions(-) create mode 100644 dapr/__init__.py create mode 100644 dapr/clients/grpc/conversation.py delete mode 100644 examples/conversation/config/anthropic.yaml delete mode 100644 examples/conversation/config/deepseek.yaml delete mode 100644 examples/conversation/config/google.yaml delete mode 100644 examples/conversation/config/mistral.yaml delete mode 100644 examples/conversation/config/openai.yaml diff --git a/README.md b/README.md index 73fae5b9..8ed06b88 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ tox -e type 8. Run examples +The tests in examples are using Mechanical Markdown (MM) from https://github.com/dapr/mechanical-markdown. + ```bash tox -e examples ``` diff --git a/dapr/__init__.py b/dapr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 65ce1ef4..e133f182 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -42,6 +42,7 @@ from dapr.aio.clients.grpc.subscription import Subscription from dapr.clients.exceptions import DaprInternalError, DaprGrpcError +from dapr.clients.grpc._conversation_helpers import _generate_unique_tool_call_id from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus @@ -78,13 +79,9 @@ InvokeMethodRequest, BindingRequest, TransactionalStateOperation, - ConversationInput, - ConversationInputAlpha2, - ConversationMessage, - ConversationMessageContent, - ConversationTools, - ConversationToolCalls, ) +from dapr.clients.grpc import conversation + from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( BindingResponse, @@ -92,8 +89,8 @@ ConversationResult, ConversationResponseAlpha2, ConversationResultAlpha2, - ConversationResultChoices, - ConversationResultMessage, + ConversationResultAlpha2Choices, + ConversationResultAlpha2Message, DaprResponse, GetSecretResponse, GetBulkSecretResponse, @@ -1733,7 +1730,7 @@ async def purge_workflow(self, instance_id: str, workflow_component: str) -> Dap async def converse_alpha1( self, name: str, - inputs: List[ConversationInput], + inputs: List[conversation.ConversationInput], *, context_id: Optional[str] = None, parameters: Optional[Dict[str, GrpcAny]] = None, @@ -1792,14 +1789,14 @@ async def converse_alpha1( async def converse_alpha2( self, name: str, - inputs: List[ConversationInputAlpha2], + inputs: List[conversation.ConversationInputAlpha2], *, context_id: Optional[str] = None, parameters: Optional[Dict[str, GrpcAny]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, - tools: Optional[List[ConversationTools]] = None, + tools: Optional[List[conversation.ConversationTools]] = None, tool_choice: Optional[str] = None, ) -> ConversationResponseAlpha2: """Invoke an LLM using the conversation API (Alpha2) with tool calling support. @@ -1822,66 +1819,6 @@ async def converse_alpha2( DaprGrpcError: If the Dapr runtime returns an error """ - def _convert_message_content(content_list: List[ConversationMessageContent]): - """Convert message content list to proto format.""" - if not content_list: - return [] - return [ - api_v1.ConversationMessageContent(text=content.text) for content in content_list - ] - - def _convert_tool_calls(tool_calls: List[ConversationToolCalls]): - """Convert tool calls to proto format.""" - if not tool_calls: - return [] - proto_calls = [] - for call in tool_calls: - proto_call = api_v1.ConversationToolCalls() - if call.id: - proto_call.id = call.id - if call.function: - proto_call.function.name = call.function.name - proto_call.function.arguments = call.function.arguments - proto_calls.append(proto_call) - return proto_calls - - def _convert_message(message: ConversationMessage): - """Convert a conversation message to proto format.""" - proto_message = api_v1.ConversationMessage() - - if message.of_developer: - proto_message.of_developer.name = message.of_developer.name or '' - proto_message.of_developer.content.extend( - _convert_message_content(message.of_developer.content or []) - ) - elif message.of_system: - proto_message.of_system.name = message.of_system.name or '' - proto_message.of_system.content.extend( - _convert_message_content(message.of_system.content or []) - ) - elif message.of_user: - proto_message.of_user.name = message.of_user.name or '' - proto_message.of_user.content.extend( - _convert_message_content(message.of_user.content or []) - ) - elif message.of_assistant: - proto_message.of_assistant.name = message.of_assistant.name or '' - proto_message.of_assistant.content.extend( - _convert_message_content(message.of_assistant.content or []) - ) - proto_message.of_assistant.tool_calls.extend( - _convert_tool_calls(message.of_assistant.tool_calls or []) - ) - elif message.of_tool: - if message.of_tool.tool_id: - proto_message.of_tool.tool_id = message.of_tool.tool_id - proto_message.of_tool.name = message.of_tool.name - proto_message.of_tool.content.extend( - _convert_message_content(message.of_tool.content or []) - ) - - return proto_message - # Convert inputs to proto format inputs_pb = [] for inp in inputs: @@ -1890,7 +1827,7 @@ def _convert_message(message: ConversationMessage): proto_input.scrub_pii = inp.scrub_pii for message in inp.messages: - proto_input.messages.append(_convert_message(message)) + proto_input.messages.append(message.to_proto()) inputs_pb.append(proto_input) @@ -1937,11 +1874,15 @@ def _convert_message(message: ConversationMessage): for output in response.outputs: choices = [] for choice in output.choices: + # workaround for some issues with missing tool ID in some providers (ie: Gemini) + for i, tool_call in enumerate(choice.message.tool_calls): + if not tool_call.id: + choice.message.tool_calls[i].id = _generate_unique_tool_call_id() choices.append( - ConversationResultChoices( + ConversationResultAlpha2Choices( finish_reason=choice.finish_reason, index=choice.index, - message=ConversationResultMessage( + message=ConversationResultAlpha2Message( content=choice.message.content, tool_calls=choice.message.tool_calls ), ) diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index 75ba4647..3e858ebb 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -12,25 +12,40 @@ See the License for the specific language governing permissions and limitations under the License. """ +import random +import string +from dapr.clients.grpc.conversation import Params import inspect from dataclasses import fields, is_dataclass from enum import Enum -from typing import Any, Dict, List, Optional, Union, get_args, get_origin, get_type_hints, TYPE_CHECKING - -if TYPE_CHECKING: - from dapr.clients.grpc._request import ConversationTools +from typing import ( + Any, + Dict, + List, + Optional, + Union, + get_args, + get_origin, + get_type_hints, + Callable, + Mapping, + Sequence, +) """ -Schema Helpers for Dapr Conversation API. +Tool Calling Helpers for Dapr Conversation API. This module provides function-to-JSON-schema helpers that automatically convert typed Python functions to tools for the Conversation API. + +These makes it easy to create tools for the Conversation API without +having to manually define the JSON schema for each tool. """ -def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[str, Any]: +def _python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[str, Any]: """Convert a Python type hint to JSON schema format. Args: @@ -41,11 +56,11 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s Dict representing the JSON schema for this type Examples: - >>> python_type_to_json_schema(str) + >>> _python_type_to_json_schema(str) {"type": "string"} - >>> python_type_to_json_schema(Optional[int]) + >>> _python_type_to_json_schema(Optional[int]) {"type": "integer"} - >>> python_type_to_json_schema(List[str]) + >>> _python_type_to_json_schema(List[str]) {"type": "array", "items": {"type": "string"}} """ # Handle None type @@ -62,17 +77,17 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s non_none_args = [arg for arg in args if arg is not type(None)] if len(non_none_args) == 1 and type(None) in args: # This is Optional[T], convert T - return python_type_to_json_schema(non_none_args[0], field_name) + return _python_type_to_json_schema(non_none_args[0], field_name) else: # This is a true Union, use anyOf - return {'anyOf': [python_type_to_json_schema(arg, field_name) for arg in args]} + return {'anyOf': [_python_type_to_json_schema(arg, field_name) for arg in args]} # Handle List types if origin is list or python_type is list: if args: return { 'type': 'array', - 'items': python_type_to_json_schema(args[0], f'{field_name}[]'), + 'items': _python_type_to_json_schema(args[0], f'{field_name}[]'), } else: return {'type': 'array'} @@ -84,7 +99,7 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s # Dict[str, ValueType] - add additionalProperties key_type, value_type = args if key_type is str: - schema['additionalProperties'] = python_type_to_json_schema( + schema['additionalProperties'] = _python_type_to_json_schema( value_type, f'{field_name}.*' ) return schema @@ -124,7 +139,7 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s dataclass_schema: Dict[str, Any] = {'type': 'object', 'properties': {}, 'required': []} for field in fields(python_type): - field_schema = python_type_to_json_schema(field.type, field.name) + field_schema = _python_type_to_json_schema(field.type, field.name) dataclass_schema['properties'][field.name] = field_schema # Check if field has no default (required) - use MISSING for dataclasses @@ -137,7 +152,7 @@ def python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[s return {'type': 'string', 'description': f'Unknown type: {python_type}'} -def extract_docstring_args(func) -> Dict[str, str]: +def _extract_docstring_args(func) -> Dict[str, str]: """Extract parameter descriptions from function docstring. Supports Google-style, NumPy-style, and Sphinx-style docstrings. @@ -209,8 +224,8 @@ def _has_potential_param_info(lines: List[str]) -> bool: # Look for informal patterns like "takes param1 which is", "param2 is an integer" has_informal_param_descriptions = bool( - re.search(r'takes\s+\w+\s+which\s+(is|are)', text) or - re.search(r'\w+\s+(is|are)\s+(a|an)\s+\w+\s+(input|argument)', text) + re.search(r'takes\s+\w+\s+which\s+(is|are)', text) + or re.search(r'\w+\s+(is|are)\s+(a|an)\s+\w+\s+(input|argument)', text) ) # Look for multiple parameter mentions suggesting documentation attempt @@ -231,9 +246,11 @@ def _has_potential_param_info(lines: List[str]) -> bool: has_excluded_phrases = any(phrase in text for phrase in exclude_phrases) return ( - (has_param_descriptions or has_param_mentions or has_informal_param_descriptions or has_multiple_param_mentions) - and not has_excluded_phrases - ) + has_param_descriptions + or has_param_mentions + or has_informal_param_descriptions + or has_multiple_param_mentions + ) and not has_excluded_phrases def _extract_sphinx_params(lines: List[str]) -> Dict[str, str]: @@ -268,11 +285,15 @@ def _extract_sphinx_params(lines: List[str]) -> Dict[str, str]: last_param = list(param_descriptions.keys())[-1] # Don't treat section headers or other directive-like content as continuations # Also don't treat content that looks like parameter definitions from other styles - if (param_descriptions[last_param] - and not any(line.startswith(prefix) for prefix in [':param', ':type', ':return', ':raises']) - and not line.lower().endswith(':') + if ( + param_descriptions[last_param] + and not any( + line.startswith(prefix) for prefix in [':param', ':type', ':return', ':raises'] + ) + and not line.lower().endswith(':') and not line.lower() in ('args', 'arguments', 'parameters', 'params') - and ':' not in line): # Avoid treating "param1: description" as continuation + and ':' not in line + ): # Avoid treating "param1: description" as continuation param_descriptions[last_param] += ' ' + line.strip() return param_descriptions @@ -303,7 +324,18 @@ def _extract_google_numpy_params(lines: List[str]) -> Dict[str, str]: next_line_idx = i + 1 if next_line_idx < len(lines): next_line = lines[next_line_idx].strip().lower() - if next_line in ('returns', 'return', 'yields', 'yield', 'raises', 'raise', 'notes', 'note', 'examples', 'example'): + if next_line in ( + 'returns', + 'return', + 'yields', + 'yield', + 'raises', + 'raise', + 'notes', + 'note', + 'examples', + 'example', + ): in_args_section = False continue @@ -311,9 +343,20 @@ def _extract_google_numpy_params(lines: List[str]) -> Dict[str, str]: if in_args_section and (line.endswith(':') and not line.startswith(' ')): in_args_section = False continue - + # Also exit on direct section headers without separators - if in_args_section and line.lower() in ('returns', 'return', 'yields', 'yield', 'raises', 'raise', 'notes', 'note', 'examples', 'example'): + if in_args_section and line.lower() in ( + 'returns', + 'return', + 'yields', + 'yield', + 'raises', + 'raise', + 'notes', + 'note', + 'examples', + 'example', + ): in_args_section = False continue @@ -324,7 +367,7 @@ def _extract_google_numpy_params(lines: List[str]) -> Dict[str, str]: if len(parts) == 2: param_name = parts[0].strip() description = parts[1].strip() - + # Handle type annotations like "param_name (type): description" if '(' in param_name and ')' in param_name: param_name = param_name.split('(')[0].strip() @@ -346,9 +389,11 @@ def _extract_google_numpy_params(lines: List[str]) -> Dict[str, str]: else: param_descriptions[param_name] = '' current_param = param_name - elif current_param and ( - original_line.startswith(' ') or original_line.startswith('\t') - ) and in_args_section: + elif ( + current_param + and (original_line.startswith(' ') or original_line.startswith('\t')) + and in_args_section + ): # Indented continuation line for current parameter (only if still in args section) if not param_descriptions[current_param]: # First description line for this parameter (for cases where description is on next line) @@ -455,7 +500,7 @@ def function_to_json_schema( type_hints = get_type_hints(func) # Extract parameter descriptions from docstring - param_descriptions = extract_docstring_args(func) + param_descriptions = _extract_docstring_args(func) # Get function description if description is None: @@ -478,7 +523,7 @@ def function_to_json_schema( param_type = type_hints.get(param_name, str) # Convert to JSON schema - param_schema = python_type_to_json_schema(param_type, param_name) + param_schema = _python_type_to_json_schema(param_type, param_name) # Add description if available if param_name in param_descriptions: @@ -493,82 +538,69 @@ def function_to_json_schema( return schema -def create_tool_from_function( - func, name: Optional[str] = None, description: Optional[str] = None -) -> 'ConversationTools': - """Create a ConversationTools from a Python function with type hints. +def _generate_unique_tool_call_id(): + """Generate a unique ID for a tool call. Mainly used if the LLM provider is not able to generate one itself.""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=9)) - This provides the ultimate developer experience - just define a typed function - and automatically get a properly configured tool for the Conversation API. - Args: - func: Python function with type hints - name: Override function name (defaults to func.__name__) - description: Override description (defaults to docstring) +# --- Tool Function Executor Backend - Returns: - ConversationTools ready to use with Alpha2 API +# --- Errors ---- - Examples: - >>> def get_weather(location: str, unit: str = "fahrenheit") -> str: - ... '''Get current weather for a location. - ... - ... Args: - ... location: The city and state or country - ... unit: Temperature unit preference - ... ''' - ... return f"Weather in {location}: sunny, 72°F" - >>> weather_tool = create_tool_from_function(get_weather) - # Now use weather_tool in your conversation API calls! +class ToolError(RuntimeError): + ... - >>> # With Pydantic models - >>> from pydantic import BaseModel - >>> class SearchQuery(BaseModel): - ... query: str - ... limit: int = 10 - ... include_images: bool = False - >>> def web_search(params: SearchQuery) -> str: - ... '''Search the web for information.''' - ... return f"Search results for: {params.query}" +class ToolNotFoundError(ToolError): + ... - >>> search_tool = create_tool_from_function(web_search) - >>> # With Enums - >>> from enum import Enum - >>> class Units(Enum): - ... CELSIUS = "celsius" - ... FAHRENHEIT = "fahrenheit" +class ToolExecutionError(ToolError): + ... - >>> def get_temperature(city: str, unit: Units = Units.FAHRENHEIT) -> float: - ... '''Get temperature for a city.''' - ... return 72.0 - >>> temp_tool = create_tool_from_function(get_temperature) - """ - # Import here to avoid circular imports - from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools +class ToolArgumentError(ToolError): + ... - # Generate JSON schema from function - json_schema = function_to_json_schema(func, name, description) - # Use provided name or function name - tool_name = name or func.__name__ +class ToolExecutionForbiddenError(ToolError): + """Exception raised when automatic execution of tools is forbidden per _ALLOW_REGISTER_TOOL_EXECUTION.""" - # Use provided description or extract from function - tool_description = description - if tool_description is None: - docstring = inspect.getdoc(func) - if docstring: - tool_description = docstring.split('\n')[0].strip() - else: - tool_description = f'Function {tool_name}' - # Create the tool function - function = ConversationToolsFunction( - name=tool_name, description=tool_description, parameters=json_schema - ) +def bind_params_to_func(fn: Callable[..., Any], params: Params): + sig = inspect.signature(fn) + if params is None: + bound = sig.bind() + bound.apply_defaults() + return bound + + if isinstance(params, Mapping): + bound = sig.bind_partial(**params) + # missing required parameters + missing = [ + p.name + for p in sig.parameters.values() + if p.default is inspect._empty + and p.kind + in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ) + and p.name not in bound.arguments + ] + if missing: + raise ToolArgumentError(f"Missing required parameter(s): {', '.join(missing)}") + # unexpected kwargs unless **kwargs present + if not any(p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()): + extra = set(params) - set(sig.parameters) + if extra: + raise ToolArgumentError(f"Unexpected parameter(s): {', '.join(sorted(extra))}") + elif isinstance(params, Sequence): + bound = sig.bind(*params) + else: + raise ToolArgumentError('params must be a mapping (kwargs), sequence (positional), or None') - # Return the complete tool - return ConversationTools(function=function) + bound.apply_defaults() + return bound diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index 103e1394..c68b0f56 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -13,10 +13,8 @@ limitations under the License. """ from enum import Enum -from typing import Any, Dict, List, Optional, Union, Tuple, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Union, Tuple -if TYPE_CHECKING: - from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage diff --git a/dapr/clients/grpc/_request.py b/dapr/clients/grpc/_request.py index 5e13070a..0ac1ef2f 100644 --- a/dapr/clients/grpc/_request.py +++ b/dapr/clients/grpc/_request.py @@ -14,9 +14,8 @@ """ import io -from dataclasses import dataclass, field from enum import Enum -from typing import Callable, Dict, List, Optional, Union +from typing import Dict, Optional, Union from google.protobuf.any_pb2 import Any as GrpcAny from google.protobuf.message import Message as GrpcMessage @@ -31,7 +30,6 @@ tuple_to_dict, unpack, ) -from dapr.clients.grpc._schema_helpers import function_to_json_schema, extract_docstring_summary from dapr.proto import api_v1, common_v1 @@ -429,132 +427,6 @@ def __next__(self): return request_proto -@dataclass -class ConversationInput: - """A single input message for the conversation.""" - - content: str - role: Optional[str] = None - scrub_pii: Optional[bool] = None - - -@dataclass -class ConversationMessageContent: - """Content for conversation messages.""" - - text: str - - -@dataclass -class ConversationMessageOfDeveloper: - """Developer message content.""" - - name: Optional[str] = None - content: List[ConversationMessageContent] = field(default_factory=list) - - -@dataclass -class ConversationMessageOfSystem: - """System message content.""" - - name: Optional[str] = None - content: List[ConversationMessageContent] = field(default_factory=list) - - -@dataclass -class ConversationMessageOfUser: - """User message content.""" - - name: Optional[str] = None - content: List[ConversationMessageContent] = field(default_factory=list) - - -@dataclass -class ConversationToolCallsOfFunction: - """Function call details within a tool call.""" - - name: str - arguments: str - - -@dataclass -class ConversationToolCalls: - """Tool calls generated by the model.""" - - id: Optional[str] = None - function: Optional[ConversationToolCallsOfFunction] = None - - -@dataclass -class ConversationMessageOfAssistant: - """Assistant message content.""" - - name: Optional[str] = None - content: List[ConversationMessageContent] = field(default_factory=list) - tool_calls: List[ConversationToolCalls] = field(default_factory=list) - - -@dataclass -class ConversationMessageOfTool: - """Tool message content.""" - - tool_id: Optional[str] = None - name: str = '' - content: List[ConversationMessageContent] = field(default_factory=list) - - -@dataclass -class ConversationMessage: - """Conversation message with different role types.""" - - of_developer: Optional[ConversationMessageOfDeveloper] = None - of_system: Optional[ConversationMessageOfSystem] = None - of_user: Optional[ConversationMessageOfUser] = None - of_assistant: Optional[ConversationMessageOfAssistant] = None - of_tool: Optional[ConversationMessageOfTool] = None - - -@dataclass -class ConversationInputAlpha2: - """Alpha2 input message for conversation API.""" - - messages: List[ConversationMessage] - scrub_pii: Optional[bool] = None - - -@dataclass -class ConversationToolsFunction: - """Function definition for conversation tools.""" - - name: str - description: Optional[str] = None - parameters: Optional[Dict] = None - - def schema_as_dict(self) -> Dict: - """Return the function's schema as a dictionary. - - Returns: - Dict: The JSON schema for the function parameters. - """ - return self.parameters or {} - - @classmethod - def from_function(cls, func: Callable) -> 'ConversationToolsFunction': - """Create a ConversationToolsFunction from a function.""" - return cls( - name=func.__name__, - description=extract_docstring_summary(func), - parameters=function_to_json_schema(func), - ) - - -@dataclass -class ConversationTools: - """Tools available for conversation.""" - - function: Optional[ConversationToolsFunction] = None - - class JobEvent: """Represents a job event received from Dapr runtime. diff --git a/dapr/clients/grpc/_response.py b/dapr/clients/grpc/_response.py index 9c885933..73a4d692 100644 --- a/dapr/clients/grpc/_response.py +++ b/dapr/clients/grpc/_response.py @@ -51,12 +51,13 @@ WorkflowRuntimeStatus, ) from dapr.proto import api_service_v1, api_v1, appcallback_v1, common_v1 +from dapr.clients.grpc import conversation # Avoid circular import dependency by only importing DaprGrpcClient # for type checking if TYPE_CHECKING: from dapr.clients.grpc.client import DaprGrpcClient - from dapr.clients.grpc._request import ConversationToolCalls + TCryptoResponse = TypeVar( 'TCryptoResponse', bound=Union[api_v1.EncryptResponse, api_v1.DecryptResponse] @@ -1083,27 +1084,27 @@ class ConversationResult: @dataclass -class ConversationResultMessage: +class ConversationResultAlpha2Message: """Message content in conversation result.""" content: str - tool_calls: List['ConversationToolCalls'] = field(default_factory=list) + tool_calls: List[conversation.ConversationToolCalls] = field(default_factory=list) @dataclass -class ConversationResultChoices: +class ConversationResultAlpha2Choices: """Choice in Alpha2 conversation result.""" finish_reason: str index: int - message: ConversationResultMessage + message: ConversationResultAlpha2Message @dataclass class ConversationResultAlpha2: """Alpha2 result from conversation input.""" - choices: List[ConversationResultChoices] = field(default_factory=list) + choices: List[ConversationResultAlpha2Choices] = field(default_factory=list) @dataclass @@ -1120,3 +1121,35 @@ class ConversationResponseAlpha2: context_id: Optional[str] outputs: List[ConversationResultAlpha2] + + def to_assistant_messages(self) -> List[conversation.ConversationMessage]: + def convert_llm_response_to_conversation_input( + result_message: ConversationResultAlpha2Message, + ) -> conversation.ConversationMessage: + """Convert ConversationResultMessage (from LLM response) to ConversationMessage.""" + + # Convert content string to ConversationMessageContent list + content = [] + if result_message.content: + content = [conversation.ConversationMessageContent(text=result_message.content)] + + # Convert tool_calls if present (they're already the right type) + tool_calls = result_message.tool_calls or [] + + # Create assistant message (since LLM responses are always assistant messages) + return conversation.ConversationMessage( + of_assistant=conversation.ConversationMessageOfAssistant( + content=content, tool_calls=tool_calls + ) + ) + + """Convert the outputs to a list of ConversationInput.""" + assistant_messages = [] + + for output_idx, output in enumerate(self.outputs or []): + for choice_idx, choice in enumerate(output.choices or []): + # Convert and collect all assistant messages + assistant_message = convert_llm_response_to_conversation_input(choice.message) + assistant_messages.append(assistant_message) + + return assistant_messages diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index c740a33e..4f2cb0f8 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -23,6 +23,7 @@ from warnings import warn from typing import Callable, Dict, Optional, Text, Union, Sequence, List, Any + from typing_extensions import Self from datetime import datetime from google.protobuf.message import Message as GrpcMessage @@ -39,6 +40,7 @@ ) from dapr.clients.exceptions import DaprInternalError, DaprGrpcError +from dapr.clients.grpc._conversation_helpers import _generate_unique_tool_call_id from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc.subscription import Subscription, StreamInactiveError @@ -67,13 +69,13 @@ TransactionalStateOperation, EncryptRequestIterator, DecryptRequestIterator, +) +from dapr.clients.grpc.conversation import ( ConversationInput, + ConversationToolCallsOfFunction, + ConversationToolCalls, ConversationInputAlpha2, - ConversationMessage, - ConversationMessageContent, ConversationTools, - ConversationToolCalls, - ConversationToolCallsOfFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( @@ -103,8 +105,8 @@ ConversationResult, ConversationResponseAlpha2, ConversationResultAlpha2, - ConversationResultChoices, - ConversationResultMessage, + ConversationResultAlpha2Choices, + ConversationResultAlpha2Message, ) @@ -1826,66 +1828,6 @@ def converse_alpha2( DaprGrpcError: If the Dapr runtime returns an error """ - def _convert_message_content(content_list: List[ConversationMessageContent]): - """Convert message content list to proto format.""" - if not content_list: - return [] - return [ - api_v1.ConversationMessageContent(text=content.text) for content in content_list - ] - - def _convert_tool_calls(tool_calls: List[ConversationToolCalls]): - """Convert tool calls to proto format.""" - if not tool_calls: - return [] - proto_calls = [] - for call in tool_calls: - proto_call = api_v1.ConversationToolCalls() - if call.id: - proto_call.id = call.id - if call.function: - proto_call.function.name = call.function.name - proto_call.function.arguments = call.function.arguments - proto_calls.append(proto_call) - return proto_calls - - def _convert_message(message: ConversationMessage): - """Convert a conversation message to proto format.""" - proto_message = api_v1.ConversationMessage() - - if message.of_developer: - proto_message.of_developer.name = message.of_developer.name or '' - proto_message.of_developer.content.extend( - _convert_message_content(message.of_developer.content or []) - ) - elif message.of_system: - proto_message.of_system.name = message.of_system.name or '' - proto_message.of_system.content.extend( - _convert_message_content(message.of_system.content or []) - ) - elif message.of_user: - proto_message.of_user.name = message.of_user.name or '' - proto_message.of_user.content.extend( - _convert_message_content(message.of_user.content or []) - ) - elif message.of_assistant: - proto_message.of_assistant.name = message.of_assistant.name or '' - proto_message.of_assistant.content.extend( - _convert_message_content(message.of_assistant.content or []) - ) - proto_message.of_assistant.tool_calls.extend( - _convert_tool_calls(message.of_assistant.tool_calls or []) - ) - elif message.of_tool: - if message.of_tool.tool_id: - proto_message.of_tool.tool_id = message.of_tool.tool_id - proto_message.of_tool.name = message.of_tool.name - proto_message.of_tool.content.extend( - _convert_message_content(message.of_tool.content or []) - ) - - return proto_message - # Convert inputs to proto format inputs_pb = [] for inp in inputs: @@ -1894,7 +1836,7 @@ def _convert_message(message: ConversationMessage): proto_input.scrub_pii = inp.scrub_pii for message in inp.messages: - proto_input.messages.append(_convert_message(message)) + proto_input.messages.append(message.to_proto()) inputs_pb.append(proto_input) @@ -1949,18 +1891,18 @@ def _convert_message(message: ConversationMessage): function_call = ConversationToolCallsOfFunction( name=tool_call.function.name, arguments=tool_call.function.arguments ) + if not tool_call.id: + tool_call.id = _generate_unique_tool_call_id() tool_calls.append( - ConversationToolCalls( - id=tool_call.id if tool_call.id else None, function=function_call - ) + ConversationToolCalls(id=tool_call.id, function=function_call) ) - result_message = ConversationResultMessage( + result_message = ConversationResultAlpha2Message( content=choice.message.content, tool_calls=tool_calls ) choices.append( - ConversationResultChoices( + ConversationResultAlpha2Choices( finish_reason=choice.finish_reason, index=choice.index, message=result_message, diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py new file mode 100644 index 00000000..dce134de --- /dev/null +++ b/dapr/clients/grpc/conversation.py @@ -0,0 +1,398 @@ +from dapr.clients.grpc import _conversation_helpers as conv_helpers +from dapr.proto import api_v1 + +import asyncio +import inspect +import json +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union, Protocol + +Params = Union[Mapping[str, Any], Sequence[Any], None] + + +@dataclass +class ConversationInput: + """A single input message for the conversation.""" + + content: str + role: Optional[str] = None + scrub_pii: Optional[bool] = None + + +@dataclass +class ConversationMessageContent: + """Content for conversation messages.""" + + text: str + + +@dataclass +class ConversationMessageOfDeveloper: + """Developer message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = field(default_factory=list) + + +@dataclass +class ConversationMessageOfSystem: + """System message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = field(default_factory=list) + + +@dataclass +class ConversationMessageOfUser: + """User message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = field(default_factory=list) + + +@dataclass +class ConversationToolCallsOfFunction: + """Function call details within a tool call.""" + + name: str + arguments: str + + +@dataclass +class ConversationToolCalls: + """Tool calls generated by the model.""" + + id: Optional[str] = None + function: Optional[ConversationToolCallsOfFunction] = None + + +@dataclass +class ConversationMessageOfAssistant: + """Assistant message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = field(default_factory=list) + tool_calls: List[ConversationToolCalls] = field(default_factory=list) + + +@dataclass +class ConversationMessageOfTool: + """Tool message content.""" + + tool_id: Optional[str] = None + name: str = '' + content: List[ConversationMessageContent] = field(default_factory=list) + + +@dataclass +class ConversationMessage: + """Conversation message with different role types.""" + + of_developer: Optional[ConversationMessageOfDeveloper] = None + of_system: Optional[ConversationMessageOfSystem] = None + of_user: Optional[ConversationMessageOfUser] = None + of_assistant: Optional[ConversationMessageOfAssistant] = None + of_tool: Optional[ConversationMessageOfTool] = None + + def to_proto(self) -> api_v1.ConversationMessage: + """Convert a conversation message to proto format.""" + + def _convert_message_content_to_proto( + content_list: List[ConversationMessageContent], + ): + """Convert message content list to proto format.""" + if not content_list: + return [] + return [ + api_v1.ConversationMessageContent(text=content.text) for content in content_list + ] + + def _convert_tool_calls_to_proto(tool_calls: List[ConversationToolCalls]): + """Convert tool calls to proto format.""" + if not tool_calls: + return [] + proto_calls = [] + for call in tool_calls: + proto_call = api_v1.ConversationToolCalls() + if call.id: + proto_call.id = call.id + if call.function: + proto_call.function.name = call.function.name + proto_call.function.arguments = call.function.arguments + proto_calls.append(proto_call) + return proto_calls + + proto_message = api_v1.ConversationMessage() + + if self.of_developer: + proto_message.of_developer.name = self.of_developer.name or '' + proto_message.of_developer.content.extend( + _convert_message_content_to_proto(self.of_developer.content or []) + ) + elif self.of_system: + proto_message.of_system.name = self.of_system.name or '' + proto_message.of_system.content.extend( + _convert_message_content_to_proto(self.of_system.content or []) + ) + elif self.of_user: + proto_message.of_user.name = self.of_user.name or '' + proto_message.of_user.content.extend( + _convert_message_content_to_proto(self.of_user.content or []) + ) + elif self.of_assistant: + proto_message.of_assistant.name = self.of_assistant.name or '' + proto_message.of_assistant.content.extend( + _convert_message_content_to_proto(self.of_assistant.content or []) + ) + proto_message.of_assistant.tool_calls.extend( + _convert_tool_calls_to_proto(self.of_assistant.tool_calls or []) + ) + elif self.of_tool: + if self.of_tool.tool_id: + proto_message.of_tool.tool_id = self.of_tool.tool_id + proto_message.of_tool.name = self.of_tool.name + proto_message.of_tool.content.extend( + _convert_message_content_to_proto(self.of_tool.content or []) + ) + + return proto_message + + +@dataclass +class ConversationInputAlpha2: + """Alpha2 input message for conversation API.""" + + messages: List[ConversationMessage] + scrub_pii: Optional[bool] = None + + +@dataclass +class ConversationToolsFunction: + """Function definition for conversation tools.""" + + name: str + description: Optional[str] = None + parameters: Optional[Dict] = None + + def schema_as_dict(self) -> Dict: + """Return the function's schema as a dictionary. + + Returns: + Dict: The JSON schema for the function parameters. + """ + return self.parameters or {} + + @classmethod + def from_function(cls, func: Callable, register: bool = True) -> 'ConversationToolsFunction': + """Create a ConversationToolsFunction from a function. + + Args: + func: The function to extract the schema from. + register: Whether to register the function in the tool registry. + """ + c = cls( + name=func.__name__, + description=conv_helpers.extract_docstring_summary(func), + parameters=conv_helpers.function_to_json_schema(func), + ) + if register: + register_tool(c.name, ConversationTools(function=c, backend=FunctionBackend(func))) + return c + +# --- Tool Helpers + + +class ToolBackend(Protocol): + """Interface for executors that knows how to execute a tool call.""" + + def invoke(self, spec: ConversationToolsFunction, params: Params) -> Any: + ... + + async def ainvoke( + self, spec: ConversationToolsFunction, params: Params, *, timeout: Union[float , None] = None + ) -> Any: + ... + + +@dataclass +class FunctionBackend: + """A backend that executes a local function.""" + + func: Callable[..., Any] = field(repr=False) + + def invoke(self, spec: ConversationToolsFunction, params: Params) -> Any: + bound = conv_helpers.bind_params_to_func(self.func, params) + if inspect.iscoroutinefunction(self.func): + raise conv_helpers.ToolExecutionError( + "This tool is async; use 'await tool.ainvoke(...)'." + ) + try: + return self.func(*bound.args, **bound.kwargs) + except Exception as e: + raise conv_helpers.ToolExecutionError(f'Tool raised: {e}') from e + + async def ainvoke( + self, spec: ConversationToolsFunction, params: Params, *, timeout: Union[float , None] = None + ) -> Any: + bound = conv_helpers.bind_params_to_func(self.func, params) + try: + if inspect.iscoroutinefunction(self.func): + coro = self.func(*bound.args, **bound.kwargs) + return await (asyncio.wait_for(coro, timeout) if timeout else coro) + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, lambda: self.func(*bound.args, **bound.kwargs)) + except asyncio.TimeoutError: + raise conv_helpers.ToolExecutionError(f'Timed out after {timeout} seconds') + except Exception as e: + raise conv_helpers.ToolExecutionError(f'Tool raised: {e}') from e + + +def tool( + func: Callable, + *, + name: Optional[str] = None, + description: Optional[str] = None, + namespace: Optional[str] = None, + register: bool = True, +): + """ + Decorate a callable as a conversation tool. + """ + + def _decorate(f: Callable): + ctf = ConversationToolsFunction.from_function(f, register=False) + + # Prefix name with namespace/module if not provided explicitly + ns = namespace or '' + if ns: + ns += '.' + ctf.name = name or f'{ns}{ctf.name}' + + if description: + ctf.description = description + + ct = ConversationTools(function=ctf, backend=FunctionBackend(f)) + + setattr(f, '__dapr_conversation_tool__', ct) + + if register: + register_tool(ctf.name, ct) + + return f + + return _decorate if func is None else _decorate(func) + + +@dataclass +class ConversationTools: + """Tools available for conversation.""" + + # currently only function is supported + function: ConversationToolsFunction + backend: Optional[ToolBackend] = None + + def invoke(self, params: Params = None) -> Any: + if not self.backend: + raise conv_helpers.ToolExecutionError('Tool backend not set') + return self.backend.invoke(self.function, params) + + async def ainvoke(self, params: Params = None, *, timeout: Union[float , None] = None) -> Any: + if not self.backend: + raise conv_helpers.ToolExecutionError('Tool backend not set') + return await self.backend.ainvoke(self.function, params, timeout=timeout) + + +# registry of tools +_TOOL_REGISTRY: Dict[str, ConversationTools] = {} + + +# whether to allow tool execution. It is set to false to express that you should validate the input of the LLM to +# avoid injection attacks. +_ALLOW_REGISTER_TOOL_EXECUTION = False + + +def register_tool(name: str, t: ConversationTools): + if name in _TOOL_REGISTRY: + raise ValueError(f"Tool '{name}' already registered") + _TOOL_REGISTRY[name] = t + + +def unregister_tool(name: str): + """Unregister a tool. Good for cleanup and avoid collisions.""" + if name in _TOOL_REGISTRY: + del _TOOL_REGISTRY[name] + + +def get_registered_tools() -> List[ConversationTools]: + """Get a list of all registered tools. This can be pass as tools in the ConversationInput.""" + return list(_TOOL_REGISTRY.values()) + + +def set_allow_register_tool_execution(allow: bool): + """Set whether tool execution is allowed.""" + global _ALLOW_REGISTER_TOOL_EXECUTION + _ALLOW_REGISTER_TOOL_EXECUTION = allow + + +def _get_tool(name: str) -> ConversationTools: + try: + return _TOOL_REGISTRY[name] + except KeyError: + raise conv_helpers.ToolNotFoundError(f"Tool '{name}' is not registered") + + +def execute_registered_tool(name: str, params: Union[Params , str] = None) -> Any: + """Execute a registered tool.""" + if not _ALLOW_REGISTER_TOOL_EXECUTION: + raise conv_helpers.ToolExecutionForbiddenError( + 'Automatic tool execution is forbidden. To enable it, set it via set_allow_register_tool_execution' + ) + if isinstance(params, str): + params = json.loads(params) + return _get_tool(name).invoke(params) + + +async def execute_registered_tool_async( + name: str, params: Union[Params , str] = None, *, timeout: Union[float , None] = None +) -> Any: + """Execute a registered tool asynchronously.""" + if not _ALLOW_REGISTER_TOOL_EXECUTION: + raise conv_helpers.ToolExecutionForbiddenError( + 'Automatic tool execution is forbidden. To enable it, set it via set_allow_register_tool_execution' + ) + if isinstance(params, str): + params = json.loads(params) + return await _get_tool(name).ainvoke(params, timeout=timeout) + + +# --- Helpers to create messages for Alpha2 inputs + + +def create_user_message(text: str) -> ConversationMessage: + """Helper to create a user message for Alpha2.""" + return ConversationMessage( + of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) + ) + + +def create_system_message(text: str) -> ConversationMessage: + """Helper to create a system message for Alpha2.""" + return ConversationMessage( + of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) + ) + + +def create_assistant_message(text: str) -> ConversationMessage: + """Helper to create an assistant message for Alpha2.""" + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant(content=[ConversationMessageContent(text=text)]) + ) + + +def create_tool_message(tool_id: str, name: str, content: str) -> ConversationMessage: + """Helper to create a tool message for Alpha2 responses (from client to LLM).""" + return ConversationMessage( + of_tool=ConversationMessageOfTool( + tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] + ) + ) diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index b2689971..b2137a1b 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -441,7 +441,7 @@ Since version 1.15 Dapr offers developers the capability to securely and reliabl ```python from dapr.clients import DaprClient -from dapr.clients.grpc._request import ConversationInput +from dapr.clients.grpc._conversation import ConversationInput with DaprClient() as d: inputs = [ diff --git a/examples/conversation/README.md b/examples/conversation/README.md index 3be9fe24..bbe4e96d 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -32,8 +32,47 @@ The Conversation API supports real LLM providers including: DEEPSEEK_API_KEY=your_deepseek_key_here GOOGLE_API_KEY=your_google_ai_key_here ``` + +4. **Run the simple conversation on the Alpha V1 version (dapr 1.15)** + + + ```bash + dapr run --app-id conversation-alpha1 \ + --log-level debug \ + --resources-path ./config \ + -- python3 conversation_alpha1.py + ``` + + + +5. **Run the simple conversation on the Alpha V2 version (dapr 1.16)** + + + ```bash + dapr run --app-id conversation-alpha2 \ + --log-level debug \ + --resources-path ./config \ + -- python3 conversation_alpha2.py + ``` + + + +6. **Run the comprehensive example with real LLM providers (This requires API Keys)** -4. **Run the comprehensive example:** ```bash python examples/conversation/real_llm_providers_example.py ``` @@ -180,12 +219,14 @@ We provide a helper function to automatically generate the tool schema from a Py ```python from typing import Optional, List from enum import Enum -from dapr.clients.grpc._request import ConversationToolsFunction, ConversationTools +from dapr.clients.grpc._conversation import ConversationToolsFunction, ConversationTools + class Units(Enum): CELSIUS = "celsius" FAHRENHEIT = "fahrenheit" + def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: '''Get current weather for a location. @@ -195,6 +236,7 @@ def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: ''' return f"Weather in {location}" + # Use the from_function class method for automatic schema generation function = ConversationToolsFunction.from_function(get_weather) weather_tool = ConversationTools(function=function) @@ -276,16 +318,16 @@ search_tool = ConversationTools(function=function) Alpha2 supports sophisticated message structures for complex conversations: ### User Messages + ```python -from dapr.clients.grpc._request import ( - ConversationMessage, - ConversationMessageOfUser, - ConversationMessageOfSystem, - ConversationMessageOfDeveloper, - ConversationMessageOfAssistant, - ConversationMessageOfTool, - ConversationMessageContent -) + +from dapr.clients.grpc._conversation import ConversationMessageContent, ConversationMessageOfDeveloper, + +ConversationMessage +ConversationMessageOfTool +ConversationMessageOfAssistant +ConversationMessageOfUser +ConversationMessageOfSystem user_message = ConversationMessage( of_user=ConversationMessageOfUser( @@ -339,7 +381,8 @@ tool_message = ConversationMessage( Alpha2 excels at multi-turn conversations with proper context accumulation: ```python -from dapr.clients.grpc._request import ConversationInputAlpha2 + +from dapr.clients.grpc._conversation import ConversationInputAlpha2 # Build conversation history conversation_history = [] @@ -438,16 +481,18 @@ dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resour Convert LLM responses for multi-turn conversations: ```python -from dapr.clients.grpc._response import ConversationResultMessage +from dapr.clients.grpc._response import ConversationResultAlpha2Message + -def convert_llm_response_to_conversation_message(result_message: ConversationResultMessage) -> ConversationMessage: +def convert_llm_response_to_conversation_message( + result_message: ConversationResultAlpha2Message) -> ConversationMessage: """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input).""" content = [] if result_message.content: content = [ConversationMessageContent(text=result_message.content)] - + tool_calls = result_message.tool_calls or [] - + return ConversationMessage( of_assistant=ConversationMessageOfAssistant( content=content, @@ -455,6 +500,7 @@ def convert_llm_response_to_conversation_message(result_message: ConversationRes ) ) + # Usage in multi-turn conversations response = client.converse_alpha2(name="openai", inputs=[input_alpha2], tools=[tool]) choice = response.outputs[0].choices[0] @@ -491,13 +537,12 @@ conversation_history.append(assistant_message) ```python from dapr.clients import DaprClient from dapr.clients.grpc._request import ( - ConversationInputAlpha2, - ConversationMessage, - ConversationMessageOfUser, - ConversationMessageContent, - ConversationToolsFunction, ConversationTools ) +from dapr.clients.grpc._conversation import ConversationMessageContent, ConversationMessageOfUser, ConversationMessage, + +ConversationToolsFunction +ConversationInputAlpha2 # Create a tool using the simple approach function = ConversationToolsFunction( @@ -536,14 +581,14 @@ with DaprClient() as client: name="openai", # or "anthropic", "mistral", etc. inputs=[input_alpha2], parameters={ - 'temperature': 0.7, # Auto-converted to GrpcAny! - 'max_tokens': 500, # Auto-converted to GrpcAny! - 'stream': False # Streaming not supported in Alpha2 + 'temperature': 0.7, # Auto-converted to GrpcAny! + 'max_tokens': 500, # Auto-converted to GrpcAny! + 'stream': False # Streaming not supported in Alpha2 }, tools=[weather_tool], tool_choice='auto' ) - + # Process the response if response.outputs and response.outputs[0].choices: choice = response.outputs[0].choices[0] @@ -634,8 +679,10 @@ BUILD_LOCAL_DAPR=true Alpha2 provides significant improvements while maintaining backward compatibility: ### Alpha1 (Legacy) + ```python -from dapr.clients.grpc._request import ConversationInput + +from dapr.clients.grpc._conversation import ConversationInput inputs = [ConversationInput( content="Hello!", @@ -650,8 +697,10 @@ response = client.converse_alpha1( ``` ### Alpha2 (Recommended) + ```python -from dapr.clients.grpc._request import ConversationInputAlpha2, ConversationMessage + +from dapr.clients.grpc._conversation import ConversationMessage, ConversationInputAlpha2 user_message = ConversationMessage( of_user=ConversationMessageOfUser( diff --git a/examples/conversation/config/anthropic.yaml b/examples/conversation/config/anthropic.yaml deleted file mode 100644 index a098425a..00000000 --- a/examples/conversation/config/anthropic.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: anthropic -spec: - type: conversation.anthropic - version: v1 - metadata: - - name: key - value: ${ANTHROPIC_API_KEY} - - name: model - value: claude-sonnet-4-20250514 diff --git a/examples/conversation/config/deepseek.yaml b/examples/conversation/config/deepseek.yaml deleted file mode 100644 index ab1d465d..00000000 --- a/examples/conversation/config/deepseek.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: deepseek -spec: - type: conversation.deepseek - version: v1 - metadata: - - name: key - value: ${DEEPSEEK_API_KEY} - - name: model - value: deepseek-chat diff --git a/examples/conversation/config/google.yaml b/examples/conversation/config/google.yaml deleted file mode 100644 index 670f40a8..00000000 --- a/examples/conversation/config/google.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: google -spec: - type: conversation.googleai - version: v1 - metadata: - - name: key - value: ${GOOGLE_API_KEY} - - name: model - value: gemini-2.5-pro diff --git a/examples/conversation/config/mistral.yaml b/examples/conversation/config/mistral.yaml deleted file mode 100644 index 0e037ce6..00000000 --- a/examples/conversation/config/mistral.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: mistral -spec: - type: conversation.mistral - version: v1 - metadata: - - name: key - value: ${MISTRAL_API_KEY} - - name: model - value: mistral-large-latest diff --git a/examples/conversation/config/openai.yaml b/examples/conversation/config/openai.yaml deleted file mode 100644 index edca7c99..00000000 --- a/examples/conversation/config/openai.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: openai -spec: - type: conversation.openai - version: v1 - metadata: - - name: key - value: ${OPENAI_API_KEY} - - name: model - value: gpt-4o-mini-2024-07-18 diff --git a/examples/conversation/conversation_alpha1.py b/examples/conversation/conversation_alpha1.py index 6b39e37c..33bfbee4 100644 --- a/examples/conversation/conversation_alpha1.py +++ b/examples/conversation/conversation_alpha1.py @@ -11,7 +11,7 @@ # limitations under the License. # ------------------------------------------------------------ from dapr.clients import DaprClient -from dapr.clients.grpc._request import ConversationInput +from dapr.clients.grpc._conversation import ConversationInput with DaprClient() as d: inputs = [ @@ -29,5 +29,6 @@ name='echo', inputs=inputs, temperature=0.7, context_id='chat-123', metadata=metadata ) + print('Result: ', end='') for output in response.outputs: - print(f'Result: {output.result}') + print(output.result) diff --git a/examples/conversation/conversation_alpha2.py b/examples/conversation/conversation_alpha2.py index dae72b11..dd52b901 100644 --- a/examples/conversation/conversation_alpha2.py +++ b/examples/conversation/conversation_alpha2.py @@ -11,11 +11,11 @@ # limitations under the License. # ------------------------------------------------------------ from dapr.clients import DaprClient -from dapr.clients.grpc._request import ( - ConversationInputAlpha2, - ConversationMessage, +from dapr.clients.grpc._conversation import ( ConversationMessageContent, ConversationMessageOfUser, + ConversationMessage, + ConversationInputAlpha2, ) with DaprClient() as d: @@ -50,5 +50,6 @@ name='echo', inputs=inputs, temperature=0.7, context_id='chat-123', metadata=metadata ) + print('Result: ', end='') for output in response.outputs: - print(f'Result: {output.choices[0].message.content}') + print(output.choices[0].message.content) diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index f3753023..d2d0048d 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -38,12 +38,16 @@ import os import sys import tempfile +from faulthandler import unregister from pathlib import Path from typing import Any, Dict, List, Optional import yaml +from dapr.clients.grpc._response import ConversationResultAlpha2Message + # Add the parent directory to the path so we can import local dapr sdk +# uncomment if running from development version sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # Load environment variables from .env file if available @@ -57,27 +61,186 @@ from dapr.aio.clients import DaprClient as AsyncDaprClient from dapr.clients import DaprClient -from dapr.clients.grpc._request import ( - ConversationInput, - ConversationInputAlpha2, - ConversationMessage, - ConversationMessageContent, - ConversationMessageOfUser, - ConversationMessageOfSystem, - ConversationMessageOfAssistant, - ConversationMessageOfTool, - ConversationToolCalls, - ConversationToolCallsOfFunction, - ConversationTools, - ConversationToolsFunction, -) -from google.protobuf.wrappers_pb2 import DoubleValue, Int32Value, BoolValue, StringValue -from dapr.clients.grpc._response import ConversationResultMessage - - -def convert_llm_response_to_conversation_message( - result_message: ConversationResultMessage, -) -> ConversationMessage: +from dapr.clients.grpc import conversation + + +def create_weather_tool() -> conversation.ConversationTools: + """Create a weather tool for testing Alpha2 tool calling using full JSON schema in parameters approach.""" + conversation.unregister_tool('get_weather') + function = conversation.ConversationToolsFunction( + name='get_weather', + description='Get the current weather for a location', + parameters={ + 'type': 'object', + 'properties': { + 'location': {'type': 'string', 'description': 'The city and state or country'}, + 'unit': { + 'type': 'string', + 'enum': ['celsius', 'fahrenheit'], + 'description': 'Temperature unit', + }, + }, + 'required': ['location'], + }, + ) + return conversation.ConversationTools(function=function) + + +def create_calculator_tool() -> conversation.ConversationTools: + """Create a calculator tool using full JSON schema in parameters approach.""" + conversation.unregister_tool('calculate') # cleanup + function = conversation.ConversationToolsFunction( + name='calculate', + description='Perform mathematical calculations', + parameters={ + 'type': 'object', + 'properties': { + 'expression': { + 'type': 'string', + 'description': "Mathematical expression to evaluate (e.g., '2+2', 'sqrt(16)')", + } + }, + 'required': ['expression'], + }, + ) + return conversation.ConversationTools(function=function) + + +def create_time_tool() -> conversation.ConversationTools: + """Create a simple tool with no parameters using full JSON schema in parameters approach.""" + conversation.unregister_tool('get_current_time') + function = conversation.ConversationToolsFunction( + name='get_current_time', + description='Get the current date and time', + parameters={'type': 'object', 'properties': {}, 'required': []}, + ) + return conversation.ConversationTools(function=function) + + +def create_search_tool() -> conversation.ConversationTools: + """Create a more complex tool with multiple parameter types and constraints using full JSON schema in parameters approach.""" + conversation.unregister_tool('web_search') + function = conversation.ConversationToolsFunction( + name='web_search', + description='Search the web for information', + parameters={ + 'type': 'object', + 'properties': { + 'query': {'type': 'string', 'description': 'Search query'}, + 'limit': { + 'type': 'integer', + 'description': 'Maximum number of results', + 'minimum': 1, + 'maximum': 10, + 'default': 5, + }, + 'include_images': { + 'type': 'boolean', + 'description': 'Whether to include image results', + 'default': False, + }, + 'domains': { + 'type': 'array', + 'items': {'type': 'string'}, + # 'description': 'Limit search to specific domains', + }, + }, + 'required': ['query'], + }, + ) + return conversation.ConversationTools(function=function) + + +def create_tool_from_typed_function_example() -> conversation.ConversationTools: + """Demonstrate creating tools from typed Python functions - Best DevEx for most cases. + + This shows the most advanced approach: define a typed function and automatically + generate the complete tool schema from type hints and docstrings. + """ + from typing import Optional, List + from enum import Enum + + conversation.unregister_tool('find_restaurants') + + # Define the tool behavior as a regular Python function with type hints + class PriceRange(Enum): + BUDGET = 'budget' + MODERATE = 'moderate' + EXPENSIVE = 'expensive' + + def find_restaurants( + location: str, + cuisine: str = 'any', + price_range: PriceRange = PriceRange.MODERATE, + max_results: int = 5, + dietary_restrictions: Optional[List[str]] = None, + ) -> str: + """Find restaurants in a specific location. + + Args: + location: The city or neighborhood to search + cuisine: Type of cuisine (italian, chinese, mexican, etc.) + price_range: Budget preference for dining + max_results: Maximum number of restaurant recommendations + dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) + """ + # This would contain actual implementation + return f'Found restaurants in {location} serving {cuisine} food' + + # Create the tool using the from_function class method + function = conversation.ConversationToolsFunction.from_function(find_restaurants) + + return conversation.ConversationTools(function=function) + + +def create_tool_from_tool_decorator_example() -> conversation.ConversationTools: + """Demonstrate creating tools from typed Python functions - Best DevEx for most cases. + + This shows the most advanced approach: define a typed function and automatically + generate the complete tool schema from type hints and docstrings. + """ + from typing import Optional, List + from enum import Enum + + conversation.unregister_tool('find_restaurants') + + # Define the tool behavior as a regular Python function with type hints + class PriceRange(Enum): + MODERATE = 'moderate' + EXPENSIVE = 'expensive' + + @conversation.tool + def find_restaurants( + location: str, + cuisine: str = 'any', + price_range: PriceRange = PriceRange.MODERATE, + max_results: int = 5, + dietary_restrictions: Optional[List[str]] = None, + ) -> str: + """Find restaurants in a specific location. + + Args: + location: The city or neighborhood to search + cuisine: Type of cuisine (italian, chinese, mexican, etc.) + price_range: Budget preference for dining + max_results: Maximum number of restaurant recommendations + dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) + """ + # This would contain actual implementation + return f'Found restaurants in {location} serving {cuisine} food' + + return conversation.ConversationTools(function=find_restaurants) + + +def execute_weather_tool(location: str, unit: str = 'fahrenheit') -> str: + """Simulate weather tool execution.""" + temp = '72°F' if unit == 'fahrenheit' else '22°C' + return f'The weather in {location} is sunny with a temperature of {temp}.' + + +def convert_llm_response_to_conversation_input( + result_message: ConversationResultAlpha2Message, +) -> conversation.ConversationMessage: """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input). This standalone utility function makes it easy to append LLM responses to conversation history @@ -94,7 +257,7 @@ def convert_llm_response_to_conversation_message( >>> choice = response.outputs[0].choices[0] >>> >>> # Convert LLM response to conversation message - >>> assistant_message = convert_llm_response_to_conversation_message(choice.message) + >>> assistant_message = convert_llm_response_to_conversation_input(choice.message) >>> conversation_history.append(assistant_message) >>> >>> # Use in next turn @@ -104,14 +267,16 @@ def convert_llm_response_to_conversation_message( # Convert content string to ConversationMessageContent list content = [] if result_message.content: - content = [ConversationMessageContent(text=result_message.content)] + content = [conversation.ConversationMessageContent(text=result_message.content)] # Convert tool_calls if present (they're already the right type) tool_calls = result_message.tool_calls or [] # Create assistant message (since LLM responses are always assistant messages) - return ConversationMessage( - of_assistant=ConversationMessageOfAssistant(content=content, tool_calls=tool_calls) + return conversation.ConversationMessage( + of_assistant=conversation.ConversationMessageOfAssistant( + content=content, tool_calls=tool_calls + ) ) @@ -143,12 +308,12 @@ def detect_available_providers(self) -> Dict[str, Dict[str, Any]]: # OpenAI if os.getenv('OPENAI_API_KEY'): providers['openai'] = { - 'display_name': 'OpenAI GPT-4o-mini', + 'display_name': 'OpenAI GPT-5-mini', 'component_type': 'conversation.openai', 'api_key_env': 'OPENAI_API_KEY', 'metadata': [ {'name': 'key', 'value': os.getenv('OPENAI_API_KEY')}, - {'name': 'model', 'value': 'gpt-4o-mini'}, + {'name': 'model', 'value': 'gpt-5-mini-2025-08-07'}, ], } @@ -240,189 +405,6 @@ def create_component_configs(self, selected_providers: Optional[List[str]] = Non return self.components_dir - def create_weather_tool(self) -> ConversationTools: - """Create a weather tool for testing Alpha2 tool calling using full JSON schema in parameters approach.""" - function = ConversationToolsFunction( - name='get_weather', - description='Get the current weather for a location', - parameters={ - 'type': 'object', - 'properties': { - 'location': {'type': 'string', 'description': 'The city and state or country'}, - 'unit': { - 'type': 'string', - 'enum': ['celsius', 'fahrenheit'], - 'description': 'Temperature unit', - }, - }, - 'required': ['location'], - }, - ) - return ConversationTools(function=function) - - def create_calculator_tool(self) -> ConversationTools: - """Create a calculator tool using full JSON schema in parameters approach.""" - function = ConversationToolsFunction( - name='calculate', - description='Perform mathematical calculations', - parameters={ - 'type': 'object', - 'properties': { - 'expression': { - 'type': 'string', - 'description': "Mathematical expression to evaluate (e.g., '2+2', 'sqrt(16)')", - } - }, - 'required': ['expression'], - }, - ) - return ConversationTools(function=function) - - def create_time_tool(self) -> ConversationTools: - """Create a simple tool with no parameters using full JSON schema in parameters approach.""" - function = ConversationToolsFunction( - name='get_current_time', - description='Get the current date and time', - parameters={'type': 'object', 'properties': {}, 'required': []}, - ) - return ConversationTools(function=function) - - def create_search_tool(self) -> ConversationTools: - """Create a more complex tool with multiple parameter types and constraints using full JSON schema in parameters approach.""" - function = ConversationToolsFunction( - name='web_search', - description='Search the web for information', - parameters={ - 'type': 'object', - 'properties': { - 'query': {'type': 'string', 'description': 'Search query'}, - 'limit': { - 'type': 'integer', - 'description': 'Maximum number of results', - 'minimum': 1, - 'maximum': 10, - 'default': 5, - }, - 'include_images': { - 'type': 'boolean', - 'description': 'Whether to include image results', - 'default': False, - }, - 'domains': { - 'type': 'array', - 'items': {'type': 'string'}, - 'description': 'Limit search to specific domains', - }, - }, - 'required': ['query'], - }, - ) - return ConversationTools(function=function) - - def create_tool_from_typed_function_example(self) -> ConversationTools: - """Demonstrate creating tools from typed Python functions - Best DevEx for most cases. - - This shows the most advanced approach: define a typed function and automatically - generate the complete tool schema from type hints and docstrings. - """ - from typing import Optional, List - from enum import Enum - - # Define the tool behavior as a regular Python function with type hints - class PriceRange(Enum): - BUDGET = 'budget' - MODERATE = 'moderate' - EXPENSIVE = 'expensive' - - def find_restaurants( - location: str, - cuisine: str = 'any', - price_range: PriceRange = PriceRange.MODERATE, - max_results: int = 5, - dietary_restrictions: Optional[List[str]] = None, - ) -> str: - """Find restaurants in a specific location. - - Args: - location: The city or neighborhood to search - cuisine: Type of cuisine (italian, chinese, mexican, etc.) - price_range: Budget preference for dining - max_results: Maximum number of restaurant recommendations - dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) - """ - # This would contain actual implementation - return f'Found restaurants in {location} serving {cuisine} food' - - # Create the tool using the from_function class method - function = ConversationToolsFunction.from_function(find_restaurants) - - return ConversationTools(function=function) - - def execute_weather_tool(self, location: str, unit: str = 'fahrenheit') -> str: - """Simulate weather tool execution.""" - temp = '72°F' if unit == 'fahrenheit' else '22°C' - return f'The weather in {location} is sunny with a temperature of {temp}.' - - def create_user_message(self, text: str) -> ConversationMessage: - """Helper to create a user message for Alpha2.""" - return ConversationMessage( - of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) - ) - - def create_system_message(self, text: str) -> ConversationMessage: - """Helper to create a system message for Alpha2.""" - return ConversationMessage( - of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) - ) - - def create_assistant_message(self, text: str) -> ConversationMessage: - """Helper to create an assistant message for Alpha2.""" - return ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - content=[ConversationMessageContent(text=text)] - ) - ) - - def create_tool_message(self, tool_id: str, name: str, content: str) -> ConversationMessage: - """Helper to create a tool message for Alpha2 responses (from client to LLM).""" - return ConversationMessage( - of_tool=ConversationMessageOfTool( - tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] - ) - ) - - def create_tool_call_message( - self, tool_id: str, name: str, arguments: str - ) -> ConversationMessage: - """Helper to create a tool call message for Alpha2 responses (from LLM to client).""" - return ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - tool_calls=[ - ConversationToolCalls( - id=tool_id, - function=ConversationToolCallsOfFunction(name=name, arguments=arguments), - ) - ] - ) - ) - - def convert_result_message_to_input_message( - self, result_message: ConversationResultMessage - ) -> ConversationMessage: - """Convert ConversationResultMessage to ConversationMessage for reuse in conversation history. - - This utility makes it easy to append LLM responses to conversation history - and use them as input for subsequent turns. - - Args: - result_message: ConversationResultMessage from LLM response - - Returns: - ConversationMessage suitable for input to next conversation turn - """ - # Delegate to standalone utility function - return convert_llm_response_to_conversation_message(result_message) - def test_basic_conversation_alpha2(self, provider_id: str) -> None: """Test basic Alpha2 conversation with a provider.""" print( @@ -432,10 +414,10 @@ def test_basic_conversation_alpha2(self, provider_id: str) -> None: try: with DaprClient() as client: # Create Alpha2 conversation input with sophisticated message structure - user_message = self.create_user_message( + user_message = conversation.create_user_message( "Hello! Please respond with exactly: 'Hello from Dapr Alpha2!'" ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) # Use new parameter conversion (raw Python values automatically converted) response = client.converse_alpha2( @@ -467,14 +449,14 @@ def test_multi_turn_conversation_alpha2(self, provider_id: str) -> None: try: with DaprClient() as client: # Create a multi-turn conversation with system, user, and assistant messages - system_message = self.create_system_message( + system_message = conversation.create_system_message( 'You are a helpful AI assistant. Be concise.' ) - user_message1 = self.create_user_message('What is 2+2?') - assistant_message = self.create_assistant_message('2+2 equals 4.') - user_message2 = self.create_user_message('What about 3+3?') + user_message1 = conversation.create_user_message('What is 2+2?') + assistant_message = conversation.create_assistant_message('2+2 equals 4.') + user_message2 = conversation.create_user_message('What about 3+3?') - input_alpha2 = ConversationInputAlpha2( + input_alpha2 = conversation.ConversationInputAlpha2( messages=[system_message, user_message1, assistant_message, user_message2] ) @@ -507,10 +489,12 @@ def test_tool_calling_alpha2(self, provider_id: str) -> None: try: with DaprClient() as client: - weather_tool = self.create_weather_tool() - user_message = self.create_user_message("What's the weather like in San Francisco?") + weather_tool = create_weather_tool() + user_message = conversation.create_user_message( + "What's the weather like in San Francisco?" + ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( name=provider_id, @@ -536,14 +520,14 @@ def test_tool_calling_alpha2(self, provider_id: str) -> None: # Execute the tool to show the workflow try: args = json.loads(tool_call.function.arguments) - weather_result = self.execute_weather_tool( + weather_result = execute_weather_tool( args.get('location', 'San Francisco'), args.get('unit', 'fahrenheit'), ) print(f'🌤️ Tool executed: {weather_result}') # Demonstrate tool result message (for multi-turn tool workflows) - tool_result_message = self.create_tool_message( + tool_result_message = conversation.create_tool_message( tool_id=tool_call.id, name=tool_call.function.name, content=weather_result, @@ -571,16 +555,16 @@ def test_parameter_conversion(self, provider_id: str) -> None: try: with DaprClient() as client: - user_message = self.create_user_message( + user_message = conversation.create_user_message( 'Tell me about the different tool creation approaches available.' ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) # Demonstrate different tool creation approaches - weather_tool = self.create_weather_tool() # Simple properties approach - calc_tool = self.create_calculator_tool() # Full JSON schema approach - time_tool = self.create_time_tool() # No parameters approach - search_tool = self.create_search_tool() # Complex schema with arrays, etc. + weather_tool = create_weather_tool() # Simple properties approach + calc_tool = create_calculator_tool() # Full JSON schema approach + time_tool = create_time_tool() # No parameters approach + search_tool = create_search_tool() # Complex schema with arrays, etc. print( f'✅ Created {len([weather_tool, calc_tool, time_tool, search_tool])} tools with different approaches!' @@ -590,6 +574,7 @@ def test_parameter_conversion(self, provider_id: str) -> None: response = client.converse_alpha2( name=provider_id, inputs=[input_alpha2], + tools=[weather_tool, calc_tool, time_tool, search_tool], parameters={ # Raw Python values - automatically converted to GrpcAny 'temperature': 0.8, # float @@ -622,18 +607,20 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: try: with DaprClient() as client: - weather_tool = self.create_weather_tool() + weather_tool = create_weather_tool() conversation_history = [] # Turn 1: User asks about weather (include tools) print('\n--- Turn 1: Initial weather query ---') - user_message1 = self.create_user_message( - "What's the weather like in San Francisco?" + user_message1 = conversation.create_user_message( + "What's the weather like in San Francisco? Use one of the tools available." ) conversation_history.append(user_message1) print(f'📝 Request 1 context: {len(conversation_history)} messages + tools') - input_alpha2_turn1 = ConversationInputAlpha2(messages=conversation_history) + input_alpha2_turn1 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) response1 = client.converse_alpha2( name=provider_id, @@ -657,13 +644,15 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: ) # Convert and collect all assistant messages - assistant_message = self.convert_result_message_to_input_message( + assistant_message = convert_llm_response_to_conversation_input( choice.message ) assistant_messages.append(assistant_message) # Check for tool calls in this choice if choice.message.tool_calls: + # if not choice.message.tool_calls[0].id: + # choice.message.tool_calls[0].id = "1" tool_calls_found.extend(choice.message.tool_calls) print( f'🔧 Found {len(choice.message.tool_calls)} tool call(s) in output {output_idx}, choice {choice_idx}' @@ -686,13 +675,13 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: # Execute the tool args = json.loads(tool_call.function.arguments) - weather_result = self.execute_weather_tool( + weather_result = execute_weather_tool( args.get('location', 'San Francisco'), args.get('unit', 'fahrenheit') ) print(f'🌤️ Tool result: {weather_result}') # Add tool result to conversation history - tool_result_message = self.create_tool_message( + tool_result_message = conversation.create_tool_message( tool_id=tool_call.id, name=tool_call.function.name, content=weather_result ) conversation_history.append(tool_result_message) @@ -700,7 +689,9 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: # Turn 2: LLM processes tool result (accumulate context + tools) print('\n--- Turn 2: LLM processes tool result ---') print(f'📝 Request 2 context: {len(conversation_history)} messages + tools') - input_alpha2_turn2 = ConversationInputAlpha2(messages=conversation_history) + input_alpha2_turn2 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) response2 = client.converse_alpha2( name=provider_id, @@ -717,14 +708,14 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: print(f'🤖 LLM response with tool context: {choice2.message.content}') # Add LLM's response to accumulated history using utility - assistant_message2 = self.convert_result_message_to_input_message( + assistant_message2 = convert_llm_response_to_conversation_input( choice2.message ) conversation_history.append(assistant_message2) # Turn 3: Follow-up question (full context + tools) print('\n--- Turn 3: Follow-up question using accumulated context ---') - user_message2 = self.create_user_message( + user_message2 = conversation.create_user_message( 'Should I bring an umbrella? Also, what about the weather in New York?' ) conversation_history.append(user_message2) @@ -737,7 +728,9 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: print(" • Assistant's weather summary") print(' • New user follow-up question') - input_alpha2_turn3 = ConversationInputAlpha2(messages=conversation_history) + input_alpha2_turn3 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) response3 = client.converse_alpha2( name=provider_id, @@ -760,8 +753,10 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: # Execute second tool call tool_call3 = choice3.message.tool_calls[0] + # if not tool_call3.id: + # tool_call3.id = "2" args3 = json.loads(tool_call3.function.arguments) - weather_result3 = self.execute_weather_tool( + weather_result3 = execute_weather_tool( args3.get('location', 'New York'), args3.get('unit', 'fahrenheit'), ) @@ -794,6 +789,155 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: except Exception as e: print(f'❌ Multi-turn tool calling error: {e}') + def test_multi_turn_tool_calling_alpha2_tool_helpers(self, provider_id: str) -> None: + """Test multi-turn Alpha2 tool calling with proper context accumulation using higher level abstractions.""" + print( + f"\n🔄🔧 Testing multi-turn tool calling with {self.available_providers[provider_id]['display_name']}" + ) + + # using decorator + + @conversation.tool + def get_weather(location: str, unit: str = 'fahrenheit') -> str: + """Get the current weather for a location.""" + # This is a mock implementation. Replace with actual weather API call. + temp = '72°F' if unit == 'fahrenheit' else '22°C' + return f'The weather in {location} is sunny with a temperature of {temp}.' + + try: + with DaprClient() as client: + conversation_history = [] + + # allow automatically execute tool calls without checking arguments + conversation.set_allow_register_tool_execution(True) + + # Turn 1: User asks about weather (include tools) + print('\n--- Turn 1: Initial weather query ---') + user_message1 = conversation.create_user_message( + "What's the weather like in San Francisco? Use one of the tools available." + ) + conversation_history.append(user_message1) + + print(f'📝 Request 1 context: {len(conversation_history)} messages + tools') + input_alpha2_turn1 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) + + response1 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn1], + tools=conversation.get_registered_tools(), # using registered tools (automatically registered by the decorator) + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + def append_response_to_history(response): + for msg in response.to_assistant_messages(): + conversation_history.append(msg) + if msg.of_assistant.tool_calls: + for _tool_call in msg.of_assistant.tool_calls: + print(f'Executing tool call: {_tool_call.function.name}') + output = conversation.execute_registered_tool( + _tool_call.function.name, _tool_call.function.arguments + ) + print(f'Tool output: {output}') + + # append result to history + conversation_history.append( + conversation.create_tool_message( + tool_id=_tool_call.id, + name=_tool_call.function.name, + content=output, + ) + ) + + append_response_to_history(response1) + + # Turn 2: LLM processes tool result (accumulate context + tools) + print('\n--- Turn 2: LLM processes tool result ---') + print(f'📝 Request 2 context: {len(conversation_history)} messages + tools') + input_alpha2_turn2 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) + + response2 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn2], + tools=conversation.get_registered_tools(), + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + # Turn 3: Follow-up question (full context + tools) + + append_response_to_history(response2) + + print('\n--- Turn 3: Follow-up question using accumulated context ---') + user_message2 = conversation.create_user_message( + 'Should I bring an umbrella? Also, what about the weather in New York?' + ) + conversation_history.append(user_message2) + + print(f'📝 Request 3 context: {len(conversation_history)} messages + tools') + print('📋 Accumulated context includes:') + + input_alpha2_turn3 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) + + response3 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn3], + tools=conversation.get_registered_tools(), + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + append_response_to_history(response3) + + print(f'📝 Request 4 context: {len(conversation_history)} messages + tools') + print('📋 Expect response about the umbrella:') + + input_alpha2_turn4 = conversation.ConversationInputAlpha2( + messages=conversation_history + ) + + response4 = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2_turn4], + tools=conversation.get_registered_tools(), + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + append_response_to_history(response4) + + # print full history + for i, msg in enumerate(conversation_history): + print(f'History Index {i}: {msg}') + + else: + print( + '⚠️ No tool calls found in any output/choice - continuing with regular conversation flow' + ) + # Could continue with regular multi-turn conversation without tools + + except Exception as e: + print(f'❌ Multi-turn tool calling error: {e}') + finally: + conversation.unregister_tool('get_weather') + def test_function_to_schema_approach(self, provider_id: str) -> None: """Test the best DevEx for most cases: function-to-JSON-schema automatic tool creation.""" print( @@ -803,12 +947,13 @@ def test_function_to_schema_approach(self, provider_id: str) -> None: try: with DaprClient() as client: # Create a tool using the typed function approach - restaurant_tool = self.create_tool_from_typed_function_example() + restaurant_tool = create_tool_from_typed_function_example() + print(restaurant_tool) - user_message = self.create_user_message( + user_message = conversation.create_user_message( 'I want to find Italian restaurants in San Francisco with a moderate price range.' ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( name=provider_id, @@ -841,6 +986,56 @@ def test_function_to_schema_approach(self, provider_id: str) -> None: except Exception as e: print(f'❌ Function-to-schema approach error: {e}') + def test_tool_decorated_function_to_schema_approach(self, provider_id: str) -> None: + """Test the best DevEx for most cases: function-to-JSON-schema automatic tool creation.""" + print( + f"\n🎯 Testing decorator tool function-to-schema approach with {self.available_providers[provider_id]['display_name']}" + ) + + try: + with DaprClient() as client: + # Create a tool using the typed function approach + create_tool_from_tool_decorator_example() + + # we can get tools registered from different places in our repo + print(conversation.get_registered_tools()) + + user_message = conversation.create_user_message( + 'I want to find Italian restaurants in San Francisco with a moderate price range.' + ) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) + + response = client.converse_alpha2( + name=provider_id, + inputs=[input_alpha2], + tools=conversation.get_registered_tools(), + tool_choice='auto', + parameters={ + 'temperature': 0.3, + 'max_tokens': 500, + }, + ) + + if response.outputs and response.outputs[0].choices: + choice = response.outputs[0].choices[0] + print(f'📊 Finish reason: {choice.finish_reason}') + + if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: + print('🎯 Function-to-schema tool calling successful!') + for tool_call in choice.message.tool_calls: + print(f' Tool: {tool_call.function.name}') + print(f' Arguments: {tool_call.function.arguments}') + + # This demonstrates the complete workflow + print('✅ Auto-generated schema worked perfectly with real LLM!') + else: + print(f'💬 Response: {choice.message.content}') + else: + print('❌ No function-to-schema response received') + + except Exception as e: + print(f'❌ Function-to-schema approach error: {e}') + async def test_async_conversation_alpha2(self, provider_id: str) -> None: """Test async Alpha2 conversation with a provider.""" print( @@ -849,10 +1044,10 @@ async def test_async_conversation_alpha2(self, provider_id: str) -> None: try: async with AsyncDaprClient() as client: - user_message = self.create_user_message( + user_message = conversation.create_user_message( 'Tell me a very short joke about async programming.' ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) response = await client.converse_alpha2( name=provider_id, @@ -880,10 +1075,10 @@ async def test_async_tool_calling_alpha2(self, provider_id: str) -> None: try: async with AsyncDaprClient() as client: - weather_tool = self.create_weather_tool() - user_message = self.create_user_message("What's the weather in Tokyo?") + weather_tool = create_weather_tool() + user_message = conversation.create_user_message("What's the weather in Tokyo?") - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) response = await client.converse_alpha2( name=provider_id, @@ -902,7 +1097,7 @@ async def test_async_tool_calling_alpha2(self, provider_id: str) -> None: for tool_call in choice.message.tool_calls: print(f' Tool: {tool_call.function.name}') args = json.loads(tool_call.function.arguments) - weather_result = self.execute_weather_tool( + weather_result = execute_weather_tool( args.get('location', 'Tokyo'), args.get('unit', 'fahrenheit') ) print(f' Result: {weather_result}') @@ -922,12 +1117,14 @@ def run_comprehensive_test(self, provider_id: str) -> None: print(f"{'='*60}") # Alpha2 Sync tests - self.test_basic_conversation_alpha2(provider_id) - self.test_multi_turn_conversation_alpha2(provider_id) - self.test_tool_calling_alpha2(provider_id) - self.test_parameter_conversion(provider_id) - self.test_function_to_schema_approach(provider_id) - self.test_multi_turn_tool_calling_alpha2(provider_id) + # self.test_basic_conversation_alpha2(provider_id) + # self.test_multi_turn_conversation_alpha2(provider_id) + # self.test_tool_calling_alpha2(provider_id) + # self.test_parameter_conversion(provider_id) + # self.test_function_to_schema_approach(provider_id) + # self.test_tool_decorated_function_to_schema_approach(provider_id) + # self.test_multi_turn_tool_calling_alpha2(provider_id) + self.test_multi_turn_tool_calling_alpha2_tool_helpers(provider_id) # Alpha2 Async tests asyncio.run(self.test_async_conversation_alpha2(provider_id)) @@ -945,7 +1142,7 @@ def test_basic_conversation_alpha1_legacy(self, provider_id: str) -> None: try: with DaprClient() as client: inputs = [ - ConversationInput( + conversation.ConversationInput( content="Hello! Please respond with: 'Hello from Dapr Alpha1!'", role='user' ) ] @@ -969,11 +1166,6 @@ def test_basic_conversation_alpha1_legacy(self, provider_id: str) -> None: print(f'❌ Alpha1 legacy conversation error: {e}') def cleanup(self) -> None: - """Clean up temporary component files and stop sidecar if needed.""" - # Stop sidecar if we started it - if self.sidecar_manager: - self.sidecar_manager.stop() - # Clean up temporary components directory if self.components_dir and Path(self.components_dir).exists(): import shutil @@ -1040,6 +1232,7 @@ def main(): print(' • Multi-turn conversations') print(' • Multi-turn tool calling with context expansion') print(' • Function-to-schema automatic tool generation') + print(' • Function-to-schema using @tool decorator for automatic tool generation') print(' • Both sync and async implementations') print(' • Backward compatibility with Alpha1') print(f"{'='*60}") diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index d73d67bc..6c522103 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -22,21 +22,98 @@ from dapr.clients import DaprClient from dapr.clients.exceptions import DaprGrpcError from dapr.conf import settings -from dapr.clients.grpc._request import ( +from dapr.clients.grpc.conversation import ( ConversationInput, - ConversationInputAlpha2, - ConversationMessage, ConversationMessageContent, - ConversationMessageOfUser, ConversationMessageOfSystem, + ConversationMessageOfUser, ConversationMessageOfAssistant, ConversationMessageOfTool, - ConversationTools, + ConversationMessage, + ConversationInputAlpha2, ConversationToolsFunction, + ConversationTools ) from tests.clients.fake_dapr_server import FakeDaprSidecar +def create_weather_tool(): + """Create a weather tool for testing.""" + return ConversationTools( + function=ConversationToolsFunction( + name='get_weather', + description='Get weather information for a location', + parameters={ + 'type': 'object', + 'properties': { + 'location': { + 'type': 'string', + 'description': 'The city and state, e.g. San Francisco, CA', + }, + 'unit': { + 'type': 'string', + 'enum': ['celsius', 'fahrenheit'], + 'description': 'Temperature unit', + }, + }, + 'required': ['location'], + }, + ) + ) + + +def create_calculate_tool(): + """Create a calculate tool for testing.""" + return ConversationTools( + function=ConversationToolsFunction( + name='calculate', + description='Perform mathematical calculations', + parameters={ + 'type': 'object', + 'properties': { + 'expression': { + 'type': 'string', + 'description': 'Mathematical expression to evaluate', + } + }, + 'required': ['expression'], + }, + ) + ) + + +def create_user_message(text): + """Helper to create a user message for Alpha2.""" + return ConversationMessage( + of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) + ) + + +def create_system_message(text): + """Helper to create a system message for Alpha2.""" + return ConversationMessage( + of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) + ) + + +def create_assistant_message(text, tool_calls=None): + """Helper to create an assistant message for Alpha2.""" + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=[ConversationMessageContent(text=text)], tool_calls=tool_calls or [] + ) + ) + + +def create_tool_message(tool_id, name, content): + """Helper to create a tool message for Alpha2.""" + return ConversationMessage( + of_tool=ConversationMessageOfTool( + tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] + ) + ) + + class ConversationTestBase: """Base class for conversation tests with common setup.""" @@ -56,77 +133,6 @@ def setUpClass(cls): def tearDownClass(cls): cls._fake_dapr_server.stop() - def create_weather_tool(self): - """Create a weather tool for testing.""" - return ConversationTools( - function=ConversationToolsFunction( - name='get_weather', - description='Get weather information for a location', - parameters={ - 'type': 'object', - 'properties': { - 'location': { - 'type': 'string', - 'description': 'The city and state, e.g. San Francisco, CA', - }, - 'unit': { - 'type': 'string', - 'enum': ['celsius', 'fahrenheit'], - 'description': 'Temperature unit', - }, - }, - 'required': ['location'], - }, - ) - ) - - def create_calculate_tool(self): - """Create a calculate tool for testing.""" - return ConversationTools( - function=ConversationToolsFunction( - name='calculate', - description='Perform mathematical calculations', - parameters={ - 'type': 'object', - 'properties': { - 'expression': { - 'type': 'string', - 'description': 'Mathematical expression to evaluate', - } - }, - 'required': ['expression'], - }, - ) - ) - - def create_user_message(self, text): - """Helper to create a user message for Alpha2.""" - return ConversationMessage( - of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) - ) - - def create_system_message(self, text): - """Helper to create a system message for Alpha2.""" - return ConversationMessage( - of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) - ) - - def create_assistant_message(self, text, tool_calls=None): - """Helper to create an assistant message for Alpha2.""" - return ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - content=[ConversationMessageContent(text=text)], tool_calls=tool_calls or [] - ) - ) - - def create_tool_message(self, tool_id, name, content): - """Helper to create a tool message for Alpha2.""" - return ConversationMessage( - of_tool=ConversationMessageOfTool( - tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] - ) - ) - class ConversationAlpha1SyncTests(ConversationTestBase, unittest.TestCase): """Synchronous Alpha1 conversation API tests.""" @@ -206,7 +212,7 @@ class ConversationAlpha2SyncTests(ConversationTestBase, unittest.TestCase): def test_basic_conversation_alpha2(self): """Test basic Alpha2 conversation functionality.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Hello Alpha2!') + user_message = create_user_message('Hello Alpha2!') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -222,8 +228,8 @@ def test_basic_conversation_alpha2(self): def test_conversation_alpha2_with_system_message(self): """Test Alpha2 conversation with system message.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - system_message = self.create_system_message('You are a helpful assistant.') - user_message = self.create_user_message('Hello!') + system_message = create_system_message('You are a helpful assistant.') + user_message = create_user_message('Hello!') input_alpha2 = ConversationInputAlpha2( messages=[system_message, user_message], scrub_pii=False @@ -245,7 +251,7 @@ def test_conversation_alpha2_with_system_message(self): def test_conversation_alpha2_with_options(self): """Test Alpha2 conversation with various options.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Alpha2 with options') + user_message = create_user_message('Alpha2 with options') input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=True) response = client.converse_alpha2( @@ -264,7 +270,7 @@ def test_conversation_alpha2_with_options(self): def test_alpha2_parameter_conversion(self): """Test Alpha2 parameter conversion with various types.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Parameter conversion test') + user_message = create_user_message('Parameter conversion test') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( @@ -290,7 +296,7 @@ def test_alpha2_error_handling(self): ) with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Error test') + user_message = create_user_message('Error test') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) with self.assertRaises(DaprGrpcError) as context: @@ -304,8 +310,8 @@ class ConversationToolCallingSyncTests(ConversationTestBase, unittest.TestCase): def test_tool_calling_weather(self): """Test tool calling with weather tool.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = self.create_weather_tool() - user_message = self.create_user_message('What is the weather in San Francisco?') + weather_tool = create_weather_tool() + user_message = create_user_message('What is the weather in San Francisco?') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) @@ -325,8 +331,8 @@ def test_tool_calling_weather(self): def test_tool_calling_calculate(self): """Test tool calling with calculate tool.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - calc_tool = self.create_calculate_tool() - user_message = self.create_user_message('Calculate 15 * 23') + calc_tool = create_calculate_tool() + user_message = create_user_message('Calculate 15 * 23') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) @@ -343,10 +349,10 @@ def test_tool_calling_calculate(self): def test_multiple_tools(self): """Test conversation with multiple tools.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = self.create_weather_tool() - calc_tool = self.create_calculate_tool() + weather_tool = create_weather_tool() + calc_tool = create_calculate_tool() - user_message = self.create_user_message('I need weather and calculation help') + user_message = create_user_message('I need weather and calculation help') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( @@ -364,8 +370,8 @@ def test_multiple_tools(self): def test_tool_choice_none(self): """Test tool choice set to 'none'.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = self.create_weather_tool() - user_message = self.create_user_message('What is the weather today?') + weather_tool = create_weather_tool() + user_message = create_user_message('What is the weather today?') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) @@ -382,10 +388,10 @@ def test_tool_choice_none(self): def test_tool_choice_specific(self): """Test tool choice set to specific tool name.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = self.create_weather_tool() - calc_tool = self.create_calculate_tool() + weather_tool = create_weather_tool() + calc_tool = create_calculate_tool() - user_message = self.create_user_message('What is the weather like?') + user_message = create_user_message('What is the weather like?') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( @@ -409,10 +415,10 @@ def test_multi_turn_conversation(self): """Test multi-turn conversation with different message types.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: # Create a conversation with system, user, and assistant messages - system_message = self.create_system_message('You are a helpful AI assistant.') - user_message1 = self.create_user_message('Hello, how are you?') - assistant_message = self.create_assistant_message('I am doing well, thank you!') - user_message2 = self.create_user_message('What can you help me with?') + system_message = create_system_message('You are a helpful AI assistant.') + user_message1 = create_user_message('Hello, how are you?') + assistant_message = create_assistant_message('I am doing well, thank you!') + user_message2 = create_user_message('What can you help me with?') input_alpha2 = ConversationInputAlpha2( messages=[system_message, user_message1, assistant_message, user_message2] @@ -434,8 +440,8 @@ def test_tool_calling_workflow(self): """Test complete tool calling workflow.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: # Step 1: User asks for weather - weather_tool = self.create_weather_tool() - user_message = self.create_user_message('What is the weather in Tokyo?') + weather_tool = create_weather_tool() + user_message = create_user_message('What is the weather in Tokyo?') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) @@ -450,7 +456,7 @@ def test_tool_calling_workflow(self): tool_call = choice.message.tool_calls[0] # Step 2: Send tool result back - tool_result_message = self.create_tool_message( + tool_result_message = create_tool_message( tool_id=tool_call.id, name='get_weather', content='{"temperature": 18, "condition": "cloudy", "humidity": 75}', @@ -471,7 +477,7 @@ def test_conversation_context_continuity(self): context_id = 'multi-turn-test-123' # First turn - user_message1 = self.create_user_message('My name is Alice.') + user_message1 = create_user_message('My name is Alice.') input1 = ConversationInputAlpha2(messages=[user_message1]) response1 = client.converse_alpha2( @@ -481,7 +487,7 @@ def test_conversation_context_continuity(self): self.assertEqual(response1.context_id, context_id) # Second turn with same context - user_message2 = self.create_user_message('What is my name?') + user_message2 = create_user_message('What is my name?') input2 = ConversationInputAlpha2(messages=[user_message2]) response2 = client.converse_alpha2( @@ -512,7 +518,7 @@ async def test_basic_async_conversation_alpha1(self): async def test_basic_async_conversation_alpha2(self): """Test basic async Alpha2 conversation.""" async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Hello async Alpha2!') + user_message = create_user_message('Hello async Alpha2!') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = await client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -524,8 +530,8 @@ async def test_basic_async_conversation_alpha2(self): async def test_async_tool_calling(self): """Test async tool calling.""" async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = self.create_weather_tool() - user_message = self.create_user_message('Async weather request for London') + weather_tool = create_weather_tool() + user_message = create_user_message('Async weather request for London') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) @@ -551,7 +557,7 @@ async def run_alpha1_conversation(message, session_id): return response.outputs[0].result async def run_alpha2_conversation(message, session_id): - user_message = self.create_user_message(message) + user_message = create_user_message(message) input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = await client.converse_alpha2( name='test-llm', inputs=[input_alpha2], context_id=session_id @@ -577,8 +583,8 @@ async def test_async_multi_turn_with_tools(self): """Test async multi-turn conversation with tool calling.""" async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: # First turn: user asks for weather - weather_tool = self.create_weather_tool() - user_message = self.create_user_message('Async weather for Paris') + weather_tool = create_weather_tool() + user_message = create_user_message('Async weather for Paris') input1 = ConversationInputAlpha2(messages=[user_message]) response1 = await client.converse_alpha2( @@ -593,7 +599,7 @@ async def test_async_multi_turn_with_tools(self): tool_call = response1.outputs[0].choices[0].message.tool_calls[0] # Second turn: provide tool result - tool_result_message = self.create_tool_message( + tool_result_message = create_tool_message( tool_id=tool_call.id, name='get_weather', content='{"temperature": 22, "condition": "sunny"}', @@ -627,7 +633,7 @@ class ConversationParameterTests(ConversationTestBase, unittest.TestCase): def test_parameter_edge_cases(self): """Test parameter conversion with edge cases.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Edge cases test') + user_message = create_user_message('Edge cases test') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = client.converse_alpha2( @@ -649,7 +655,7 @@ def test_parameter_edge_cases(self): def test_realistic_provider_parameters(self): """Test with realistic LLM provider parameters.""" with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = self.create_user_message('Provider parameters test') + user_message = create_user_message('Provider parameters test') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) # OpenAI-style parameters @@ -716,7 +722,7 @@ def test_mixed_alpha1_alpha2_compatibility(self): alpha1_response = client.converse_alpha1(name='test-llm', inputs=alpha1_inputs) # Alpha2 call - user_message = self.create_user_message('Alpha2 call') + user_message = create_user_message('Alpha2 call') alpha2_input = ConversationInputAlpha2(messages=[user_message]) alpha2_response = client.converse_alpha2(name='test-llm', inputs=[alpha2_input]) diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index 2d9c5c11..cc5e360c 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -36,17 +36,6 @@ from dapr.clients.grpc._request import ( TransactionalStateOperation, TransactionOperationType, - ConversationInput, - ConversationInputAlpha2, - ConversationMessage, - ConversationMessageContent, - ConversationMessageOfUser, - ConversationMessageOfAssistant, - ConversationMessageOfSystem, - ConversationMessageOfDeveloper, - ConversationMessageOfTool, - ConversationTools, - ConversationToolsFunction, ) from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem @@ -60,6 +49,7 @@ WorkflowRuntimeStatus, TopicEventResponse, ) +from dapr.clients.grpc import conversation class DaprGrpcClientTests(unittest.TestCase): @@ -1201,8 +1191,8 @@ def test_converse_alpha1_basic(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') inputs = [ - ConversationInput(content='Hello', role='user'), - ConversationInput(content='How are you?', role='user'), + conversation.ConversationInput(content='Hello', role='user'), + conversation.ConversationInput(content='How are you?', role='user'), ] response = dapr.converse_alpha1(name='test-llm', inputs=inputs) @@ -1216,7 +1206,7 @@ def test_converse_alpha1_basic(self): def test_converse_alpha1_with_options(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') - inputs = [ConversationInput(content='Hello', role='user', scrub_pii=True)] + inputs = [conversation.ConversationInput(content='Hello', role='user', scrub_pii=True)] response = dapr.converse_alpha1( name='test-llm', @@ -1239,7 +1229,7 @@ def test_converse_alpha1_error_handling(self): status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Invalid argument') ) - inputs = [ConversationInput(content='Hello', role='user')] + inputs = [conversation.ConversationInput(content='Hello', role='user')] with self.assertRaises(DaprGrpcError) as context: dapr.converse_alpha1(name='test-llm', inputs=inputs) @@ -1250,14 +1240,14 @@ def test_converse_alpha2_basic_user_message(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create user message - user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - name='TestUser', content=[ConversationMessageContent(text='Hello, how are you?')] + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + name='TestUser', content=[conversation.ConversationMessageContent(text='Hello, how are you?')] ) ) # Create Alpha2 input - input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=False) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message], scrub_pii=False) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1277,8 +1267,8 @@ def test_converse_alpha2_with_tools_weather_request(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create weather tool - weather_tool = ConversationTools( - function=ConversationToolsFunction( + weather_tool = conversation.ConversationTools( + function=conversation.ConversationToolsFunction( name='get_weather', description='Get current weather information', parameters={ @@ -1292,13 +1282,13 @@ def test_converse_alpha2_with_tools_weather_request(self): ) # Create user message asking for weather - user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="What's the weather like?")] + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text="What's the weather like?")] ) ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) response = dapr.converse_alpha2( name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto' @@ -1327,13 +1317,13 @@ def test_converse_alpha2_system_message(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create system message - system_message = ConversationMessage( - of_system=ConversationMessageOfSystem( - content=[ConversationMessageContent(text='You are a helpful assistant.')] + system_message = conversation.ConversationMessage( + of_system=conversation.ConversationMessageOfSystem( + content=[conversation.ConversationMessageContent(text='You are a helpful assistant.')] ) ) - input_alpha2 = ConversationInputAlpha2(messages=[system_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[system_message]) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1349,14 +1339,14 @@ def test_converse_alpha2_developer_message(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create developer message - developer_message = ConversationMessage( - of_developer=ConversationMessageOfDeveloper( + developer_message = conversation.ConversationMessage( + of_developer=conversation.ConversationMessageOfDeveloper( name='DevTeam', - content=[ConversationMessageContent(text='Debug: Processing user input')], + content=[conversation.ConversationMessageContent(text='Debug: Processing user input')], ) ) - input_alpha2 = ConversationInputAlpha2(messages=[developer_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[developer_message]) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1372,17 +1362,17 @@ def test_converse_alpha2_tool_message(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create tool message - tool_message = ConversationMessage( - of_tool=ConversationMessageOfTool( + tool_message = conversation.ConversationMessage( + of_tool=conversation.ConversationMessageOfTool( tool_id='call_123', name='get_weather', content=[ - ConversationMessageContent(text='{"temperature": 22, "condition": "sunny"}') + conversation.ConversationMessageContent(text='{"temperature": 22, "condition": "sunny"}') ], ) ) - input_alpha2 = ConversationInputAlpha2(messages=[tool_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[tool_message]) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1399,13 +1389,13 @@ def test_converse_alpha2_assistant_message(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create assistant message - assistant_message = ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - content=[ConversationMessageContent(text='I understand your request.')] + assistant_message = conversation.ConversationMessage( + of_assistant=conversation.ConversationMessageOfAssistant( + content=[conversation.ConversationMessageContent(text='I understand your request.')] ) ) - input_alpha2 = ConversationInputAlpha2(messages=[assistant_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[assistant_message]) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1419,17 +1409,17 @@ def test_converse_alpha2_multiple_messages(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create multiple messages - system_message = ConversationMessage( - of_system=ConversationMessageOfSystem( - content=[ConversationMessageContent(text='You are helpful.')] + system_message = conversation.ConversationMessage( + of_system=conversation.ConversationMessageOfSystem( + content=[conversation.ConversationMessageContent(text='You are helpful.')] ) ) - user_message = ConversationMessage( - of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text='Hello!')]) + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser(content=[conversation.ConversationMessageContent(text='Hello!')]) ) - input_alpha2 = ConversationInputAlpha2(messages=[system_message, user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[system_message, user_message]) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1450,13 +1440,13 @@ def test_converse_alpha2_with_context_and_options(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') - user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text='Continue our conversation')] + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text='Continue our conversation')] ) ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=True) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message], scrub_pii=True) # Create custom parameters params = {'custom_param': GrpcAny(value=b'{"setting": "value"}')} @@ -1487,13 +1477,13 @@ def test_converse_alpha2_error_handling(self): status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha2 Invalid argument') ) - user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text='Test error')] + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text='Test error')] ) ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) with self.assertRaises(DaprGrpcError) as context: dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1505,23 +1495,23 @@ def test_converse_alpha2_tool_choice_specific(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') # Create multiple tools - weather_tool = ConversationTools( - function=ConversationToolsFunction( + weather_tool = conversation.ConversationTools( + function=conversation.ConversationToolsFunction( name='get_weather', description='Get weather information' ) ) - calculator_tool = ConversationTools( - function=ConversationToolsFunction(name='calculate', description='Perform calculations') + calculator_tool = conversation.ConversationTools( + function=conversation.ConversationToolsFunction(name='calculate', description='Perform calculations') ) - user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="What's the weather today?")] + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text="What's the weather today?")] ) ) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) response = dapr.converse_alpha2( name='test-llm', diff --git a/tests/clients/test_dapr_grpc_client_async.py b/tests/clients/test_dapr_grpc_client_async.py index 1e3210b2..de8e0fb0 100644 --- a/tests/clients/test_dapr_grpc_client_async.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -29,7 +29,8 @@ from .fake_dapr_server import FakeDaprSidecar from dapr.conf import settings from dapr.clients.grpc._helpers import to_bytes -from dapr.clients.grpc._request import TransactionalStateOperation, ConversationInput +from dapr.clients.grpc._request import TransactionalStateOperation +from dapr.clients.grpc.conversation import ConversationInput from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions diff --git a/tests/clients/test_grpc_helpers.py b/tests/clients/test_grpc_helpers.py index fbde03ae..7520397d 100644 --- a/tests/clients/test_grpc_helpers.py +++ b/tests/clients/test_grpc_helpers.py @@ -24,12 +24,9 @@ python_type_to_json_schema, extract_docstring_args, function_to_json_schema, - create_tool_from_function, extract_docstring_summary, ) - from dapr.clients.grpc._request import ( - ConversationToolsFunction, - ) + from dapr.clients.grpc._conversation import ConversationToolsFunction HELPERS_AVAILABLE = True except ImportError as e: @@ -1176,89 +1173,6 @@ def google_function(data: str): self.assertEqual(result.description, expected_description) -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestCreateToolFromFunction(unittest.TestCase): - """Test the create_tool_from_function function.""" - - def test_create_tool_basic(self): - """Test creating a tool from a basic function.""" - - def get_weather(location: str, unit: str = 'celsius') -> str: - """Get weather information. - - Args: - location: City name - unit: Temperature unit - """ - return f'Weather in {location}' - - # Test that the function works without errors - tool = create_tool_from_function(get_weather) - - # Basic validation that we got a tool object - self.assertIsNotNone(tool) - self.assertIsNotNone(tool.function) - self.assertEqual(tool.function.name, 'get_weather') - self.assertEqual(tool.function.description, 'Get weather information.') - - def test_create_tool_with_overrides(self): - """Test creating a tool with name and description overrides.""" - - def simple_func() -> str: - """Simple function.""" - return 'result' - - tool = create_tool_from_function( - simple_func, name='custom_name', description='Custom description' - ) - self.assertEqual(tool.function.name, 'custom_name') - self.assertEqual(tool.function.description, 'Custom description') - - def test_create_tool_complex_function(self): - """Test creating a tool from a complex function with multiple types.""" - from enum import Enum - from typing import List, Optional - - class Status(Enum): - ACTIVE = 'active' - INACTIVE = 'inactive' - - def manage_user( - user_id: str, - status: Status = Status.ACTIVE, - tags: Optional[List[str]] = None, - metadata: Dict[str, any] = None, - ) -> str: - """Manage user account. - - Args: - user_id: Unique user identifier - status: Account status - tags: User tags for categorization - metadata: Additional user metadata - """ - return f'Managed user {user_id}' - - tool = create_tool_from_function(manage_user) - self.assertIsNotNone(tool) - self.assertEqual(tool.function.name, 'manage_user') - - # Verify the schema was generated correctly by unpacking it - schema = tool.function.schema_as_dict() - self.assertIsNotNone(schema) - - # Check that all parameters are present - props = schema['properties'] - self.assertIn('user_id', props) - self.assertIn('status', props) - self.assertIn('tags', props) - self.assertIn('metadata', props) - - # Verify enum handling - self.assertEqual(props['status']['type'], 'string') - self.assertIn('enum', props['status']) - - @unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') class TestIntegrationScenarios(unittest.TestCase): """Test real-world integration scenarios.""" diff --git a/tox.ini b/tox.ini index 0c9ebeab..ebd403c3 100644 --- a/tox.ini +++ b/tox.ini @@ -75,6 +75,25 @@ commands_pre = pip3 install -e {toxinidir}/ext/dapr-ext-fastapi/ allowlist_externals=* +[testenv:example-component] +; This environment is used to validate a specific example component. +; Usage: tox -e example-component -- component_name +; Example: tox -e example-component -- conversation +passenv = HOME +basepython = python3 +changedir = ./examples/ +deps = + mechanical-markdown +commands = + ./validate.sh {posargs} + +commands_pre = + pip3 install -e {toxinidir}/ + pip3 install -e {toxinidir}/ext/dapr-ext-workflow/ + pip3 install -e {toxinidir}/ext/dapr-ext-grpc/ + pip3 install -e {toxinidir}/ext/dapr-ext-fastapi/ +allowlist_externals=* + [testenv:type] basepython = python3 usedevelop = False From 975df5d9da738c6bd3e514e209e6abc441c0ba22 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:11:19 -0500 Subject: [PATCH 07/29] fix import in examples Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- examples/conversation/conversation_alpha1.py | 2 +- examples/conversation/conversation_alpha2.py | 2 +- examples/conversation/real_llm_providers_example.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/conversation/conversation_alpha1.py b/examples/conversation/conversation_alpha1.py index 33bfbee4..42ba94c9 100644 --- a/examples/conversation/conversation_alpha1.py +++ b/examples/conversation/conversation_alpha1.py @@ -11,7 +11,7 @@ # limitations under the License. # ------------------------------------------------------------ from dapr.clients import DaprClient -from dapr.clients.grpc._conversation import ConversationInput +from dapr.clients.grpc.conversation import ConversationInput with DaprClient() as d: inputs = [ diff --git a/examples/conversation/conversation_alpha2.py b/examples/conversation/conversation_alpha2.py index dd52b901..7c0e5da8 100644 --- a/examples/conversation/conversation_alpha2.py +++ b/examples/conversation/conversation_alpha2.py @@ -11,7 +11,7 @@ # limitations under the License. # ------------------------------------------------------------ from dapr.clients import DaprClient -from dapr.clients.grpc._conversation import ( +from dapr.clients.grpc.conversation import ( ConversationMessageContent, ConversationMessageOfUser, ConversationMessage, diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index d2d0048d..84f171c7 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -38,7 +38,6 @@ import os import sys import tempfile -from faulthandler import unregister from pathlib import Path from typing import Any, Dict, List, Optional From 6bbb0f5f58e090b70a18301b01b903da5478574d Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:15:21 -0500 Subject: [PATCH 08/29] cleanup, import, lint, more conversation helpers Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/_conversation_helpers.py | 25 +- dapr/clients/grpc/_response.py | 4 +- dapr/clients/grpc/conversation.py | 158 ++++++-- examples/conversation/README.md | 367 +++++++----------- examples/conversation/TOOL-CALL-QUICKSTART.md | 165 ++++++++ examples/conversation/conversation_alpha2.py | 4 +- .../real_llm_providers_example.py | 77 ++-- tests/clients/test_conversation.py | 224 ++++++++++- tests/clients/test_dapr_grpc_client.py | 27 +- 9 files changed, 721 insertions(+), 330 deletions(-) create mode 100644 examples/conversation/TOOL-CALL-QUICKSTART.md diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index 3e858ebb..5b3282d3 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -12,28 +12,28 @@ See the License for the specific language governing permissions and limitations under the License. """ -import random -import string - -from dapr.clients.grpc.conversation import Params import inspect +import random +import string from dataclasses import fields, is_dataclass from enum import Enum from typing import ( Any, + Callable, Dict, List, + Mapping, Optional, + Sequence, Union, get_args, get_origin, get_type_hints, - Callable, - Mapping, - Sequence, ) +from dapr.clients.grpc.conversation import Params + """ Tool Calling Helpers for Dapr Conversation API. @@ -564,11 +564,16 @@ class ToolArgumentError(ToolError): ... -class ToolExecutionForbiddenError(ToolError): - """Exception raised when automatic execution of tools is forbidden per _ALLOW_REGISTER_TOOL_EXECUTION.""" +def bind_params_to_func(fn: Callable[..., Any], params: Params): + """Bind parameters to a function in the correct order. + Args: + fn: The function to bind parameters to + params: The parameters to bind -def bind_params_to_func(fn: Callable[..., Any], params: Params): + Returns: + The bound parameters + """ sig = inspect.signature(fn) if params is None: bound = sig.bind() diff --git a/dapr/clients/grpc/_response.py b/dapr/clients/grpc/_response.py index 73a4d692..2197ee15 100644 --- a/dapr/clients/grpc/_response.py +++ b/dapr/clients/grpc/_response.py @@ -1146,8 +1146,8 @@ def convert_llm_response_to_conversation_input( """Convert the outputs to a list of ConversationInput.""" assistant_messages = [] - for output_idx, output in enumerate(self.outputs or []): - for choice_idx, choice in enumerate(output.choices or []): + for output in self.outputs or []: + for choice in output.choices or []: # Convert and collect all assistant messages assistant_message = convert_llm_response_to_conversation_input(choice.message) assistant_messages.append(assistant_message) diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index dce134de..17716133 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -1,11 +1,26 @@ -from dapr.clients.grpc import _conversation_helpers as conv_helpers -from dapr.proto import api_v1 +# -*- coding: utf-8 -*- + +""" +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/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 asyncio import inspect import json from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union, Protocol +from typing import Any, Callable, Dict, List, Mapping, Optional, Protocol, Sequence, Union + +from dapr.clients.grpc import _conversation_helpers as conv_helpers +from dapr.proto import api_v1 Params = Union[Mapping[str, Any], Sequence[Any], None] @@ -26,6 +41,17 @@ class ConversationMessageContent: text: str +def _indent_lines(title: str, text: str, indent: int) -> str: + """Indent lines of text.""" + indent_after_first_line = indent + len(title) + 2 + lines = text.splitlines() if text is not None else [''] + first = lines[0] if lines else '' + rest = '' + for line in lines[1:]: + rest += f'\n{indent_after_first_line * " "}{line}' + return f'{indent * " "}{title}: {first}{rest}' + + @dataclass class ConversationMessageOfDeveloper: """Developer message content.""" @@ -33,6 +59,13 @@ class ConversationMessageOfDeveloper: name: Optional[str] = None content: List[ConversationMessageContent] = field(default_factory=list) + def trace_print(self, indent: int = 0) -> None: + base = ' ' * indent + if self.name: + print(f'{base}name: {self.name}') + for i, c in enumerate(self.content): + print(_indent_lines(f'content[{i}]', c.text, indent)) + @dataclass class ConversationMessageOfSystem: @@ -41,6 +74,13 @@ class ConversationMessageOfSystem: name: Optional[str] = None content: List[ConversationMessageContent] = field(default_factory=list) + def trace_print(self, indent: int = 0) -> None: + base = ' ' * indent + if self.name: + print(f'{base}name: {self.name}') + for i, c in enumerate(self.content): + print(_indent_lines(f'content[{i}]', c.text, indent)) + @dataclass class ConversationMessageOfUser: @@ -49,6 +89,13 @@ class ConversationMessageOfUser: name: Optional[str] = None content: List[ConversationMessageContent] = field(default_factory=list) + def trace_print(self, indent: int = 0) -> None: + base = ' ' * indent + if self.name: + print(f'{base}name: {self.name}') + for i, c in enumerate(self.content): + print(_indent_lines(f'content[{i}]', c.text, indent)) + @dataclass class ConversationToolCallsOfFunction: @@ -74,6 +121,24 @@ class ConversationMessageOfAssistant: content: List[ConversationMessageContent] = field(default_factory=list) tool_calls: List[ConversationToolCalls] = field(default_factory=list) + def trace_print(self, indent: int = 0) -> None: + base = ' ' * indent + if self.name: + print(f'{base}name: {self.name}') + for i, c in enumerate(self.content): + lines = c.text.splitlines() if c.text is not None else [''] + first = lines[0] if lines else '' + print(f'{base}content[{i}]: {first}') + for extra in lines[1:]: + print(extra) + if self.tool_calls: + print(f'{base}tool_calls: {len(self.tool_calls)}') + for idx, tc in enumerate(self.tool_calls): + tc_id = tc.id or '' + fn = tc.function.name if tc.function else '' + args = tc.function.arguments if tc.function else '' + print(f'{base} [{idx}] id={tc_id} function={fn}({args})') + @dataclass class ConversationMessageOfTool: @@ -83,6 +148,19 @@ class ConversationMessageOfTool: name: str = '' content: List[ConversationMessageContent] = field(default_factory=list) + def trace_print(self, indent: int = 0) -> None: + base = ' ' * indent + if self.tool_id: + print(f'{base}tool_id: {self.tool_id}') + if self.name: + print(f'{base}name: {self.name}') + for i, c in enumerate(self.content): + lines = c.text.splitlines() if c.text is not None else [''] + first = lines[0] if lines else '' + print(f'{base}content[{i}]: {first}') + for extra in lines[1:]: + print(extra) + @dataclass class ConversationMessage: @@ -94,6 +172,25 @@ class ConversationMessage: of_assistant: Optional[ConversationMessageOfAssistant] = None of_tool: Optional[ConversationMessageOfTool] = None + def trace_print(self, indent: int = 0): + print() + """Print the conversation message with indentation and direction arrows.""" + if self.of_developer: + print(f'{" " * indent}client[developer] ---------> LLM[assistant]:') + self.of_developer.trace_print(indent + 2) + if self.of_system: + print(f'{" " * indent}client[system] ---------> LLM[assistant]:') + self.of_system.trace_print(indent + 2) + if self.of_user: + print(f'{" " * indent}client[user] ---------> LLM[assistant]:') + self.of_user.trace_print(indent + 2) + if self.of_assistant: + print(f'{" " * indent}client <-------- LLM[assistant]:') + self.of_assistant.trace_print(indent + 2) + if self.of_tool: + print(f'{" " * indent}client[tool] --------> LLM[assistant]:') + self.of_tool.trace_print(indent + 2) + def to_proto(self) -> api_v1.ConversationMessage: """Convert a conversation message to proto format.""" @@ -199,7 +296,10 @@ def from_function(cls, func: Callable, register: bool = True) -> 'ConversationTo register_tool(c.name, ConversationTools(function=c, backend=FunctionBackend(func))) return c -# --- Tool Helpers + +# ------------------------------------------------------------------------------------------------ +# Tool Helpers +# ------------------------------------------------------------------------------------------------ class ToolBackend(Protocol): @@ -209,7 +309,7 @@ def invoke(self, spec: ConversationToolsFunction, params: Params) -> Any: ... async def ainvoke( - self, spec: ConversationToolsFunction, params: Params, *, timeout: Union[float , None] = None + self, spec: ConversationToolsFunction, params: Params, *, timeout: Union[float, None] = None ) -> Any: ... @@ -232,7 +332,7 @@ def invoke(self, spec: ConversationToolsFunction, params: Params) -> Any: raise conv_helpers.ToolExecutionError(f'Tool raised: {e}') from e async def ainvoke( - self, spec: ConversationToolsFunction, params: Params, *, timeout: Union[float , None] = None + self, spec: ConversationToolsFunction, params: Params, *, timeout: Union[float, None] = None ) -> Any: bound = conv_helpers.bind_params_to_func(self.func, params) try: @@ -241,14 +341,16 @@ async def ainvoke( return await (asyncio.wait_for(coro, timeout) if timeout else coro) loop = asyncio.get_running_loop() return await loop.run_in_executor(None, lambda: self.func(*bound.args, **bound.kwargs)) - except asyncio.TimeoutError: - raise conv_helpers.ToolExecutionError(f'Timed out after {timeout} seconds') + except asyncio.TimeoutError as err: + raise conv_helpers.ToolExecutionError( + f'Timed out after {timeout} seconds' + ) from err except Exception as e: raise conv_helpers.ToolExecutionError(f'Tool raised: {e}') from e def tool( - func: Callable, + func: Optional[Callable] = None, *, name: Optional[str] = None, description: Optional[str] = None, @@ -273,7 +375,8 @@ def _decorate(f: Callable): ct = ConversationTools(function=ctf, backend=FunctionBackend(f)) - setattr(f, '__dapr_conversation_tool__', ct) + # Store the tool in the function for later retrieval + f.__dapr_conversation_tool__ = ct if register: register_tool(ctf.name, ct) @@ -296,7 +399,7 @@ def invoke(self, params: Params = None) -> Any: raise conv_helpers.ToolExecutionError('Tool backend not set') return self.backend.invoke(self.function, params) - async def ainvoke(self, params: Params = None, *, timeout: Union[float , None] = None) -> Any: + async def ainvoke(self, params: Params = None, *, timeout: Union[float, None] = None) -> Any: if not self.backend: raise conv_helpers.ToolExecutionError('Tool backend not set') return await self.backend.ainvoke(self.function, params, timeout=timeout) @@ -306,11 +409,6 @@ async def ainvoke(self, params: Params = None, *, timeout: Union[float , None] = _TOOL_REGISTRY: Dict[str, ConversationTools] = {} -# whether to allow tool execution. It is set to false to express that you should validate the input of the LLM to -# avoid injection attacks. -_ALLOW_REGISTER_TOOL_EXECUTION = False - - def register_tool(name: str, t: ConversationTools): if name in _TOOL_REGISTRY: raise ValueError(f"Tool '{name}' already registered") @@ -328,44 +426,34 @@ def get_registered_tools() -> List[ConversationTools]: return list(_TOOL_REGISTRY.values()) -def set_allow_register_tool_execution(allow: bool): - """Set whether tool execution is allowed.""" - global _ALLOW_REGISTER_TOOL_EXECUTION - _ALLOW_REGISTER_TOOL_EXECUTION = allow - - def _get_tool(name: str) -> ConversationTools: try: return _TOOL_REGISTRY[name] - except KeyError: - raise conv_helpers.ToolNotFoundError(f"Tool '{name}' is not registered") + except KeyError as err: + raise conv_helpers.ToolNotFoundError( + f"Tool '{name}' is not registered" + ) from err -def execute_registered_tool(name: str, params: Union[Params , str] = None) -> Any: +def execute_registered_tool(name: str, params: Union[Params, str] = None) -> Any: """Execute a registered tool.""" - if not _ALLOW_REGISTER_TOOL_EXECUTION: - raise conv_helpers.ToolExecutionForbiddenError( - 'Automatic tool execution is forbidden. To enable it, set it via set_allow_register_tool_execution' - ) if isinstance(params, str): params = json.loads(params) return _get_tool(name).invoke(params) async def execute_registered_tool_async( - name: str, params: Union[Params , str] = None, *, timeout: Union[float , None] = None + name: str, params: Union[Params, str] = None, *, timeout: Union[float, None] = None ) -> Any: """Execute a registered tool asynchronously.""" - if not _ALLOW_REGISTER_TOOL_EXECUTION: - raise conv_helpers.ToolExecutionForbiddenError( - 'Automatic tool execution is forbidden. To enable it, set it via set_allow_register_tool_execution' - ) if isinstance(params, str): params = json.loads(params) return await _get_tool(name).ainvoke(params, timeout=timeout) -# --- Helpers to create messages for Alpha2 inputs +# ------------------------------------------------------------------------------------------------ +# Helpers to create messages for Alpha2 inputs +# ------------------------------------------------------------------------------------------------ def create_user_message(text: str) -> ConversationMessage: diff --git a/examples/conversation/README.md b/examples/conversation/README.md index bbe4e96d..c569b4fa 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -71,7 +71,7 @@ The Conversation API supports real LLM providers including: -6. **Run the comprehensive example with real LLM providers (This requires API Keys)** +6. **Run the comprehensive example with real LLM providers (This requires LLMAPI Keys)** ```bash python examples/conversation/real_llm_providers_example.py @@ -132,11 +132,28 @@ The Alpha2 API introduces sophisticated features: ## Tool Creation (Alpha2) - Excellent DevEx! The Alpha2 API provides powerful functions for tool creation and parameter handling with clean JSON schema support. +You can either define tools via JSON schema or use the recommended decorator-based approach that auto-generates the schema from a typed Python function. + +### Decorator-based Tool Definition (Recommended for most use cases) +```python +from dapr.clients.grpc import conversation + +@conversation.tool +def get_weather(location: str, unit: str = 'fahrenheit') -> str: + """Get current weather for a location.""" + # Implementation or placeholder + return f"Weather in {location} (unit={unit})" + +# Tools registered via @conversation.tool can be retrieved with: +tools = conversation.get_registered_tools() +``` ### Simple JSON Schema Approach ```python # Clean, simple, intuitive approach using standard JSON schema -function = ConversationToolsFunction( +from dapr.clients.grpc import conversation + +function = conversation.ConversationToolsFunction( name="get_weather", description="Get current weather", parameters={ @@ -154,37 +171,9 @@ function = ConversationToolsFunction( "required": ["location"] } ) -weather_tool = ConversationTools(function=function) +weather_tool = conversation.ConversationTools(function=function) ``` -## Understanding the Protobuf Structure - -The Dapr Conversation API uses a protobuf Struct to represent JSON schema for tool parameters, which directly maps to JSON: - -```python -# ConversationToolsFunction.parameters is a protobuf Struct (Dict[str, Any]) -# The parameters directly represent the JSON schema structure -parameters = { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City name"}, - "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} - }, - "required": ["location"] -} -``` - -**Key insights:** -- ✅ **Direct JSON representation** - parameters is a standard Python dict that gets converted to protobuf Struct -- ✅ **Clean developer experience** - write standard JSON schema as Python dict -- ✅ **OpenAI function calling standard** - follows OpenAI's JSON schema format - -**Benefits of Struct-based approach:** - -- Direct JSON schema representation -- Automatic type conversion -- Better error messages and debugging - ### Automatic Parameter Conversion Parameters argument in the converse method for alpha2 are now automatically converted from raw Python types: @@ -212,34 +201,28 @@ response = client.converse_alpha2( ``` -### Function-to-Schema Approach (Ultimate DevEx!) +### Alternative: Function-to-Schema (from_function) We provide a helper function to automatically generate the tool schema from a Python function. ```python -from typing import Optional, List from enum import Enum -from dapr.clients.grpc._conversation import ConversationToolsFunction, ConversationTools +from dapr.clients.grpc import conversation class Units(Enum): - CELSIUS = "celsius" - FAHRENHEIT = "fahrenheit" + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: - '''Get current weather for a location. - - Args: - location: The city and state or country - unit: Temperature unit preference - ''' + """Get current weather for a location.""" return f"Weather in {location}" # Use the from_function class method for automatic schema generation -function = ConversationToolsFunction.from_function(get_weather) -weather_tool = ConversationTools(function=function) +function = conversation.ConversationToolsFunction.from_function(get_weather) +weather_tool = conversation.ConversationTools(function=function) ``` **Benefits:** @@ -248,11 +231,16 @@ weather_tool = ConversationTools(function=function) - ✅ **Ultimate DevEx**: Define functions, get tools automatically - ✅ **90%+ less boilerplate** compared to manual schema creation -### Multiple Tool Creation Approaches +### Alternative Tool Creation Approaches -#### 1. Simple JSON Schema (Recommended) +If you can't use the decorator (e.g., dynamic registration), these alternatives mirror the same +schema but require a bit more boilerplate. + +#### 1) Simple JSON Schema ```python -function = ConversationToolsFunction( +from dapr.clients.grpc import conversation + +function = conversation.ConversationToolsFunction( name="get_weather", description="Get weather", parameters={ @@ -264,12 +252,14 @@ function = ConversationToolsFunction( "required": ["location"] } ) -weather_tool = ConversationTools(function=function) +weather_tool = conversation.ConversationTools(function=function) ``` -#### 2. Complete JSON Schema +#### 2) Complete JSON Schema ```python -function = ConversationToolsFunction( +from dapr.clients.grpc import conversation + +function = conversation.ConversationToolsFunction( name="calculate", description="Perform calculations", parameters={ @@ -280,22 +270,26 @@ function = ConversationToolsFunction( "required": ["expression"] } ) -calc_tool = ConversationTools(function=function) +calc_tool = conversation.ConversationTools(function=function) ``` -#### 3. No Parameters +#### 3) No Parameters ```python -function = ConversationToolsFunction( +from dapr.clients.grpc import conversation + +function = conversation.ConversationToolsFunction( name="get_time", description="Get current time", parameters={"type": "object", "properties": {}, "required": []} ) -time_tool = ConversationTools(function=function) +time_tool = conversation.ConversationTools(function=function) ``` -#### 4. Complex Schema with Arrays +#### 4) Complex Schema with Arrays ```python -function = ConversationToolsFunction( +from dapr.clients.grpc import conversation + +function = conversation.ConversationToolsFunction( name="search", description="Search the web", parameters={ @@ -310,7 +304,7 @@ function = ConversationToolsFunction( "required": ["query"] } ) -search_tool = ConversationTools(function=function) +search_tool = conversation.ConversationTools(function=function) ``` ## Advanced Message Types (Alpha2) @@ -381,38 +375,104 @@ tool_message = ConversationMessage( Alpha2 excels at multi-turn conversations with proper context accumulation: ```python +from dapr.clients.grpc import conversation +from dapr.clients.grpc._response import ConversationResultAlpha2 -from dapr.clients.grpc._conversation import ConversationInputAlpha2 - -# Build conversation history -conversation_history = [] +conversation_history: list[conversation.ConversationMessage] = [] # Turn 1: User asks question -user_message = create_user_message("What's the weather in San Francisco?") -conversation_history.append(user_message) +conversation_history.append(conversation.create_user_message("What's the weather in SF?")) -# LLM responds (potentially with tool calls) -response1 = client.converse_alpha2( +response1: ConversationResultAlpha2 = client.converse_alpha2( name="openai", - inputs=[ConversationInputAlpha2(messages=conversation_history)], - tools=[weather_tool] + inputs=[conversation.ConversationInputAlpha2(messages=conversation_history)], + tools=conversation.get_registered_tools(), + tool_choice='auto', ) -# Add LLM response to history -assistant_message = convert_llm_response_to_conversation_message(response1.outputs[0].choices[0].message) -conversation_history.append(assistant_message) - -# Turn 2: Follow-up question with full context -user_message2 = create_user_message("Should I bring an umbrella?") -conversation_history.append(user_message2) +# Append assistant messages directly using the helper +for msg in response1.to_assistant_messages(): + conversation_history.append(msg) + # If tool calls were returned, you can execute and append a tool message + for tc in msg.of_assistant.tool_calls: + tool_output = conversation.execute_registered_tool(tc.function.name, tc.function.arguments) + conversation_history.append( + conversation.create_tool_message(tool_id=tc.id, name=tc.function.name, content=str(tool_output)) + ) +# Turn 2 with accumulated context +conversation_history.append(conversation.create_user_message("Should I bring an umbrella?")) response2 = client.converse_alpha2( name="openai", - inputs=[ConversationInputAlpha2(messages=conversation_history)], - tools=[weather_tool] + inputs=[conversation.ConversationInputAlpha2(messages=conversation_history)], + tools=conversation.get_registered_tools(), ) ``` +### Trace print + +We have added a trace print method to the ConversationMessage class that will print the conversation history with the direction arrows and the content of the messages that is good for debugging. + +For example in the real_llm_providers_example.py file, we have the following code in a multi-turn conversation: + +```python + + for msg in conversation_history: + msg.trace_print(2) +``` + +That will print the conversation history with the following output (might vary depending on the LLM provider): + +``` +Full conversation history trace: + + client[user] ---------> LLM[assistant]: + content[0]: What's the weather like in San Francisco? Use one of the tools available. + + client <-------- LLM[assistant]: + tool_calls: 1 + [0] id=call_23YKsQyzRhQxcNjjRNRhMmze function=get_weather({"location":"San Francisco"}) + + client[tool] --------> LLM[assistant]: + tool_id: call_23YKsQyzRhQxcNjjRNRhMmze + name: get_weather + content[0]: The weather in San Francisco is sunny with a temperature of 72°F. + + client <-------- LLM[assistant]: + content[0]: The weather in San Francisco is sunny with a temperature of 72°F. + + client[user] ---------> LLM[assistant]: + content[0]: Should I bring an umbrella? Also, what about the weather in New York? + + client <-------- LLM[assistant]: + tool_calls: 2 + [0] id=call_eWXkLbxKOAZRPoaAK0siYwHx function=get_weather({"location": "New York"}) + [1] id=call_CnKVzPmbCUEPZqipoemMF5jr function=get_weather({"location": "San Francisco"}) + + client[tool] --------> LLM[assistant]: + tool_id: call_eWXkLbxKOAZRPoaAK0siYwHx + name: get_weather + content[0]: The weather in New York is sunny with a temperature of 72°F. + + client[tool] --------> LLM[assistant]: + tool_id: call_CnKVzPmbCUEPZqipoemMF5jr + name: get_weather + content[0]: The weather in San Francisco is sunny with a temperature of 72°F. + + client <-------- LLM[assistant]: + content[0]: The weather in both San Francisco and New York is sunny with a temperature of 72°F. Since it's sunny in both locations, you likely won't need to bring an umbrella. +``` + + +### How context accumulation works + +- Context is not automatic. Each turn you must pass the entire `messages` history you want the LLM to see. +- Append assistant responses using `response.to_assistant_messages()` before the next turn. +- If the LLM makes tool calls, execute them locally and append a tool result message via `conversation.create_tool_message(...)`. +- Re-send available tools on every turn (e.g., `tools=conversation.get_registered_tools()`), especially when the provider requires tools to be present to call them. +- Keep history as a list of `ConversationMessage` objects; add new user/assistant/tool messages as the dialog progresses. +- Context Engineering is a key skill for multi-turn conversations and you will need to experiment with different approaches to get the best results as you cannot keep accumulating context forever. + ## Async Support Full async/await support for non-blocking operations: @@ -518,6 +578,7 @@ conversation_history.append(assistant_message) - Parameter conversion demonstration - Multi-turn tool calling with context accumulation - Function-to-schema automatic tool generation + - Decorator-based tool definition - Async conversation and tool calling support - Backward compatibility with Alpha1 @@ -530,106 +591,7 @@ conversation_history.append(assistant_message) - `.env.example` - Environment variables template - `config/` directory - Provider-specific component configurations -## Quick Start - -### Basic Alpha2 Conversation with Tool Calling - -```python -from dapr.clients import DaprClient -from dapr.clients.grpc._request import ( - ConversationTools -) -from dapr.clients.grpc._conversation import ConversationMessageContent, ConversationMessageOfUser, ConversationMessage, - -ConversationToolsFunction -ConversationInputAlpha2 - -# Create a tool using the simple approach -function = ConversationToolsFunction( - name="get_weather", - description="Get current weather for a location", - parameters={ - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state or country" - }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "Temperature unit" - } - }, - "required": ["location"] - } -) -weather_tool = ConversationTools(function=function) - -# Create a user message -user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="What's the weather in Paris?")] - ) -) - -# Create input and make the request -input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - -with DaprClient() as client: - response = client.converse_alpha2( - name="openai", # or "anthropic", "mistral", etc. - inputs=[input_alpha2], - parameters={ - 'temperature': 0.7, # Auto-converted to GrpcAny! - 'max_tokens': 500, # Auto-converted to GrpcAny! - 'stream': False # Streaming not supported in Alpha2 - }, - tools=[weather_tool], - tool_choice='auto' - ) - - # Process the response - if response.outputs and response.outputs[0].choices: - choice = response.outputs[0].choices[0] - if choice.finish_reason == 'tool_calls' and choice.message.tool_calls: - print(f"LLM wants to call: {choice.message.tool_calls[0].function.name}") - print(f"With arguments: {choice.message.tool_calls[0].function.arguments}") - else: - print(f"LLM response: {choice.message.content}") -``` - -### Real Provider Setup -1. Set up environment: - ```bash - export OPENAI_API_KEY="your_key_here" - ``` - -2. Create component configuration (`components/openai.yaml`): - ```yaml - apiVersion: dapr.io/v1alpha1 - kind: Component - metadata: - name: openai - spec: - type: conversation.openai - version: v1 - metadata: - - name: key - value: "your_openai_api_key" - - name: model - value: "gpt-4o-mini" - ``` - -3. Start Dapr sidecar: - ```bash - dapr run --app-id test-app --dapr-grpc-port 50001 --resources-path ./components/ - ``` - -4. Run your conversation code! - -For a complete working example with multiple providers, see `real_llm_providers_example.py`. ## Troubleshooting @@ -659,61 +621,6 @@ For a complete working example with multiple providers, see `real_llm_providers_ - Set `stream: False` in parameters (this is the default) - All responses are returned as complete, non-streaming messages -### Environment Variables - -```bash -# Required for respective providers -OPENAI_API_KEY=... -ANTHROPIC_API_KEY=... -MISTRAL_API_KEY=... -DEEPSEEK_API_KEY=... -GOOGLE_API_KEY=... - -# Optional: Use local development build -USE_LOCAL_DEV=true -BUILD_LOCAL_DAPR=true -``` - -## Migration from Alpha1 to Alpha2 - -Alpha2 provides significant improvements while maintaining backward compatibility: - -### Alpha1 (Legacy) - -```python - -from dapr.clients.grpc._conversation import ConversationInput - -inputs = [ConversationInput( - content="Hello!", - role="user" -)] - -response = client.converse_alpha1( - name="provider", - inputs=inputs, - parameters={'temperature': 0.7} -) -``` - -### Alpha2 (Recommended) - -```python - -from dapr.clients.grpc._conversation import ConversationMessage, ConversationInputAlpha2 - -user_message = ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="Hello!")] - ) -) - -response = client.converse_alpha2( - name="provider", - inputs=[ConversationInputAlpha2(messages=[user_message])], - parameters={'temperature': 0.7} # Auto-converted! -) -``` ## Features Overview diff --git a/examples/conversation/TOOL-CALL-QUICKSTART.md b/examples/conversation/TOOL-CALL-QUICKSTART.md new file mode 100644 index 00000000..4d0ab88e --- /dev/null +++ b/examples/conversation/TOOL-CALL-QUICKSTART.md @@ -0,0 +1,165 @@ +# Conversation API Tool Calling Quickstart (Alpha2) + +This guide shows the cleanest, most ergonomic way to use the Conversation API with tools and multi‑turn flows. + +## Recommended: Decorator‑based Tools + +```python +from dapr.clients import DaprClient +from dapr.clients.grpc import conversation + + +@conversation.tool +def get_weather(location: str, unit: str = 'fahrenheit') -> str: + """Get current weather for a location.""" + return f"Weather in {location} (unit={unit})" + + +user_msg = conversation.create_user_message("What's the weather in Paris?") +input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_msg]) + +with DaprClient() as client: + response = client.converse_alpha2( + name="openai", + inputs=[input_alpha2], + tools=conversation.get_registered_tools(), # tools registered by @conversation.tool + tool_choice='auto', + parameters={'temperature': 0.2, 'max_tokens': 200}, # raw values auto-converted + ) + + for msg in response.to_assistant_messages(): + if msg.of_assistant.tool_calls: + for tc in msg.of_assistant.tool_calls: + print(f"Tool call: {tc.function.name} args={tc.function.arguments}") + else: + print(msg.of_assistant.content[0].text) +``` + +## Minimal Multi‑turn Pattern with Tools + +```python +from dapr.clients import DaprClient +from dapr.clients.grpc import conversation + + +@conversation.tool +def get_weather(location: str, unit: str = 'fahrenheit') -> str: + return f"Weather in {location} (unit={unit})" + + +history: list[conversation.ConversationMessage] = [] +history.append(conversation.create_user_message("What's the weather in San Francisco?")) + +with DaprClient() as client: + # Turn 1 + resp1 = client.converse_alpha2( + name="openai", + inputs=[conversation.ConversationInputAlpha2(messages=history)], + tools=conversation.get_registered_tools(), + tool_choice='auto', + parameters={'temperature': 0.2}, + ) + + # Append assistant messages; execute any tool calls and append tool results + for msg in resp1.to_assistant_messages(): + history.append(msg) + for tc in msg.of_assistant.tool_calls: + # Execute (we suggest validating inputs before execution in production) + tool_output = conversation.execute_registered_tool(tc.function.name, tc.function.arguments) + history.append( + conversation.create_tool_message(tool_id=tc.id, name=tc.function.name, content=str(tool_output)) + ) + + # Turn 2 (LLM sees tool result) + history.append(conversation.create_user_message("Should I bring an umbrella?")) + resp2 = client.converse_alpha2( + name="openai", + inputs=[conversation.ConversationInputAlpha2(messages=history)], + tools=conversation.get_registered_tools(), + parameters={'temperature': 0.2}, + ) + + for msg in resp2.to_assistant_messages(): + history.append(msg) + if not msg.of_assistant.tool_calls: + print(msg.of_assistant.content[0].text) +``` + + + +## Alternative: Function‑to‑Schema (from_function) + +```python +from enum import Enum +from dapr.clients.grpc import conversation + + +class Units(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + + +def get_weather(location: str, unit: Units = Units.FAHRENHEIT) -> str: + return f"Weather in {location}" + + +fn = conversation.ConversationToolsFunction.from_function(get_weather) +weather_tool = conversation.ConversationTools(function=fn) +``` + +## JSON Schema Variants (fallbacks) + +```python +from dapr.clients.grpc import conversation + + +# Simple schema +fn = conversation.ConversationToolsFunction( + name='get_weather', + description='Get current weather', + parameters={ + 'type': 'object', + 'properties': { + 'location': {'type': 'string'}, + 'unit': {'type': 'string', 'enum': ['celsius', 'fahrenheit']}, + }, + 'required': ['location'], + }, +) +weather_tool = conversation.ConversationTools(function=fn) +``` + +## Async Variant + +```python +import asyncio +from dapr.aio.clients import DaprClient as AsyncDaprClient +from dapr.clients.grpc import conversation + + +@conversation.tool +def get_time() -> str: + return '2025-01-01T12:00:00Z' + + +async def main(): + async with AsyncDaprClient() as client: + msg = conversation.create_user_message('What time is it?') + inp = conversation.ConversationInputAlpha2(messages=[msg]) + resp = await client.converse_alpha2( + name='openai', inputs=[inp], tools=conversation.get_registered_tools() + ) + for m in resp.to_assistant_messages(): + if m.of_assistant.content: + print(m.of_assistant.content[0].text) + + +asyncio.run(main()) +``` + +## See also + +- `examples/conversation/real_llm_providers_example.py` — end‑to‑end multi‑turn and tool calling flows with real providers +- Main README in this folder for provider setup and additional examples + + diff --git a/examples/conversation/conversation_alpha2.py b/examples/conversation/conversation_alpha2.py index 7c0e5da8..7d3c7111 100644 --- a/examples/conversation/conversation_alpha2.py +++ b/examples/conversation/conversation_alpha2.py @@ -12,10 +12,10 @@ # ------------------------------------------------------------ from dapr.clients import DaprClient from dapr.clients.grpc.conversation import ( + ConversationInputAlpha2, + ConversationMessage, ConversationMessageContent, ConversationMessageOfUser, - ConversationMessage, - ConversationInputAlpha2, ) with DaprClient() as d: diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index 84f171c7..57c56b1d 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -1,4 +1,15 @@ -#!/usr/bin/env python3 +# ------------------------------------------------------------ +# 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/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. +# ------------------------------------------------------------ """ Real LLM Providers Example for Dapr Conversation API (Alpha2) @@ -9,6 +20,7 @@ - Automatic parameter conversion (raw Python values) - Enhanced tool calling capabilities - Multi-turn conversations +- Decorator-based tool definition - Both sync and async implementations Prerequisites: @@ -16,10 +28,7 @@ 2. For manual mode: Start Dapr sidecar manually Usage: - # Automatic mode (recommended) - manages Dapr sidecar automatically - python examples/conversation/real_llm_providers_example.py - - # Manual mode - requires manual Dapr sidecar setup + # requires manual Dapr sidecar setup python examples/conversation/real_llm_providers_example.py # Show help @@ -39,11 +48,12 @@ import sys import tempfile from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional import yaml -from dapr.clients.grpc._response import ConversationResultAlpha2Message +if TYPE_CHECKING: + from dapr.clients.grpc._response import ConversationResultAlpha2Message # Add the parent directory to the path so we can import local dapr sdk # uncomment if running from development version @@ -238,7 +248,7 @@ def execute_weather_tool(location: str, unit: str = 'fahrenheit') -> str: def convert_llm_response_to_conversation_input( - result_message: ConversationResultAlpha2Message, + result_message: 'ConversationResultAlpha2Message', ) -> conversation.ConversationMessage: """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input). @@ -805,10 +815,7 @@ def get_weather(location: str, unit: str = 'fahrenheit') -> str: try: with DaprClient() as client: - conversation_history = [] - - # allow automatically execute tool calls without checking arguments - conversation.set_allow_register_tool_execution(True) + conversation_history = [] # our context to pass to the LLM on each turn # Turn 1: User asks about weather (include tools) print('\n--- Turn 1: Initial weather query ---') @@ -833,25 +840,27 @@ def get_weather(location: str, unit: str = 'fahrenheit') -> str: }, ) - def append_response_to_history(response): + def append_response_to_history(response: 'ConversationResultAlpha2Message'): + """Helper to append response to history and execute tool calls.""" for msg in response.to_assistant_messages(): conversation_history.append(msg) - if msg.of_assistant.tool_calls: - for _tool_call in msg.of_assistant.tool_calls: - print(f'Executing tool call: {_tool_call.function.name}') - output = conversation.execute_registered_tool( - _tool_call.function.name, _tool_call.function.arguments - ) - print(f'Tool output: {output}') - - # append result to history - conversation_history.append( - conversation.create_tool_message( - tool_id=_tool_call.id, - name=_tool_call.function.name, - content=output, - ) + if not msg.of_assistant.tool_calls: + continue + for _tool_call in msg.of_assistant.tool_calls: + print(f'Executing tool call: {_tool_call.function.name}') + output = conversation.execute_registered_tool( + _tool_call.function.name, _tool_call.function.arguments + ) + print(f'Tool output: {output}') + + # append result to history + conversation_history.append( + conversation.create_tool_message( + tool_id=_tool_call.id, + name=_tool_call.function.name, + content=output, ) + ) append_response_to_history(response1) @@ -922,15 +931,9 @@ def append_response_to_history(response): append_response_to_history(response4) - # print full history - for i, msg in enumerate(conversation_history): - print(f'History Index {i}: {msg}') - - else: - print( - '⚠️ No tool calls found in any output/choice - continuing with regular conversation flow' - ) - # Could continue with regular multi-turn conversation without tools + print('Full conversation history trace:') + for msg in conversation_history: + msg.trace_print(2) except Exception as e: print(f'❌ Multi-turn tool calling error: {e}') diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index 6c522103..43d75e5b 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -12,28 +12,40 @@ - Both sync and async implementations - Parameter conversion and validation """ - import asyncio import unittest +import uuid from google.rpc import code_pb2, status_pb2 from dapr.aio.clients import DaprClient as AsyncDaprClient from dapr.clients import DaprClient from dapr.clients.exceptions import DaprGrpcError -from dapr.conf import settings +from dapr.clients.grpc import conversation +from dapr.clients.grpc._conversation_helpers import ( + ToolArgumentError, + ToolExecutionError, +) from dapr.clients.grpc.conversation import ( ConversationInput, + ConversationInputAlpha2, + ConversationMessage, ConversationMessageContent, - ConversationMessageOfSystem, - ConversationMessageOfUser, ConversationMessageOfAssistant, + ConversationMessageOfSystem, ConversationMessageOfTool, - ConversationMessage, - ConversationInputAlpha2, + ConversationMessageOfUser, + ConversationTools, ConversationToolsFunction, - ConversationTools + FunctionBackend, + execute_registered_tool_async, + get_registered_tools, + register_tool, + tool as tool_decorator, + unregister_tool, ) +from dapr.conf import settings + from tests.clients.fake_dapr_server import FakeDaprSidecar @@ -736,5 +748,203 @@ def test_mixed_alpha1_alpha2_compatibility(self): self.assertTrue(hasattr(alpha2_response.outputs[0], 'choices')) +class ConversationToolHelpersSyncTests(unittest.TestCase): + """Tests for conversation tool helpers, registry, and backends (sync).""" + + def tearDown(self): + # Cleanup tools with known prefixes + for t in list(get_registered_tools()): + try: + name = t.function.name + if name.startswith('test_') or name.startswith('ns_') or name.startswith('dup_'): + unregister_tool(name) + except Exception: + continue + + def test_tool_decorator_namespace_and_name_override(self): + ns_unique = uuid.uuid4().hex[:6] + name_override = f'test_sum_{ns_unique}' + + @tool_decorator(namespace=f'ns.{ns_unique}', name=name_override) + def foo(x: int, y: int) -> int: + return x + y + + names = {t.function.name for t in get_registered_tools()} + self.assertIn(name_override, names) + unregister_tool(name_override) + + ns_tool = f'ns.{ns_unique}.bar' + + @tool_decorator(namespace=f'ns.{ns_unique}') + def bar(q: int) -> int: + return q * 2 + + names = {t.function.name for t in get_registered_tools()} + self.assertIn(ns_tool, names) + unregister_tool(ns_tool) + + def test_register_tool_duplicate_raises(self): + dup_name = f'dup_tool_{uuid.uuid4().hex[:6]}' + ct = ConversationTools( + function=ConversationToolsFunction(name=dup_name, parameters={'type': 'object'}), + backend=FunctionBackend(lambda: None), + ) + register_tool(dup_name, ct) + try: + with self.assertRaises(ValueError): + register_tool(dup_name, ct) + finally: + unregister_tool(dup_name) + + def test_conversationtools_invoke_without_backend_raises(self): + ct = ConversationTools( + function=ConversationToolsFunction( + name='test_no_backend', parameters={'type': 'object'} + ), + backend=None, + ) + with self.assertRaises(ToolExecutionError): + ct.invoke({'a': 1}) + + async def run(): + with self.assertRaises(ToolExecutionError): + await ct.ainvoke({'a': 1}) + + asyncio.run(run()) + + def test_functionbackend_sync_and_async_and_timeout(self): + def mul(a: int, b: int) -> int: + return a * b + + fb_sync = FunctionBackend(mul) + self.assertEqual( + fb_sync.invoke(ConversationToolsFunction(name='mul'), {'a': 3, 'b': 5}), + 15, + ) + + async def run_sync_via_async(): + res = await fb_sync.ainvoke(ConversationToolsFunction(name='mul'), {'a': 2, 'b': 7}) + self.assertEqual(res, 14) + + asyncio.run(run_sync_via_async()) + + async def wait_and_return(x: int, delay: float = 0.01) -> int: + await asyncio.sleep(delay) + return x + + fb_async = FunctionBackend(wait_and_return) + with self.assertRaises(ToolExecutionError): + fb_async.invoke(ConversationToolsFunction(name='wait'), {'x': 1}) + + async def run_async_ok(): + res = await fb_async.ainvoke(ConversationToolsFunction(name='wait'), {'x': 42}) + self.assertEqual(res, 42) + + asyncio.run(run_async_ok()) + + async def run_async_timeout(): + with self.assertRaises(ToolExecutionError): + await fb_async.ainvoke( + ConversationToolsFunction(name='wait'), + {'x': 1, 'delay': 0.2}, + timeout=0.01, + ) + + asyncio.run(run_async_timeout()) + + with self.assertRaises(ToolArgumentError): + fb_sync.invoke(ConversationToolsFunction(name='mul'), {'a': 1}) + + async def run_missing_arg_async(): + with self.assertRaises(ToolArgumentError): + await fb_sync.ainvoke(ConversationToolsFunction(name='mul'), {'a': 1}) + + asyncio.run(run_missing_arg_async()) + + def test_conversationtoolsfunction_from_function_and_schema(self): + def greet(name: str, punctuation: str = '!') -> str: + """Say hello. + + Args: + name: Person to greet + punctuation: Trailing punctuation + """ + + return f'Hello, {name}{punctuation}' + + spec = ConversationToolsFunction.from_function(greet, register=False) + schema = spec.schema_as_dict() + self.assertIn('name', schema.get('properties', {})) + self.assertIn('name', schema.get('required', [])) + self.assertIn('punctuation', schema.get('properties', {})) + + spec2 = ConversationToolsFunction.from_function(greet, register=True) + try: + names = {t.function.name for t in get_registered_tools()} + self.assertIn(spec2.name, names) + finally: + unregister_tool(spec2.name) + + def test_message_helpers_and_to_proto(self): + user_msg = conversation.create_user_message('hi') + self.assertIsNotNone(user_msg.of_user) + self.assertEqual(user_msg.of_user.content[0].text, 'hi') + proto_user = user_msg.to_proto() + self.assertEqual(proto_user.of_user.content[0].text, 'hi') + + sys_msg = conversation.create_system_message('sys') + proto_sys = sys_msg.to_proto() + self.assertEqual(proto_sys.of_system.content[0].text, 'sys') + + tc = conversation.ConversationToolCalls( + id='abc123', + function=conversation.ConversationToolCallsOfFunction(name='fn', arguments='{}'), + ) + asst_msg = conversation.ConversationMessage( + of_assistant=conversation.ConversationMessageOfAssistant( + content=[conversation.ConversationMessageContent(text='ok')], + tool_calls=[tc], + ) + ) + proto_asst = asst_msg.to_proto() + self.assertEqual(proto_asst.of_assistant.content[0].text, 'ok') + self.assertEqual(proto_asst.of_assistant.tool_calls[0].function.name, 'fn') + + tool_msg = conversation.create_tool_message('tid1', 'get_weather', 'cloudy') + proto_tool = tool_msg.to_proto() + self.assertEqual(proto_tool.of_tool.tool_id, 'tid1') + self.assertEqual(proto_tool.of_tool.name, 'get_weather') + self.assertEqual(proto_tool.of_tool.content[0].text, 'cloudy') + + +class ConversationToolHelpersAsyncTests(unittest.IsolatedAsyncioTestCase): + async def asyncTearDown(self): + for t in list(get_registered_tools()): + try: + name = t.function.name + if name.startswith('test_'): + unregister_tool(name) + except Exception: + continue + + async def test_execute_registered_tool_async(self): + unique = uuid.uuid4().hex[:8] + tool_name = f'test_async_{unique}' + + @tool_decorator(name=tool_name) + async def echo(value: str, delay: float = 0.0) -> str: + await asyncio.sleep(delay) + return value + + out = await execute_registered_tool_async(tool_name, {'value': 'hello'}) + self.assertEqual(out, 'hello') + + with self.assertRaises(ToolExecutionError): + await execute_registered_tool_async( + tool_name, {'value': 'slow', 'delay': 0.2}, timeout=0.01 + ) + unregister_tool(tool_name) + + if __name__ == '__main__': unittest.main() diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index cc5e360c..e0713f70 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -1242,12 +1242,15 @@ def test_converse_alpha2_basic_user_message(self): # Create user message user_message = conversation.ConversationMessage( of_user=conversation.ConversationMessageOfUser( - name='TestUser', content=[conversation.ConversationMessageContent(text='Hello, how are you?')] + name='TestUser', + content=[conversation.ConversationMessageContent(text='Hello, how are you?')], ) ) # Create Alpha2 input - input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message], scrub_pii=False) + input_alpha2 = conversation.ConversationInputAlpha2( + messages=[user_message], scrub_pii=False + ) response = dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) @@ -1319,7 +1322,9 @@ def test_converse_alpha2_system_message(self): # Create system message system_message = conversation.ConversationMessage( of_system=conversation.ConversationMessageOfSystem( - content=[conversation.ConversationMessageContent(text='You are a helpful assistant.')] + content=[ + conversation.ConversationMessageContent(text='You are a helpful assistant.') + ] ) ) @@ -1342,7 +1347,9 @@ def test_converse_alpha2_developer_message(self): developer_message = conversation.ConversationMessage( of_developer=conversation.ConversationMessageOfDeveloper( name='DevTeam', - content=[conversation.ConversationMessageContent(text='Debug: Processing user input')], + content=[ + conversation.ConversationMessageContent(text='Debug: Processing user input') + ], ) ) @@ -1367,7 +1374,9 @@ def test_converse_alpha2_tool_message(self): tool_id='call_123', name='get_weather', content=[ - conversation.ConversationMessageContent(text='{"temperature": 22, "condition": "sunny"}') + conversation.ConversationMessageContent( + text='{"temperature": 22, "condition": "sunny"}' + ) ], ) ) @@ -1416,7 +1425,9 @@ def test_converse_alpha2_multiple_messages(self): ) user_message = conversation.ConversationMessage( - of_user=conversation.ConversationMessageOfUser(content=[conversation.ConversationMessageContent(text='Hello!')]) + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text='Hello!')] + ) ) input_alpha2 = conversation.ConversationInputAlpha2(messages=[system_message, user_message]) @@ -1502,7 +1513,9 @@ def test_converse_alpha2_tool_choice_specific(self): ) calculator_tool = conversation.ConversationTools( - function=conversation.ConversationToolsFunction(name='calculate', description='Perform calculations') + function=conversation.ConversationToolsFunction( + name='calculate', description='Perform calculations' + ) ) user_message = conversation.ConversationMessage( From 8697b8e278052e91827639f01578f1b08671b86e Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:55:36 -0500 Subject: [PATCH 09/29] clarify README, minor test import changes, copyright Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- examples/conversation/README.md | 130 ++++++++------------------------ 1 file changed, 31 insertions(+), 99 deletions(-) diff --git a/examples/conversation/README.md b/examples/conversation/README.md index c569b4fa..d7d88c10 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -129,12 +129,14 @@ The Alpha2 API introduces sophisticated features: - **Streaming**: Response streaming is not yet supported in Alpha2. All responses are returned as complete messages. -## Tool Creation (Alpha2) - Excellent DevEx! +## Tool Creation (Alpha2) -The Alpha2 API provides powerful functions for tool creation and parameter handling with clean JSON schema support. -You can either define tools via JSON schema or use the recommended decorator-based approach that auto-generates the schema from a typed Python function. +Recommended order of approaches: +- Decorator-based definition (best ergonomics) +- Function-to-Schema (automatic schema from typed function) +- JSON schema variants (fallbacks for dynamic/manual cases) -### Decorator-based Tool Definition (Recommended for most use cases) +### Decorator-based Tool Definition (Recommended) ```python from dapr.clients.grpc import conversation @@ -148,62 +150,9 @@ def get_weather(location: str, unit: str = 'fahrenheit') -> str: tools = conversation.get_registered_tools() ``` -### Simple JSON Schema Approach -```python -# Clean, simple, intuitive approach using standard JSON schema -from dapr.clients.grpc import conversation +### Function-to-Schema (from_function) -function = conversation.ConversationToolsFunction( - name="get_weather", - description="Get current weather", - parameters={ - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "City name" - }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"] - } - }, - "required": ["location"] - } -) -weather_tool = conversation.ConversationTools(function=function) -``` - -### Automatic Parameter Conversion - -Parameters argument in the converse method for alpha2 are now automatically converted from raw Python types: - -```python -# Before: Manual GrpcAny creation for every parameter ❌ -temp_param = GrpcAny() -temp_param.Pack(DoubleValue(value=0.7)) - -# After: Raw Python values automatically converted ✅ -response = client.converse_alpha2( - name="my-provider", - inputs=[input_alpha2], - parameters={ - 'temperature': 0.7, # float -> GrpcAny - 'max_tokens': 1000, # int -> GrpcAny - 'stream': False, # bool -> GrpcAny - 'model': 'gpt-4', # string -> GrpcAny - 'config': { # dict -> GrpcAny (JSON) - 'features': ['a', 'b'], # nested arrays supported - 'enabled': True # nested values converted - } - } -) -``` - - -### Alternative: Function-to-Schema (from_function) - -We provide a helper function to automatically generate the tool schema from a Python function. +Automatically generate the tool schema from a typed Python function. ```python from enum import Enum @@ -225,18 +174,17 @@ function = conversation.ConversationToolsFunction.from_function(get_weather) weather_tool = conversation.ConversationTools(function=function) ``` -**Benefits:** +Benefits: - ✅ **Type Safety**: Full Python type hint support (str, int, List, Optional, Enum, etc.) - ✅ **Auto-Documentation**: Docstring parsing for parameter descriptions - ✅ **Ultimate DevEx**: Define functions, get tools automatically - ✅ **90%+ less boilerplate** compared to manual schema creation -### Alternative Tool Creation Approaches +### JSON Schema Variants (fallbacks) -If you can't use the decorator (e.g., dynamic registration), these alternatives mirror the same -schema but require a bit more boilerplate. +Use when you can't decorate or need to build tools dynamically. -#### 1) Simple JSON Schema +#### Simple JSON Schema ```python from dapr.clients.grpc import conversation @@ -255,7 +203,7 @@ function = conversation.ConversationToolsFunction( weather_tool = conversation.ConversationTools(function=function) ``` -#### 2) Complete JSON Schema +#### Complete JSON Schema (e.g., calculator) ```python from dapr.clients.grpc import conversation @@ -273,7 +221,7 @@ function = conversation.ConversationToolsFunction( calc_tool = conversation.ConversationTools(function=function) ``` -#### 3) No Parameters +#### No Parameters ```python from dapr.clients.grpc import conversation @@ -285,7 +233,7 @@ function = conversation.ConversationToolsFunction( time_tool = conversation.ConversationTools(function=function) ``` -#### 4) Complex Schema with Arrays +#### Complex Schema with Arrays ```python from dapr.clients.grpc import conversation @@ -370,6 +318,21 @@ tool_message = ConversationMessage( ) ``` +### Convenience message helpers + +You can create the same messages more concisely using helpers from `conversation`: + +```python +from dapr.clients.grpc import conversation + +user = conversation.create_user_message("What's the weather in Paris?") +system = conversation.create_system_message("You are a helpful AI assistant.") +assistant = conversation.create_assistant_message("I can help you with that!") +tool_result = conversation.create_tool_message( + tool_id="call_123", name="get_weather", content="Weather: 72°F, sunny" +) +``` + ## Multi-turn Conversations Alpha2 excels at multi-turn conversations with proper context accumulation: @@ -512,7 +475,7 @@ result = asyncio.run(async_conversation()) ## Dapr Component Configuration -For real LLM providers, you need Dapr component configurations. The example automatically creates these: +For real LLM providers, you need Dapr component configurations. The example real_llm_providers_example.py automatically creates these for each provider you have configured in the .env file: ### OpenAI Component Example ```yaml @@ -536,37 +499,6 @@ spec: dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path /tmp/dapr-llm-components-xyz/ ``` -## Helper Functions - -Convert LLM responses for multi-turn conversations: - -```python -from dapr.clients.grpc._response import ConversationResultAlpha2Message - - -def convert_llm_response_to_conversation_message( - result_message: ConversationResultAlpha2Message) -> ConversationMessage: - """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input).""" - content = [] - if result_message.content: - content = [ConversationMessageContent(text=result_message.content)] - - tool_calls = result_message.tool_calls or [] - - return ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - content=content, - tool_calls=tool_calls - ) - ) - - -# Usage in multi-turn conversations -response = client.converse_alpha2(name="openai", inputs=[input_alpha2], tools=[tool]) -choice = response.outputs[0].choices[0] -assistant_message = convert_llm_response_to_conversation_message(choice.message) -conversation_history.append(assistant_message) -``` ## Examples in This Directory From 5d16fb51201a6354147ee00b0d08923b8195912f Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:00:53 -0500 Subject: [PATCH 10/29] feedback DRY test_conversation file Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- tests/clients/test_conversation.py | 711 ++++++++++++++--------------- 1 file changed, 344 insertions(+), 367 deletions(-) diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index 43d75e5b..d504082d 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -1,17 +1,19 @@ -#!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ -Comprehensive tests for Dapr conversation API functionality. - -This test suite covers: -- Basic conversation API (Alpha1) -- Advanced conversation API (Alpha2) with tool calling -- Multi-turn conversations -- Different message types (user, system, assistant, developer, tool) -- Error handling -- Both sync and async implementations -- Parameter conversion and validation +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 asyncio import unittest import uuid @@ -29,25 +31,37 @@ from dapr.clients.grpc.conversation import ( ConversationInput, ConversationInputAlpha2, - ConversationMessage, - ConversationMessageContent, - ConversationMessageOfAssistant, - ConversationMessageOfSystem, - ConversationMessageOfTool, - ConversationMessageOfUser, ConversationTools, ConversationToolsFunction, FunctionBackend, + create_assistant_message, + create_system_message, + create_tool_message, + create_user_message, execute_registered_tool_async, get_registered_tools, register_tool, - tool as tool_decorator, unregister_tool, ) +from dapr.clients.grpc.conversation import ( + tool as tool_decorator, +) from dapr.conf import settings - from tests.clients.fake_dapr_server import FakeDaprSidecar +""" +Comprehensive tests for Dapr conversation API functionality. + +This test suite covers: +- Basic conversation API (Alpha1) +- Advanced conversation API (Alpha2) with tool calling +- Multi-turn conversations +- Different message types (user, system, assistant, developer, tool) +- Error handling +- Both sync and async implementations +- Parameter conversion and validation +""" + def create_weather_tool(): """Create a weather tool for testing.""" @@ -94,38 +108,6 @@ def create_calculate_tool(): ) -def create_user_message(text): - """Helper to create a user message for Alpha2.""" - return ConversationMessage( - of_user=ConversationMessageOfUser(content=[ConversationMessageContent(text=text)]) - ) - - -def create_system_message(text): - """Helper to create a system message for Alpha2.""" - return ConversationMessage( - of_system=ConversationMessageOfSystem(content=[ConversationMessageContent(text=text)]) - ) - - -def create_assistant_message(text, tool_calls=None): - """Helper to create an assistant message for Alpha2.""" - return ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - content=[ConversationMessageContent(text=text)], tool_calls=tool_calls or [] - ) - ) - - -def create_tool_message(tool_id, name, content): - """Helper to create a tool message for Alpha2.""" - return ConversationMessage( - of_tool=ConversationMessageOfTool( - tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] - ) - ) - - class ConversationTestBase: """Base class for conversation tests with common setup.""" @@ -145,31 +127,50 @@ def setUpClass(cls): def tearDownClass(cls): cls._fake_dapr_server.stop() +class ConversationTestBaseSync(ConversationTestBase, unittest.TestCase): + """Base class for conversation tests with common setup.""" + + def setUp(self): + super().setUp() + self.client = DaprClient(f'{self.scheme}localhost:{self.grpc_port}') + + def tearDown(self): + super().tearDown() + self.client.close() + -class ConversationAlpha1SyncTests(ConversationTestBase, unittest.TestCase): +class ConversationTestBaseAsync(ConversationTestBase, unittest.IsolatedAsyncioTestCase): + """Base class for conversation tests with common setup.""" + async def asyncSetUp(self): + await super().asyncSetUp() + self.client = AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') + + async def asyncTearDown(self): + await super().asyncTearDown() + await self.client.close() + +class ConversationAlpha1SyncTests(ConversationTestBaseSync): """Synchronous Alpha1 conversation API tests.""" def test_basic_conversation_alpha1(self): """Test basic Alpha1 conversation functionality.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ - ConversationInput(content='Hello', role='user'), - ConversationInput(content='How are you?', role='user'), - ] + inputs = [ + ConversationInput(content='Hello', role='user'), + ConversationInput(content='How are you?', role='user'), + ] - response = client.converse_alpha1(name='test-llm', inputs=inputs) + response = self.client.converse_alpha1(name='test-llm', inputs=inputs) - self.assertIsNotNone(response) - self.assertEqual(len(response.outputs), 2) - self.assertIn('Hello', response.outputs[0].result) - self.assertIn('How are you?', response.outputs[1].result) + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 2) + self.assertIn('Hello', response.outputs[0].result) + self.assertIn('How are you?', response.outputs[1].result) def test_conversation_alpha1_with_options(self): """Test Alpha1 conversation with various options.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ConversationInput(content='Hello with options', role='user', scrub_pii=True)] + inputs = [ConversationInput(content='Hello with options', role='user', scrub_pii=True)] - response = client.converse_alpha1( + response = self.client.converse_alpha1( name='test-llm', inputs=inputs, context_id='test-context-123', @@ -178,17 +179,16 @@ def test_conversation_alpha1_with_options(self): metadata={'test_key': 'test_value'}, ) - self.assertIsNotNone(response) - self.assertEqual(len(response.outputs), 1) - self.assertEqual(response.context_id, 'test-context-123') + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(response.context_id, 'test-context-123') def test_alpha1_parameter_conversion(self): """Test Alpha1 parameter conversion with raw Python values.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ConversationInput(content='Test with parameters', role='user')] + inputs = [ConversationInput(content='Test with parameters', role='user')] - # Test with raw Python parameters - these should be automatically converted - response = client.converse_alpha1( + # Test with raw Python parameters - these should be automatically converted + response = self.client.converse_alpha1( name='test-llm', inputs=inputs, parameters={ @@ -200,8 +200,8 @@ def test_alpha1_parameter_conversion(self): }, ) - self.assertIsNotNone(response) - self.assertEqual(len(response.outputs), 1) + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) def test_alpha1_error_handling(self): """Test Alpha1 conversation error handling.""" @@ -210,96 +210,91 @@ def test_alpha1_error_handling(self): status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha1 test error') ) - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ConversationInput(content='Error test', role='user')] + inputs = [ConversationInput(content='Error test', role='user')] - with self.assertRaises(DaprGrpcError) as context: - client.converse_alpha1(name='test-llm', inputs=inputs) + with self.assertRaises(DaprGrpcError) as context: + self.client.converse_alpha1(name='test-llm', inputs=inputs) self.assertIn('Alpha1 test error', str(context.exception)) -class ConversationAlpha2SyncTests(ConversationTestBase, unittest.TestCase): +class ConversationAlpha2SyncTests(ConversationTestBaseSync): """Synchronous Alpha2 conversation API tests.""" def test_basic_conversation_alpha2(self): """Test basic Alpha2 conversation functionality.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Hello Alpha2!') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('Hello Alpha2!') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + response = self.client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) - self.assertIsNotNone(response) - self.assertEqual(len(response.outputs), 1) - self.assertEqual(len(response.outputs[0].choices), 1) + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 1) - choice = response.outputs[0].choices[0] - self.assertEqual(choice.finish_reason, 'stop') - self.assertIn('Hello Alpha2!', choice.message.content) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'stop') + self.assertIn('Hello Alpha2!', choice.message.content) def test_conversation_alpha2_with_system_message(self): """Test Alpha2 conversation with system message.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - system_message = create_system_message('You are a helpful assistant.') - user_message = create_user_message('Hello!') + system_message = create_system_message('You are a helpful assistant.') + user_message = create_user_message('Hello!') - input_alpha2 = ConversationInputAlpha2( - messages=[system_message, user_message], scrub_pii=False - ) + input_alpha2 = ConversationInputAlpha2( + messages=[system_message, user_message], scrub_pii=False + ) - response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + response = self.client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) - self.assertIsNotNone(response) - self.assertEqual(len(response.outputs[0].choices), 2) + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs[0].choices), 2) - # Check system message response - system_choice = response.outputs[0].choices[0] - self.assertIn('System acknowledged', system_choice.message.content) + # Check system message response + system_choice = response.outputs[0].choices[0] + self.assertIn('System acknowledged', system_choice.message.content) - # Check user message response - user_choice = response.outputs[0].choices[1] - self.assertIn('Response to user', user_choice.message.content) + # Check user message response + user_choice = response.outputs[0].choices[1] + self.assertIn('Response to user', user_choice.message.content) def test_conversation_alpha2_with_options(self): """Test Alpha2 conversation with various options.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Alpha2 with options') - input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=True) + user_message = create_user_message('Alpha2 with options') + input_alpha2 = ConversationInputAlpha2(messages=[user_message], scrub_pii=True) - response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - context_id='alpha2-context-123', + response = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + context_id='alpha2-context-123', temperature=0.8, scrub_pii=True, metadata={'alpha2_test': 'true'}, tool_choice='none', ) - self.assertIsNotNone(response) - self.assertEqual(response.context_id, 'alpha2-context-123') + self.assertIsNotNone(response) + self.assertEqual(response.context_id, 'alpha2-context-123') def test_alpha2_parameter_conversion(self): """Test Alpha2 parameter conversion with various types.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Parameter conversion test') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('Parameter conversion test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - parameters={ - 'model': 'gpt-4o-mini', - 'temperature': 0.7, - 'max_tokens': 1000, - 'top_p': 1.0, - 'frequency_penalty': 0.0, - 'presence_penalty': 0.0, - 'stream': False, - }, - ) + response = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'model': 'gpt-4o-mini', + 'temperature': 0.7, + 'max_tokens': 1000, + 'top_p': 1.0, + 'frequency_penalty': 0.0, + 'presence_penalty': 0.0, + 'stream': False, + }, + ) - self.assertIsNotNone(response) + self.assertIsNotNone(response) def test_alpha2_error_handling(self): """Test Alpha2 conversation error handling.""" @@ -307,120 +302,115 @@ def test_alpha2_error_handling(self): status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha2 test error') ) - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Error test') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('Error test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - with self.assertRaises(DaprGrpcError) as context: - client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) - self.assertIn('Alpha2 test error', str(context.exception)) + with self.assertRaises(DaprGrpcError) as context: + self.client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + self.assertIn('Alpha2 test error', str(context.exception)) -class ConversationToolCallingSyncTests(ConversationTestBase, unittest.TestCase): +class ConversationToolCallingSyncTests(ConversationTestBaseSync): """Synchronous tool calling tests for Alpha2.""" def test_tool_calling_weather(self): """Test tool calling with weather tool.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = create_weather_tool() - user_message = create_user_message('What is the weather in San Francisco?') + weather_tool = create_weather_tool() + user_message = create_user_message('What is the weather in San Francisco?') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2( - name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto' - ) + response = self.client.converse_alpha2( + name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto' + ) - self.assertIsNotNone(response) - choice = response.outputs[0].choices[0] - self.assertEqual(choice.finish_reason, 'tool_calls') - self.assertEqual(len(choice.message.tool_calls), 1) + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + self.assertEqual(len(choice.message.tool_calls), 1) - tool_call = choice.message.tool_calls[0] - self.assertEqual(tool_call.function.name, 'get_weather') - self.assertIn('San Francisco', tool_call.function.arguments) + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') + self.assertIn('San Francisco', tool_call.function.arguments) def test_tool_calling_calculate(self): """Test tool calling with calculate tool.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - calc_tool = create_calculate_tool() - user_message = create_user_message('Calculate 15 * 23') + calc_tool = create_calculate_tool() + user_message = create_user_message('Calculate 15 * 23') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2( - name='test-llm', inputs=[input_alpha2], tools=[calc_tool] - ) + response = self.client.converse_alpha2( + name='test-llm', inputs=[input_alpha2], tools=[calc_tool] + ) - # Note: Our fake server only triggers weather tools, so this won't return tool calls - # but it tests that the API works with different tools - self.assertIsNotNone(response) - choice = response.outputs[0].choices[0] - self.assertIn('Calculate', choice.message.content) + # Note: Our fake server only triggers weather tools, so this won't return tool calls + # but it tests that the API works with different tools + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertIn('Calculate', choice.message.content) def test_multiple_tools(self): """Test conversation with multiple tools.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = create_weather_tool() - calc_tool = create_calculate_tool() + weather_tool = create_weather_tool() + calc_tool = create_calculate_tool() - user_message = create_user_message('I need weather and calculation help') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('I need weather and calculation help') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool, calc_tool], - tool_choice='auto', - ) + response = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool, calc_tool], + tool_choice='auto', + ) - self.assertIsNotNone(response) - # The fake server will call weather tool if "weather" is in the message - choice = response.outputs[0].choices[0] - self.assertEqual(choice.finish_reason, 'tool_calls') + self.assertIsNotNone(response) + # The fake server will call weather tool if "weather" is in the message + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') def test_tool_choice_none(self): """Test tool choice set to 'none'.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = create_weather_tool() - user_message = create_user_message('What is the weather today?') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + weather_tool = create_weather_tool() + user_message = create_user_message('What is the weather today?') - response = client.converse_alpha2( - name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='none' - ) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - self.assertIsNotNone(response) - choice = response.outputs[0].choices[0] - # With tool_choice='none', should not make tool calls even if weather is mentioned - # (though our fake server may still trigger based on content) - self.assertIsNotNone(choice.message.content) + response = self.client.converse_alpha2( + name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='none' + ) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + # With tool_choice='none', should not make tool calls even if weather is mentioned + # (though our fake server may still trigger based on content) + self.assertIsNotNone(choice.message.content) def test_tool_choice_specific(self): """Test tool choice set to specific tool name.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = create_weather_tool() - calc_tool = create_calculate_tool() + weather_tool = create_weather_tool() + calc_tool = create_calculate_tool() - user_message = create_user_message('What is the weather like?') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('What is the weather like?') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - tools=[weather_tool, calc_tool], - tool_choice='get_weather', - ) + response = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool, calc_tool], + tool_choice='get_weather', + ) - self.assertIsNotNone(response) - choice = response.outputs[0].choices[0] - if choice.finish_reason == 'tool_calls': - tool_call = choice.message.tool_calls[0] - self.assertEqual(tool_call.function.name, 'get_weather') + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + if choice.finish_reason == 'tool_calls': + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') -class ConversationMultiTurnSyncTests(ConversationTestBase, unittest.TestCase): +class ConversationMultiTurnSyncTests(ConversationTestBaseSync): """Multi-turn conversation tests for Alpha2.""" def test_multi_turn_conversation(self): @@ -510,120 +500,114 @@ def test_conversation_context_continuity(self): self.assertIsNotNone(response2.outputs[0].choices[0].message.content) -class ConversationAsyncTests(ConversationTestBase, unittest.IsolatedAsyncioTestCase): +class ConversationAsyncTests(ConversationTestBaseAsync): """Asynchronous conversation API tests.""" async def test_basic_async_conversation_alpha1(self): """Test basic async Alpha1 conversation.""" - async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ - ConversationInput(content='Hello async', role='user'), - ConversationInput(content='How are you async?', role='user'), - ] + inputs = [ + ConversationInput(content='Hello async', role='user'), + ConversationInput(content='How are you async?', role='user'), + ] - response = await client.converse_alpha1(name='test-llm', inputs=inputs) + response = await self.client.converse_alpha1(name='test-llm', inputs=inputs) - self.assertIsNotNone(response) - self.assertEqual(len(response.outputs), 2) - self.assertIn('Hello async', response.outputs[0].result) + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 2) + self.assertIn('Hello async', response.outputs[0].result) async def test_basic_async_conversation_alpha2(self): """Test basic async Alpha2 conversation.""" - async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Hello async Alpha2!') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('Hello async Alpha2!') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = await client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + response = await self.client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) - self.assertIsNotNone(response) - choice = response.outputs[0].choices[0] - self.assertIn('Hello async Alpha2!', choice.message.content) + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertIn('Hello async Alpha2!', choice.message.content) async def test_async_tool_calling(self): """Test async tool calling.""" - async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - weather_tool = create_weather_tool() - user_message = create_user_message('Async weather request for London') + weather_tool = create_weather_tool() + user_message = create_user_message('Async weather request for London') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = await client.converse_alpha2( - name='test-llm', inputs=[input_alpha2], tools=[weather_tool] - ) + response = await self.client.converse_alpha2( + name='test-llm', inputs=[input_alpha2], tools=[weather_tool] + ) - self.assertIsNotNone(response) - choice = response.outputs[0].choices[0] - self.assertEqual(choice.finish_reason, 'tool_calls') - tool_call = choice.message.tool_calls[0] - self.assertEqual(tool_call.function.name, 'get_weather') + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') async def test_concurrent_async_conversations(self): """Test multiple concurrent async conversations.""" - async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - - async def run_alpha1_conversation(message, session_id): - inputs = [ConversationInput(content=message, role='user')] - response = await client.converse_alpha1( + async def run_alpha1_conversation(message, session_id): + inputs = [ConversationInput(content=message, role='user')] + response = await self.client.converse_alpha1( name='test-llm', inputs=inputs, context_id=session_id ) - return response.outputs[0].result + return response.outputs[0].result - async def run_alpha2_conversation(message, session_id): - user_message = create_user_message(message) - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = await client.converse_alpha2( + async def run_alpha2_conversation(message, session_id): + user_message = create_user_message(message) + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + response = await self.client.converse_alpha2( name='test-llm', inputs=[input_alpha2], context_id=session_id ) - return response.outputs[0].choices[0].message.content + return response.outputs[0].choices[0].message.content - # Run concurrent conversations with both Alpha1 and Alpha2 - tasks = [ - run_alpha1_conversation('First Alpha1 message', 'concurrent-alpha1'), - run_alpha2_conversation('First Alpha2 message', 'concurrent-alpha2'), - run_alpha1_conversation('Second Alpha1 message', 'concurrent-alpha1-2'), - run_alpha2_conversation('Second Alpha2 message', 'concurrent-alpha2-2'), - ] + # Run concurrent conversations with both Alpha1 and Alpha2 + tasks = [ + run_alpha1_conversation('First Alpha1 message', 'concurrent-alpha1'), + run_alpha2_conversation('First Alpha2 message', 'concurrent-alpha2'), + run_alpha1_conversation('Second Alpha1 message', 'concurrent-alpha1-2'), + run_alpha2_conversation('Second Alpha2 message', 'concurrent-alpha2-2'), + ] - results = await asyncio.gather(*tasks) + results = await asyncio.gather(*tasks) - self.assertEqual(len(results), 4) - for result in results: - self.assertIsNotNone(result) - self.assertIsInstance(result, str) + self.assertEqual(len(results), 4) + for result in results: + self.assertIsNotNone(result) + self.assertIsInstance(result, str) async def test_async_multi_turn_with_tools(self): """Test async multi-turn conversation with tool calling.""" - async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - # First turn: user asks for weather - weather_tool = create_weather_tool() - user_message = create_user_message('Async weather for Paris') - input1 = ConversationInputAlpha2(messages=[user_message]) - - response1 = await client.converse_alpha2( - name='test-llm', - inputs=[input1], - tools=[weather_tool], - context_id='async-multi-turn', - ) + # First turn: user asks for weather + weather_tool = create_weather_tool() + user_message = create_user_message('Async weather for Paris') + input1 = ConversationInputAlpha2(messages=[user_message]) + + response1 = await self.client.converse_alpha2( + name='test-llm', + inputs=[input1], + tools=[weather_tool], + context_id='async-multi-turn', + ) - # Should get tool call - self.assertEqual(response1.outputs[0].choices[0].finish_reason, 'tool_calls') - tool_call = response1.outputs[0].choices[0].message.tool_calls[0] + # Should get tool call + self.assertEqual(response1.outputs[0].choices[0].finish_reason, 'tool_calls') + tool_call = response1.outputs[0].choices[0].message.tool_calls[0] - # Second turn: provide tool result - tool_result_message = create_tool_message( - tool_id=tool_call.id, - name='get_weather', - content='{"temperature": 22, "condition": "sunny"}', - ) - input2 = ConversationInputAlpha2(messages=[tool_result_message]) + # Second turn: provide tool result + tool_result_message = create_tool_message( + tool_id=tool_call.id, + name='get_weather', + content='{"temperature": 22, "condition": "sunny"}', + ) + input2 = ConversationInputAlpha2(messages=[tool_result_message]) - response2 = await client.converse_alpha2( - name='test-llm', inputs=[input2], context_id='async-multi-turn' - ) + response2 = await self.client.converse_alpha2( + name='test-llm', inputs=[input2], context_id='async-multi-turn' + ) - self.assertIsNotNone(response2) - self.assertIn('Tool result processed', response2.outputs[0].choices[0].message.content) + self.assertIsNotNone(response2) + self.assertIn('Tool result processed', response2.outputs[0].choices[0].message.content) async def test_async_error_handling(self): """Test async conversation error handling.""" @@ -631,124 +615,117 @@ async def test_async_error_handling(self): status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Async test error') ) - async with AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - inputs = [ConversationInput(content='Async error test', role='user')] + inputs = [ConversationInput(content='Async error test', role='user')] - with self.assertRaises(DaprGrpcError) as context: - await client.converse_alpha1(name='test-llm', inputs=inputs) - self.assertIn('Async test error', str(context.exception)) + with self.assertRaises(DaprGrpcError) as context: + await self.client.converse_alpha1(name='test-llm', inputs=inputs) + self.assertIn('Async test error', str(context.exception)) -class ConversationParameterTests(ConversationTestBase, unittest.TestCase): +class ConversationParameterTests(ConversationTestBaseSync): """Tests for parameter handling and conversion.""" def test_parameter_edge_cases(self): """Test parameter conversion with edge cases.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Edge cases test') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('Edge cases test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - response = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - parameters={ - 'int32_max': 2147483647, # Int32 maximum - 'int64_large': 9999999999, # Requires Int64 - 'negative_temp': -0.5, # Negative float - 'zero_value': 0, # Zero integer - 'false_flag': False, # Boolean false - 'true_flag': True, # Boolean true - 'empty_string': '', # Empty string - }, - ) + response = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'int32_max': 2147483647, # Int32 maximum + 'int64_large': 9999999999, # Requires Int64 + 'negative_temp': -0.5, # Negative float + 'zero_value': 0, # Zero integer + 'false_flag': False, # Boolean false + 'true_flag': True, # Boolean true + 'empty_string': '', # Empty string + }, + ) - self.assertIsNotNone(response) + self.assertIsNotNone(response) def test_realistic_provider_parameters(self): """Test with realistic LLM provider parameters.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - user_message = create_user_message('Provider parameters test') - input_alpha2 = ConversationInputAlpha2(messages=[user_message]) + user_message = create_user_message('Provider parameters test') + input_alpha2 = ConversationInputAlpha2(messages=[user_message]) # OpenAI-style parameters - response1 = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], + response1 = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], parameters={ - 'model': 'gpt-4o-mini', - 'temperature': 0.7, - 'max_tokens': 1000, - 'top_p': 1.0, - 'frequency_penalty': 0.0, - 'presence_penalty': 0.0, - 'stream': False, - 'tool_choice': 'auto', - }, - ) + 'model': 'gpt-4o-mini', + 'temperature': 0.7, + 'max_tokens': 1000, + 'top_p': 1.0, + 'frequency_penalty': 0.0, + 'presence_penalty': 0.0, + 'stream': False, + 'tool_choice': 'auto', + }, + ) # Anthropic-style parameters - response2 = client.converse_alpha2( - name='test-llm', - inputs=[input_alpha2], - parameters={ - 'model': 'claude-3-5-sonnet-20241022', - 'max_tokens': 4096, - 'temperature': 0.8, - 'top_p': 0.9, - 'top_k': 250, - 'stream': False, - }, - ) + response2 = self.client.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + parameters={ + 'model': 'claude-3-5-sonnet-20241022', + 'max_tokens': 4096, + 'temperature': 0.8, + 'top_p': 0.9, + 'top_k': 250, + 'stream': False, + }, + ) - self.assertIsNotNone(response1) - self.assertIsNotNone(response2) + self.assertIsNotNone(response1) + self.assertIsNotNone(response2) -class ConversationValidationTests(ConversationTestBase, unittest.TestCase): +class ConversationValidationTests(ConversationTestBaseSync): """Tests for input validation and edge cases.""" def test_empty_inputs_alpha1(self): """Test Alpha1 with empty inputs.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - response = client.converse_alpha1(name='test-llm', inputs=[]) - self.assertIsNotNone(response) + response = self.client.converse_alpha1(name='test-llm', inputs=[]) + self.assertIsNotNone(response) def test_empty_inputs_alpha2(self): """Test Alpha2 with empty inputs.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - response = client.converse_alpha2(name='test-llm', inputs=[]) - self.assertIsNotNone(response) + response = self.client.converse_alpha2(name='test-llm', inputs=[]) + self.assertIsNotNone(response) def test_empty_messages_alpha2(self): """Test Alpha2 with empty messages in input.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - input_alpha2 = ConversationInputAlpha2(messages=[]) - response = client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) - self.assertIsNotNone(response) + input_alpha2 = ConversationInputAlpha2(messages=[]) + response = self.client.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + self.assertIsNotNone(response) def test_mixed_alpha1_alpha2_compatibility(self): """Test that Alpha1 and Alpha2 can be used in the same session.""" - with DaprClient(f'{self.scheme}localhost:{self.grpc_port}') as client: - # Alpha1 call - alpha1_inputs = [ConversationInput(content='Alpha1 call', role='user')] - alpha1_response = client.converse_alpha1(name='test-llm', inputs=alpha1_inputs) + # Alpha1 call + alpha1_inputs = [ConversationInput(content='Alpha1 call', role='user')] + alpha1_response = self.client.converse_alpha1(name='test-llm', inputs=alpha1_inputs) - # Alpha2 call - user_message = create_user_message('Alpha2 call') - alpha2_input = ConversationInputAlpha2(messages=[user_message]) - alpha2_response = client.converse_alpha2(name='test-llm', inputs=[alpha2_input]) + # Alpha2 call + user_message = create_user_message('Alpha2 call') + alpha2_input = ConversationInputAlpha2(messages=[user_message]) + alpha2_response = self.client.converse_alpha2(name='test-llm', inputs=[alpha2_input]) - # Both should work - self.assertIsNotNone(alpha1_response) - self.assertIsNotNone(alpha2_response) + # Both should work + self.assertIsNotNone(alpha1_response) + self.assertIsNotNone(alpha2_response) - # Check response structures are different but valid - self.assertTrue(hasattr(alpha1_response, 'outputs')) - self.assertTrue(hasattr(alpha2_response, 'outputs')) - self.assertTrue(hasattr(alpha2_response.outputs[0], 'choices')) + # Check response structures are different but valid + self.assertTrue(hasattr(alpha1_response, 'outputs')) + self.assertTrue(hasattr(alpha2_response, 'outputs')) + self.assertTrue(hasattr(alpha2_response.outputs[0], 'choices')) -class ConversationToolHelpersSyncTests(unittest.TestCase): +class ConversationToolHelpersSyncTests(ConversationTestBaseSync): """Tests for conversation tool helpers, registry, and backends (sync).""" def tearDown(self): @@ -917,7 +894,7 @@ def test_message_helpers_and_to_proto(self): self.assertEqual(proto_tool.of_tool.content[0].text, 'cloudy') -class ConversationToolHelpersAsyncTests(unittest.IsolatedAsyncioTestCase): +class ConversationToolHelpersAsyncTests(ConversationTestBaseAsync): async def asyncTearDown(self): for t in list(get_registered_tools()): try: From d0703aea25432c2b677cf9ad7b66bbe0765e40a7 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:11:01 -0500 Subject: [PATCH 11/29] lint Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/conversation.py | 14 +++---- tests/clients/test_conversation.py | 62 ++++++++++++++++-------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 17716133..4822f881 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -17,7 +17,7 @@ import inspect import json from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Mapping, Optional, Protocol, Sequence, Union +from typing import Any, Callable, Dict, List, Mapping, Optional, Protocol, Sequence, Union, cast from dapr.clients.grpc import _conversation_helpers as conv_helpers from dapr.proto import api_v1 @@ -342,9 +342,7 @@ async def ainvoke( loop = asyncio.get_running_loop() return await loop.run_in_executor(None, lambda: self.func(*bound.args, **bound.kwargs)) except asyncio.TimeoutError as err: - raise conv_helpers.ToolExecutionError( - f'Timed out after {timeout} seconds' - ) from err + raise conv_helpers.ToolExecutionError(f'Timed out after {timeout} seconds') from err except Exception as e: raise conv_helpers.ToolExecutionError(f'Tool raised: {e}') from e @@ -375,8 +373,8 @@ def _decorate(f: Callable): ct = ConversationTools(function=ctf, backend=FunctionBackend(f)) - # Store the tool in the function for later retrieval - f.__dapr_conversation_tool__ = ct + # Store the tool in the function for later retrieval (mypy-safe without setattr) + cast(Any, f).__dapr_conversation_tool__ = ct if register: register_tool(ctf.name, ct) @@ -430,9 +428,7 @@ def _get_tool(name: str) -> ConversationTools: try: return _TOOL_REGISTRY[name] except KeyError as err: - raise conv_helpers.ToolNotFoundError( - f"Tool '{name}' is not registered" - ) from err + raise conv_helpers.ToolNotFoundError(f"Tool '{name}' is not registered") from err def execute_registered_tool(name: str, params: Union[Params, str] = None) -> Any: diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index d504082d..7e79800a 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -127,6 +127,7 @@ def setUpClass(cls): def tearDownClass(cls): cls._fake_dapr_server.stop() + class ConversationTestBaseSync(ConversationTestBase, unittest.TestCase): """Base class for conversation tests with common setup.""" @@ -141,6 +142,7 @@ def tearDown(self): class ConversationTestBaseAsync(ConversationTestBase, unittest.IsolatedAsyncioTestCase): """Base class for conversation tests with common setup.""" + async def asyncSetUp(self): await super().asyncSetUp() self.client = AsyncDaprClient(f'{self.scheme}localhost:{self.grpc_port}') @@ -149,6 +151,7 @@ async def asyncTearDown(self): await super().asyncTearDown() await self.client.close() + class ConversationAlpha1SyncTests(ConversationTestBaseSync): """Synchronous Alpha1 conversation API tests.""" @@ -171,13 +174,13 @@ def test_conversation_alpha1_with_options(self): inputs = [ConversationInput(content='Hello with options', role='user', scrub_pii=True)] response = self.client.converse_alpha1( - name='test-llm', - inputs=inputs, - context_id='test-context-123', - temperature=0.7, - scrub_pii=True, - metadata={'test_key': 'test_value'}, - ) + name='test-llm', + inputs=inputs, + context_id='test-context-123', + temperature=0.7, + scrub_pii=True, + metadata={'test_key': 'test_value'}, + ) self.assertIsNotNone(response) self.assertEqual(len(response.outputs), 1) @@ -189,16 +192,16 @@ def test_alpha1_parameter_conversion(self): # Test with raw Python parameters - these should be automatically converted response = self.client.converse_alpha1( - name='test-llm', - inputs=inputs, - parameters={ - 'temperature': 0.7, - 'max_tokens': 1000, - 'top_p': 0.9, - 'frequency_penalty': 0.0, - 'presence_penalty': 0.0, - }, - ) + name='test-llm', + inputs=inputs, + parameters={ + 'temperature': 0.7, + 'max_tokens': 1000, + 'top_p': 0.9, + 'frequency_penalty': 0.0, + 'presence_penalty': 0.0, + }, + ) self.assertIsNotNone(response) self.assertEqual(len(response.outputs), 1) @@ -266,11 +269,11 @@ def test_conversation_alpha2_with_options(self): name='test-llm', inputs=[input_alpha2], context_id='alpha2-context-123', - temperature=0.8, - scrub_pii=True, - metadata={'alpha2_test': 'true'}, - tool_choice='none', - ) + temperature=0.8, + scrub_pii=True, + metadata={'alpha2_test': 'true'}, + tool_choice='none', + ) self.assertIsNotNone(response) self.assertEqual(response.context_id, 'alpha2-context-123') @@ -546,19 +549,20 @@ async def test_async_tool_calling(self): async def test_concurrent_async_conversations(self): """Test multiple concurrent async conversations.""" + async def run_alpha1_conversation(message, session_id): inputs = [ConversationInput(content=message, role='user')] response = await self.client.converse_alpha1( - name='test-llm', inputs=inputs, context_id=session_id - ) + name='test-llm', inputs=inputs, context_id=session_id + ) return response.outputs[0].result async def run_alpha2_conversation(message, session_id): user_message = create_user_message(message) input_alpha2 = ConversationInputAlpha2(messages=[user_message]) response = await self.client.converse_alpha2( - name='test-llm', inputs=[input_alpha2], context_id=session_id - ) + name='test-llm', inputs=[input_alpha2], context_id=session_id + ) return response.outputs[0].choices[0].message.content # Run concurrent conversations with both Alpha1 and Alpha2 @@ -651,11 +655,11 @@ def test_realistic_provider_parameters(self): user_message = create_user_message('Provider parameters test') input_alpha2 = ConversationInputAlpha2(messages=[user_message]) - # OpenAI-style parameters + # OpenAI-style parameters response1 = self.client.converse_alpha2( name='test-llm', inputs=[input_alpha2], - parameters={ + parameters={ 'model': 'gpt-4o-mini', 'temperature': 0.7, 'max_tokens': 1000, @@ -667,7 +671,7 @@ def test_realistic_provider_parameters(self): }, ) - # Anthropic-style parameters + # Anthropic-style parameters response2 = self.client.converse_alpha2( name='test-llm', inputs=[input_alpha2], From e10d2d8b1a0eaa52d9985e473b192f6b8147c087 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Sat, 16 Aug 2025 09:11:41 -0500 Subject: [PATCH 12/29] move conversation classes in _response module to conversation module. Some example README refactor/lint Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 8 +- dapr/clients/grpc/_response.py | 82 ------------- dapr/clients/grpc/client.py | 40 +++--- dapr/clients/grpc/conversation.py | 108 ++++++++++++++-- examples/conversation/README.md | 115 ++++++++++-------- .../real_llm_providers_example.py | 12 +- 6 files changed, 187 insertions(+), 178 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index e133f182..1b749fbe 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -85,12 +85,6 @@ from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( BindingResponse, - ConversationResponse, - ConversationResult, - ConversationResponseAlpha2, - ConversationResultAlpha2, - ConversationResultAlpha2Choices, - ConversationResultAlpha2Message, DaprResponse, GetSecretResponse, GetBulkSecretResponse, @@ -111,6 +105,8 @@ StartWorkflowResponse, TopicEventResponse, ) +from dapr.clients.grpc.conversation import ConversationResult, ConversationResultAlpha2Message, \ + ConversationResultAlpha2Choices, ConversationResultAlpha2, ConversationResponse, ConversationResponseAlpha2 class DaprGrpcClientAsync: diff --git a/dapr/clients/grpc/_response.py b/dapr/clients/grpc/_response.py index 2197ee15..fff511ff 100644 --- a/dapr/clients/grpc/_response.py +++ b/dapr/clients/grpc/_response.py @@ -18,7 +18,6 @@ import contextlib import json import threading -from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import ( @@ -51,7 +50,6 @@ WorkflowRuntimeStatus, ) from dapr.proto import api_service_v1, api_v1, appcallback_v1, common_v1 -from dapr.clients.grpc import conversation # Avoid circular import dependency by only importing DaprGrpcClient # for type checking @@ -1073,83 +1071,3 @@ class EncryptResponse(CryptoResponse[TCryptoResponse]): class DecryptResponse(CryptoResponse[TCryptoResponse]): ... - - -@dataclass -class ConversationResult: - """Result from a single conversation input.""" - - result: str - parameters: Dict[str, GrpcAny] = field(default_factory=dict) - - -@dataclass -class ConversationResultAlpha2Message: - """Message content in conversation result.""" - - content: str - tool_calls: List[conversation.ConversationToolCalls] = field(default_factory=list) - - -@dataclass -class ConversationResultAlpha2Choices: - """Choice in Alpha2 conversation result.""" - - finish_reason: str - index: int - message: ConversationResultAlpha2Message - - -@dataclass -class ConversationResultAlpha2: - """Alpha2 result from conversation input.""" - - choices: List[ConversationResultAlpha2Choices] = field(default_factory=list) - - -@dataclass -class ConversationResponse: - """Response from the conversation API.""" - - context_id: Optional[str] - outputs: List[ConversationResult] - - -@dataclass -class ConversationResponseAlpha2: - """Alpha2 response from the conversation API.""" - - context_id: Optional[str] - outputs: List[ConversationResultAlpha2] - - def to_assistant_messages(self) -> List[conversation.ConversationMessage]: - def convert_llm_response_to_conversation_input( - result_message: ConversationResultAlpha2Message, - ) -> conversation.ConversationMessage: - """Convert ConversationResultMessage (from LLM response) to ConversationMessage.""" - - # Convert content string to ConversationMessageContent list - content = [] - if result_message.content: - content = [conversation.ConversationMessageContent(text=result_message.content)] - - # Convert tool_calls if present (they're already the right type) - tool_calls = result_message.tool_calls or [] - - # Create assistant message (since LLM responses are always assistant messages) - return conversation.ConversationMessage( - of_assistant=conversation.ConversationMessageOfAssistant( - content=content, tool_calls=tool_calls - ) - ) - - """Convert the outputs to a list of ConversationInput.""" - assistant_messages = [] - - for output in self.outputs or []: - for choice in output.choices or []: - # Convert and collect all assistant messages - assistant_message = convert_llm_response_to_conversation_input(choice.message) - assistant_messages.append(assistant_message) - - return assistant_messages diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 4f2cb0f8..8429ac5b 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -70,13 +70,7 @@ EncryptRequestIterator, DecryptRequestIterator, ) -from dapr.clients.grpc.conversation import ( - ConversationInput, - ConversationToolCallsOfFunction, - ConversationToolCalls, - ConversationInputAlpha2, - ConversationTools, -) +from dapr.clients.grpc import conversation from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._response import ( BindingResponse, @@ -101,12 +95,6 @@ EncryptResponse, DecryptResponse, TopicEventResponse, - ConversationResponse, - ConversationResult, - ConversationResponseAlpha2, - ConversationResultAlpha2, - ConversationResultAlpha2Choices, - ConversationResultAlpha2Message, ) @@ -1739,14 +1727,14 @@ def purge_workflow(self, instance_id: str, workflow_component: str) -> DaprRespo def converse_alpha1( self, name: str, - inputs: List[ConversationInput], + inputs: List[conversation.ConversationInput], *, context_id: Optional[str] = None, parameters: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, - ) -> ConversationResponse: + ) -> conversation.ConversationResponse: """Invoke an LLM using the conversation API (Alpha). Args: @@ -1787,27 +1775,27 @@ def converse_alpha1( response, call = self.retry_policy.run_rpc(self._stub.ConverseAlpha1.with_call, request) outputs = [ - ConversationResult(result=output.result, parameters=output.parameters) + conversation.ConversationResult(result=output.result, parameters=output.parameters) for output in response.outputs ] - return ConversationResponse(context_id=response.contextID, outputs=outputs) + return conversation.ConversationResponse(context_id=response.contextID, outputs=outputs) except RpcError as err: raise DaprGrpcError(err) from err def converse_alpha2( self, name: str, - inputs: List[ConversationInputAlpha2], + inputs: List[conversation.ConversationInputAlpha2], *, context_id: Optional[str] = None, parameters: Optional[Dict[str, Union[GrpcAny, Any]]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, - tools: Optional[List[ConversationTools]] = None, + tools: Optional[List[conversation.ConversationTools]] = None, tool_choice: Optional[str] = None, - ) -> ConversationResponseAlpha2: + ) -> conversation.ConversationResponseAlpha2: """Invoke an LLM using the conversation API (Alpha2) with tool calling support. Args: @@ -1888,30 +1876,30 @@ def converse_alpha2( # Convert tool calls from response tool_calls = [] for tool_call in choice.message.tool_calls: - function_call = ConversationToolCallsOfFunction( + function_call = conversation.ConversationToolCallsOfFunction( name=tool_call.function.name, arguments=tool_call.function.arguments ) if not tool_call.id: tool_call.id = _generate_unique_tool_call_id() tool_calls.append( - ConversationToolCalls(id=tool_call.id, function=function_call) + conversation.ConversationToolCalls(id=tool_call.id, function=function_call) ) - result_message = ConversationResultAlpha2Message( + result_message = conversation.ConversationResultAlpha2Message( content=choice.message.content, tool_calls=tool_calls ) choices.append( - ConversationResultAlpha2Choices( + conversation.ConversationResultAlpha2Choices( finish_reason=choice.finish_reason, index=choice.index, message=result_message, ) ) - outputs.append(ConversationResultAlpha2(choices=choices)) + outputs.append(conversation.ConversationResultAlpha2(choices=choices)) - return ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) + return conversation.ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) except RpcError as err: raise DaprGrpcError(err) from err diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 4822f881..42f7c0be 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +from __future__ import annotations import asyncio import inspect @@ -19,11 +20,17 @@ from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Mapping, Optional, Protocol, Sequence, Union, cast +from google.protobuf.any_pb2 import Any as GrpcAny + from dapr.clients.grpc import _conversation_helpers as conv_helpers from dapr.proto import api_v1 Params = Union[Mapping[str, Any], Sequence[Any], None] +# ------------------------------------------------------------------------------------------------ +# Request Classes +# ------------------------------------------------------------------------------------------------ + @dataclass class ConversationInput: @@ -126,11 +133,7 @@ def trace_print(self, indent: int = 0) -> None: if self.name: print(f'{base}name: {self.name}') for i, c in enumerate(self.content): - lines = c.text.splitlines() if c.text is not None else [''] - first = lines[0] if lines else '' - print(f'{base}content[{i}]: {first}') - for extra in lines[1:]: - print(extra) + print(_indent_lines(f'content[{i}]', c.text, indent)) if self.tool_calls: print(f'{base}tool_calls: {len(self.tool_calls)}') for idx, tc in enumerate(self.tool_calls): @@ -176,19 +179,19 @@ def trace_print(self, indent: int = 0): print() """Print the conversation message with indentation and direction arrows.""" if self.of_developer: - print(f'{" " * indent}client[developer] ---------> LLM[assistant]:') + print(f'{" " * indent}client[devel] --------------> LLM[assistant]:') self.of_developer.trace_print(indent + 2) if self.of_system: - print(f'{" " * indent}client[system] ---------> LLM[assistant]:') + print(f'{" " * indent}client[system] --------------> LLM[assistant]:') self.of_system.trace_print(indent + 2) if self.of_user: - print(f'{" " * indent}client[user] ---------> LLM[assistant]:') + print(f'{" " * indent}client[user] --------------> LLM[assistant]:') self.of_user.trace_print(indent + 2) if self.of_assistant: - print(f'{" " * indent}client <-------- LLM[assistant]:') + print(f'{" " * indent}client <------------- LLM[assistant]:') self.of_assistant.trace_print(indent + 2) if self.of_tool: - print(f'{" " * indent}client[tool] --------> LLM[assistant]:') + print(f'{" " * indent}client[tool] -------------> LLM[assistant]:') self.of_tool.trace_print(indent + 2) def to_proto(self) -> api_v1.ConversationMessage: @@ -296,6 +299,91 @@ def from_function(cls, func: Callable, register: bool = True) -> 'ConversationTo register_tool(c.name, ConversationTools(function=c, backend=FunctionBackend(func))) return c +# ------------------------------------------------------------------------------------------------ +# Response Classes +# ------------------------------------------------------------------------------------------------ + + +@dataclass +class ConversationResult: + """One of the outputs to a request to the conversation API.""" + + result: str + parameters: Dict[str, GrpcAny] = field(default_factory=dict) + + +@dataclass +class ConversationResultAlpha2Message: + """Message content in one conversation result choice.""" + + content: str + tool_calls: List[ConversationToolCalls] = field(default_factory=list) + + +@dataclass +class ConversationResultAlpha2Choices: + """Choice in one Alpha2 conversation result output.""" + + finish_reason: str + index: int + message: ConversationResultAlpha2Message + + +@dataclass +class ConversationResultAlpha2: + """One of the outputs in Alpha2 response from conversation input.""" + + choices: List[ConversationResultAlpha2Choices] = field(default_factory=list) + + +@dataclass +class ConversationResponse: + """Response to a request from the conversation API.""" + + context_id: Optional[str] + outputs: List[ConversationResult] + + +@dataclass +class ConversationResponseAlpha2: + """Alpha2 response to a request from the conversation API.""" + + context_id: Optional[str] + outputs: List[ConversationResultAlpha2] + + def to_assistant_messages(self) -> List[ConversationMessage]: + """Helper to convert to Assistant messages and makes it easy to use in multi-turn conversations.""" + + def convert_llm_response_to_conversation_input( + result_message: ConversationResultAlpha2Message, + ) -> ConversationMessage: + """Convert ConversationResultMessage (from LLM response) to ConversationMessage.""" + + # Convert content string to ConversationMessageContent list + content = [] + if result_message.content: + content = [ConversationMessageContent(text=(result_message.content))] + + # Convert tool_calls if present (they're already the right type) + tool_calls = result_message.tool_calls or [] + + # Create an assistant message (since LLM responses are always assistant messages) + return ConversationMessage( + of_assistant=ConversationMessageOfAssistant( + content=content, tool_calls=tool_calls + ) + ) + + """Convert the outputs to a list of ConversationInput.""" + assistant_messages = [] + + for output in self.outputs or []: + for choice in output.choices or []: + # Convert and collect all assistant messages + assistant_message = convert_llm_response_to_conversation_input(choice.message) + assistant_messages.append(assistant_message) + + return assistant_messages # ------------------------------------------------------------------------------------------------ # Tool Helpers diff --git a/examples/conversation/README.md b/examples/conversation/README.md index d7d88c10..1fb6e249 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -18,22 +18,8 @@ The Conversation API supports real LLM providers including: ```bash pip install python-dotenv # For .env file support ``` - -2. **Create .env file:** - ```bash - cp .env.example .env - ``` - -3. **Add your API keys to .env:** - ```bash - OPENAI_API_KEY=your_openai_key_here - ANTHROPIC_API_KEY=your_anthropic_key_here - MISTRAL_API_KEY=your_mistral_key_here - DEEPSEEK_API_KEY=your_deepseek_key_here - GOOGLE_API_KEY=your_google_ai_key_here - ``` - -4. **Run the simple conversation on the Alpha V1 version (dapr 1.15)** + +2. **Run the simple conversation on the Alpha V1 version (dapr 1.15)** -5. **Run the simple conversation on the Alpha V2 version (dapr 1.16)** +3. **Run the simple conversation on the Alpha V2 version (dapr 1.16)** -6. **Run the comprehensive example with real LLM providers (This requires LLMAPI Keys)** +4. **Run the comprehensive example with real LLM providers (This requires LLMAPI Keys)** + + To run the comprehensive example with real LLM providers, you need to have at least one of the following LLM providers API keys: - ```bash - python examples/conversation/real_llm_providers_example.py - ``` + - OpenAI + - Anthropic + - Mistral + - Deepseek + - Google AI + + **Create .env file:** + + We use the python-dotenv package to load environment variables from a .env file, so we need to create one first. + If you don't have an .env file, you can copy the .env.example file and rename it to .env: + ```bash + cp .env.example .env + ``` + + **Add your API keys to .env:** + + Open the .env file and add your API keys for the providers you want to use. For example: + ```bash + OPENAI_API_KEY=your_openai_key_here + ANTHROPIC_API_KEY=your_anthropic_key_here + MISTRAL_API_KEY=your_mistral_key_here + DEEPSEEK_API_KEY=your_deepseek_key_here + GOOGLE_API_KEY=your_google_ai_key_here + ``` + Run the example: + + ```bash + python examples/conversation/real_llm_providers_example.py + ``` Depending on what API key you have, this will run and print the result of each test function in the example file. @@ -339,14 +353,13 @@ Alpha2 excels at multi-turn conversations with proper context accumulation: ```python from dapr.clients.grpc import conversation -from dapr.clients.grpc._response import ConversationResultAlpha2 -conversation_history: list[conversation.ConversationMessage] = [] +conversation_history: list[conversation.ConversationMessage] = [ + conversation.create_user_message("What's the weather in SF?")] -# Turn 1: User asks question -conversation_history.append(conversation.create_user_message("What's the weather in SF?")) +# Turn 1: User asks a question -response1: ConversationResultAlpha2 = client.converse_alpha2( +response1: conversation.ConversationResponseAlpha2 = client.converse_alpha2( name="openai", inputs=[conversation.ConversationInputAlpha2(messages=conversation_history)], tools=conversation.get_registered_tools(), @@ -389,41 +402,47 @@ That will print the conversation history with the following output (might vary d ``` Full conversation history trace: - client[user] ---------> LLM[assistant]: + client[user] --------------> LLM[assistant]: content[0]: What's the weather like in San Francisco? Use one of the tools available. - client <-------- LLM[assistant]: + client <------------- LLM[assistant]: + content[0]: I'll check the current weather in San Francisco for you. + + client <------------- LLM[assistant]: tool_calls: 1 - [0] id=call_23YKsQyzRhQxcNjjRNRhMmze function=get_weather({"location":"San Francisco"}) + [0] id=toolu_01TJSATPrtE4uL9GcpJJDKEY function=get_weather({"location":"San Francisco"}) - client[tool] --------> LLM[assistant]: - tool_id: call_23YKsQyzRhQxcNjjRNRhMmze + client[tool] -------------> LLM[assistant]: + tool_id: toolu_01TJSATPrtE4uL9GcpJJDKEY name: get_weather content[0]: The weather in San Francisco is sunny with a temperature of 72°F. - client <-------- LLM[assistant]: - content[0]: The weather in San Francisco is sunny with a temperature of 72°F. + client <------------- LLM[assistant]: + content[0]: The weather in San Francisco is currently sunny with a temperature of 72°F. It's a beautiful day there! - client[user] ---------> LLM[assistant]: + client[user] --------------> LLM[assistant]: content[0]: Should I bring an umbrella? Also, what about the weather in New York? - client <-------- LLM[assistant]: - tool_calls: 2 - [0] id=call_eWXkLbxKOAZRPoaAK0siYwHx function=get_weather({"location": "New York"}) - [1] id=call_CnKVzPmbCUEPZqipoemMF5jr function=get_weather({"location": "San Francisco"}) + client <------------- LLM[assistant]: + content[0]: Let me check the weather in New York for you to help answer both questions. - client[tool] --------> LLM[assistant]: - tool_id: call_eWXkLbxKOAZRPoaAK0siYwHx - name: get_weather - content[0]: The weather in New York is sunny with a temperature of 72°F. + client <------------- LLM[assistant]: + tool_calls: 1 + [0] id=toolu_01DqngeKSXhgqbn128NC4J1o function=get_weather({"location":"New York"}) - client[tool] --------> LLM[assistant]: - tool_id: call_CnKVzPmbCUEPZqipoemMF5jr + client[tool] -------------> LLM[assistant]: + tool_id: toolu_01DqngeKSXhgqbn128NC4J1o name: get_weather - content[0]: The weather in San Francisco is sunny with a temperature of 72°F. + content[0]: The weather in New York is sunny with a temperature of 72°F. - client <-------- LLM[assistant]: - content[0]: The weather in both San Francisco and New York is sunny with a temperature of 72°F. Since it's sunny in both locations, you likely won't need to bring an umbrella. + client <------------- LLM[assistant]: + content[0]: Based on the weather information: + + **San Francisco**: Sunny, 72°F - No need for an umbrella there! + + **New York**: Also sunny, 72°F - No umbrella needed here either. + + Both cities are having beautiful, sunny weather today, so you shouldn't need an umbrella for either location. Perfect weather for being outdoors! ``` @@ -539,7 +558,7 @@ dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resour - Ensure LLM provider supports function calling 3. **Multi-turn context issues** - - Use `convert_llm_response_to_conversation_message()` helper function + - Use `to_assistant_message()` helper function - Maintain conversation history across turns - Include all previous messages in subsequent requests diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index 57c56b1d..efcf872b 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -52,9 +52,6 @@ import yaml -if TYPE_CHECKING: - from dapr.clients.grpc._response import ConversationResultAlpha2Message - # Add the parent directory to the path so we can import local dapr sdk # uncomment if running from development version sys.path.insert(0, str(Path(__file__).parent.parent.parent)) @@ -248,7 +245,7 @@ def execute_weather_tool(location: str, unit: str = 'fahrenheit') -> str: def convert_llm_response_to_conversation_input( - result_message: 'ConversationResultAlpha2Message', + result_message: conversation.ConversationResultAlpha2Message, ) -> conversation.ConversationMessage: """Convert ConversationResultMessage (from LLM response) to ConversationMessage (for conversation input). @@ -262,15 +259,18 @@ def convert_llm_response_to_conversation_input( ConversationMessage suitable for input to next conversation turn Example: + >>> import dapr.clients.grpc.conversation + >>> client = DaprClient() >>> response = client.converse_alpha2(name="openai", inputs=[input_alpha2], tools=[tool]) >>> choice = response.outputs[0].choices[0] >>> >>> # Convert LLM response to conversation message + >>> conversation_history = [] >>> assistant_message = convert_llm_response_to_conversation_input(choice.message) >>> conversation_history.append(assistant_message) >>> >>> # Use in next turn - >>> next_input = ConversationInputAlpha2(messages=conversation_history) + >>> next_input = conversation.ConversationInputAlpha2(messages=conversation_history) >>> next_response = client.converse_alpha2(name="openai", inputs=[next_input]) """ # Convert content string to ConversationMessageContent list @@ -840,7 +840,7 @@ def get_weather(location: str, unit: str = 'fahrenheit') -> str: }, ) - def append_response_to_history(response: 'ConversationResultAlpha2Message'): + def append_response_to_history(response: conversation.ConversationResponseAlpha2): """Helper to append response to history and execute tool calls.""" for msg in response.to_assistant_messages(): conversation_history.append(msg) From 3854bbd1f7196cfd0172526494575755ae369df6 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Sat, 16 Aug 2025 09:19:09 -0500 Subject: [PATCH 13/29] minor readme change Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- examples/conversation/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/conversation/README.md b/examples/conversation/README.md index 1fb6e249..376e1d92 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -57,9 +57,9 @@ The Conversation API supports real LLM providers including: -4. **Run the comprehensive example with real LLM providers (This requires LLMAPI Keys)** +4. **Run the comprehensive example with real LLM providers (This requires LLM API Keys)** - To run the comprehensive example with real LLM providers, you need to have at least one of the following LLM providers API keys: + You need to have at least one of the following LLM providers API keys: - OpenAI - Anthropic From 20189784851103260b77c2f5749a335559327eca Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:27:57 -0500 Subject: [PATCH 14/29] Update daprdocs/content/en/python-sdk-docs/python-client.md Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- daprdocs/content/en/python-sdk-docs/python-client.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index b2137a1b..f03a6a74 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -441,7 +441,7 @@ Since version 1.15 Dapr offers developers the capability to securely and reliabl ```python from dapr.clients import DaprClient -from dapr.clients.grpc._conversation import ConversationInput +from dapr.clients.grpc.conversation import ConversationInput with DaprClient() as d: inputs = [ From ee6729ed259512936936e845215c8ea8a318d4dc Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:11:13 -0500 Subject: [PATCH 15/29] lint Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 10 ++++++++-- dapr/clients/grpc/_conversation_helpers.py | 9 --------- dapr/clients/grpc/client.py | 8 ++++++-- dapr/clients/grpc/conversation.py | 9 +++++---- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 1b749fbe..a8e88757 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -105,8 +105,14 @@ StartWorkflowResponse, TopicEventResponse, ) -from dapr.clients.grpc.conversation import ConversationResult, ConversationResultAlpha2Message, \ - ConversationResultAlpha2Choices, ConversationResultAlpha2, ConversationResponse, ConversationResponseAlpha2 +from dapr.clients.grpc.conversation import ( + ConversationResult, + ConversationResultAlpha2Message, + ConversationResultAlpha2Choices, + ConversationResultAlpha2, + ConversationResponse, + ConversationResponseAlpha2, +) class DaprGrpcClientAsync: diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index 5b3282d3..c27b1f4e 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -502,15 +502,6 @@ def function_to_json_schema( # Extract parameter descriptions from docstring param_descriptions = _extract_docstring_args(func) - # Get function description - if description is None: - docstring = inspect.getdoc(func) - if docstring: - # Use first line of docstring as description - description = docstring.split('\n')[0].strip() - else: - description = f'Function {func.__name__}' - # Build JSON schema schema: Dict[str, Any] = {'type': 'object', 'properties': {}, 'required': []} diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 8429ac5b..bc9d53bc 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -1882,7 +1882,9 @@ def converse_alpha2( if not tool_call.id: tool_call.id = _generate_unique_tool_call_id() tool_calls.append( - conversation.ConversationToolCalls(id=tool_call.id, function=function_call) + conversation.ConversationToolCalls( + id=tool_call.id, function=function_call + ) ) result_message = conversation.ConversationResultAlpha2Message( @@ -1899,7 +1901,9 @@ def converse_alpha2( outputs.append(conversation.ConversationResultAlpha2(choices=choices)) - return conversation.ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) + return conversation.ConversationResponseAlpha2( + context_id=response.context_id, outputs=outputs + ) except RpcError as err: raise DaprGrpcError(err) from err diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 42f7c0be..9fbc5d77 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -299,6 +299,7 @@ def from_function(cls, func: Callable, register: bool = True) -> 'ConversationTo register_tool(c.name, ConversationTools(function=c, backend=FunctionBackend(func))) return c + # ------------------------------------------------------------------------------------------------ # Response Classes # ------------------------------------------------------------------------------------------------ @@ -355,7 +356,7 @@ def to_assistant_messages(self) -> List[ConversationMessage]: """Helper to convert to Assistant messages and makes it easy to use in multi-turn conversations.""" def convert_llm_response_to_conversation_input( - result_message: ConversationResultAlpha2Message, + result_message: ConversationResultAlpha2Message, ) -> ConversationMessage: """Convert ConversationResultMessage (from LLM response) to ConversationMessage.""" @@ -369,9 +370,7 @@ def convert_llm_response_to_conversation_input( # Create an assistant message (since LLM responses are always assistant messages) return ConversationMessage( - of_assistant=ConversationMessageOfAssistant( - content=content, tool_calls=tool_calls - ) + of_assistant=ConversationMessageOfAssistant(content=content, tool_calls=tool_calls) ) """Convert the outputs to a list of ConversationInput.""" @@ -385,6 +384,7 @@ def convert_llm_response_to_conversation_input( return assistant_messages + # ------------------------------------------------------------------------------------------------ # Tool Helpers # ------------------------------------------------------------------------------------------------ @@ -481,6 +481,7 @@ class ConversationTools: backend: Optional[ToolBackend] = None def invoke(self, params: Params = None) -> Any: + """execute the tool with params""" if not self.backend: raise conv_helpers.ToolExecutionError('Tool backend not set') return self.backend.invoke(self.function, params) From 7126b2f0b1c4f6983c01cadc5a4a909f548240b8 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:17:29 -0500 Subject: [PATCH 16/29] updates to fix issue with tool calling helper when dealing with classes instead of dataclasses, and also with serializatin output of the tool back to the LLM Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/_conversation_helpers.py | 417 +++++++++++++++++- dapr/clients/grpc/conversation.py | 3 +- examples/conversation/README.md | 55 +-- .../real_llm_providers_example.py | 69 +-- tests/clients/test_conversation_helpers.py | 367 +++++++++++++++ 5 files changed, 843 insertions(+), 68 deletions(-) create mode 100644 tests/clients/test_conversation_helpers.py diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index c27b1f4e..a6a0bd57 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -27,12 +27,14 @@ Optional, Sequence, Union, + Literal, get_args, get_origin, get_type_hints, ) from dapr.clients.grpc.conversation import Params +import os """ Tool Calling Helpers for Dapr Conversation API. @@ -44,6 +46,13 @@ having to manually define the JSON schema for each tool. """ +# Configuration for handling large enums to avoid massive JSON schemas that can exceed LLM token limits +_MAX_ENUM_ITEMS = int(os.getenv('DAPR_CONVERSATION_MAX_ENUM_ITEMS', '100')) +_LARGE_ENUM_BEHAVIOR = os.getenv('DAPR_CONVERSATION_LARGE_ENUM_BEHAVIOR', 'string').lower() +# Allowed behaviors: 'string' (default), 'error' +if _LARGE_ENUM_BEHAVIOR not in {'string', 'error'}: + _LARGE_ENUM_BEHAVIOR = 'string' + def _python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[str, Any]: """Convert a Python type hint to JSON schema format. @@ -82,6 +91,55 @@ def _python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[ # This is a true Union, use anyOf return {'anyOf': [_python_type_to_json_schema(arg, field_name) for arg in args]} + # Handle Literal types -> map to enum + if origin is Literal: + # Normalize literal values (convert Enum members to their value) + literal_values: List[Any] = [] + for val in args: + try: + from enum import Enum as _Enum + + if isinstance(val, _Enum): + literal_values.append(val.value) + else: + literal_values.append(val) + except Exception: + literal_values.append(val) + + # Determine JSON Schema primitive types for provided literals + def _json_primitive_type(v: Any) -> str: + if v is None: + return 'null' + if isinstance(v, bool): + return 'boolean' + if isinstance(v, int) and not isinstance(v, bool): + return 'integer' + if isinstance(v, float): + return 'number' + if isinstance(v, (bytes, bytearray)): + return 'string' + if isinstance(v, str): + return 'string' + # Fallback: let enum carry through without explicit type + return 'string' + + types = {_json_primitive_type(v) for v in literal_values} + schema: Dict[str, Any] = {'enum': literal_values} + # If all non-null literals share same type, include it + non_null_types = {t for t in types if t != 'null'} + if len(non_null_types) == 1 and (len(types) == 1 or len(types) == 2 and 'null' in types): + only_type = next(iter(non_null_types)) if non_null_types else 'null' + if only_type == 'string' and any( + isinstance(v, (bytes, bytearray)) for v in literal_values + ): + schema['type'] = 'string' + # Note: bytes literals represented as raw bytes are unusual; keeping enum as-is. + else: + schema['type'] = only_type + elif types == {'null'}: + schema['type'] = 'null' + return schema + # Handle List types if origin is list or python_type is list: if args: @@ -118,7 +176,31 @@ def _python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[ # Handle Enum types if inspect.isclass(python_type) and issubclass(python_type, Enum): - return {'type': 'string', 'enum': [item.value for item in python_type]} + try: + members = list(python_type) + except Exception: + members = [] + count = len(members) + # If enum is small enough, include full enum list (current behavior) + if count <= _MAX_ENUM_ITEMS: + return {'type': 'string', 'enum': [item.value for item in members]} + # Large enum handling + if _LARGE_ENUM_BEHAVIOR == 'error': + raise ValueError( + f"Enum '{getattr(python_type, '__name__', str(python_type))}' has {count} members, " + f"exceeding DAPR_CONVERSATION_MAX_ENUM_ITEMS={_MAX_ENUM_ITEMS}. " + f"Either reduce the enum size or set DAPR_CONVERSATION_LARGE_ENUM_BEHAVIOR=string to allow compact schema." + ) + # Default behavior: compact schema as a string with helpful context and a few examples + example_values = [item.value for item in members[:5]] if members else [] + desc = ( + f"{getattr(python_type, '__name__', 'Enum')} (enum with {count} values). " + f"Provide a valid value. Schema compacted to avoid oversized enum listing." + ) + schema: Dict[str, Any] = {'type': 'string', 'description': desc} + if example_values: + schema['examples'] = example_values + return schema # Handle Pydantic models (if available) if hasattr(python_type, 'model_json_schema'): @@ -148,8 +230,64 @@ def _python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[ return dataclass_schema - # Fallback for unknown types - return {'type': 'string', 'description': f'Unknown type: {python_type}'} + # Handle plain classes (non-dataclass) using __init__ signature and annotations + if inspect.isclass(python_type): + try: + # Gather type hints from __init__ if available; fall back to class annotations + init = getattr(python_type, '__init__', None) + init_hints = get_type_hints(init) if init else {} + class_hints = get_type_hints(python_type) + except Exception: + init_hints = {} + class_hints = {} + + # Build properties from __init__ parameters (excluding self) + properties: Dict[str, Any] = {} + required: List[str] = [] + + try: + sig = inspect.signature(python_type) + except Exception: + sig = None # type: ignore + + if sig is not None: + for pname, param in sig.parameters.items(): + if pname == 'self': + continue + # Determine type for this parameter + ptype = init_hints.get(pname) or class_hints.get(pname) or Any + properties[pname] = _python_type_to_json_schema(ptype, pname) + # Required if no default provided and not VAR_KEYWORD/POSITIONAL + if param.default is inspect._empty and param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): + required.append(pname) + else: + # Fall back to __slots__ if present + slots = getattr(python_type, '__slots__', None) + if isinstance(slots, (list, tuple)): + for pname in slots: + ptype = class_hints.get(pname, Any) + properties[pname] = _python_type_to_json_schema(ptype, pname) + required.append(pname) + + # If we found nothing, return a generic object + if not properties: + return {'type': 'object'} + + schema: Dict[str, Any] = {'type': 'object', 'properties': properties} + if required: + schema['required'] = required + return schema + + # Fallback for unknown/unsupported types + raise TypeError( + f"Unsupported type in JSON schema conversion for field '{field_name}': {python_type}. " + f"Please use supported typing annotations (e.g., str, int, float, bool, bytes, List[T], Dict[str, V], Union, Optional, Literal, Enum, dataclass, or plain classes)." + f"You can report this issue for future support of this type. You can always create the json schema manually." + ) def _extract_docstring_args(func) -> Dict[str, str]: @@ -534,6 +672,70 @@ def _generate_unique_tool_call_id(): return ''.join(random.choices(string.ascii_letters + string.digits, k=9)) +def stringify_tool_output(value: Any) -> str: + """Convert arbitrary tool return values into a serializable string. + + Rules: + - If value is already a string, return as-is. + - For bytes/bytearray, return a base64-encoded string with 'base64:' prefix (not JSON). + - Otherwise, attempt to JSON-serialize the value and return the JSON string. + Uses a conservative default encoder that supports only: + * Enum -> enum.value (fallback to name) + * dataclass -> asdict + If JSON serialization still fails, fallback to str(value). If that fails, return ''. + """ + import json as _json + import base64 as _b64 + from dataclasses import asdict as _asdict + + if isinstance(value, str): + return value + + # bytes/bytearray -> base64 string (raw, not JSON-quoted) + if isinstance(value, (bytes, bytearray)): + try: + return 'base64:' + _b64.b64encode(bytes(value)).decode('ascii') + except Exception: + try: + return str(value) + except Exception: + return '' + + def _default(o: Any): + # Enum handling + try: + from enum import Enum as _Enum + if isinstance(o, _Enum): + try: + return o.value + except Exception: + return getattr(o, 'name', str(o)) + except Exception: + pass + + # dataclass handling + try: + if is_dataclass(o): + return _asdict(o) + except Exception: + pass + + # Do not attempt to auto-serialize arbitrary objects via __dict__ to avoid + # partially serialized structures. Let json raise and we will fallback to str(o). + + # Fallback: cause JSON to fail for unsupported types + raise TypeError(f'Object of type {type(o).__name__} is not JSON serializable') + + try: + return _json.dumps(value, default=_default, ensure_ascii=False) + except Exception: + try: + # Last resort: convert to string + return str(value) + except Exception: + return '' + + # --- Tool Function Executor Backend # --- Errors ---- @@ -555,6 +757,199 @@ class ToolArgumentError(ToolError): ... +def _coerce_bool(value: Any) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, (int,)): + return bool(value) + if isinstance(value, str): + v = value.strip().lower() + if v in {'true', '1', 'yes', 'y', 'on'}: + return True + if v in {'false', '0', 'no', 'n', 'off'}: + return False + raise ValueError(f'Cannot coerce to bool: {value!r}') + + +def _coerce_scalar(value: Any, expected_type: Any) -> Any: + # Basic scalar coercions + if expected_type is str: + return value if isinstance(value, str) else str(value) + if expected_type is int: + if isinstance(value, bool): # avoid True->1 surprises + return int(value) + if isinstance(value, int): + return value + if isinstance(value, (float,)) and value.is_integer(): + return int(value) + if isinstance(value, str): + return int(value.strip()) + raise ValueError + if expected_type is float: + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + return float(value.strip()) + raise ValueError + if expected_type is bool: + return _coerce_bool(value) + return value + + +def _coerce_enum(value: Any, enum_type: Any) -> Any: + # Accept enum instance, name, or value + if isinstance(value, enum_type): + return value + try: + # match by value + for member in enum_type: + if member.value == value: + return member + if isinstance(value, str): + name = value.strip() + try: + return enum_type[name] + except Exception: + # try case-insensitive + for member in enum_type: + if member.name.lower() == name.lower(): + return member + except Exception: + pass + raise ValueError(f'Cannot coerce {value!r} to {enum_type.__name__}') + + +def _coerce_literal(value: Any, lit_args: List[Any]) -> Any: + # Try exact match first + if value in lit_args: + return value + # Try string-to-number coercions if literal set is homogeneous numeric + try_coerced = [] + for target in lit_args: + try: + if isinstance(target, int) and not isinstance(target, bool) and isinstance(value, str): + try_coerced.append(int(value)) + elif isinstance(target, float) and isinstance(value, str): + try_coerced.append(float(value)) + else: + try_coerced.append(value) + except Exception: + try_coerced.append(value) + for coerced in try_coerced: + if coerced in lit_args: + return coerced + raise ValueError(f'{value!r} not in allowed literals {lit_args!r}') + + +def _coerce_and_validate(value: Any, expected_type: Any) -> Any: + origin = get_origin(expected_type) + args = get_args(expected_type) + + # Optional[T] -> Union[T, None] + if origin is Union: + # try each option + last_err: Optional[Exception] = None + for opt in args: + if opt is type(None) and value is None: + return None + try: + return _coerce_and_validate(value, opt) + except Exception as e: + last_err = e + continue + raise ValueError( + str(last_err) if last_err else f'Cannot coerce {value!r} to {expected_type}' + ) + + # Literal + if origin is Literal: + return _coerce_literal(value, list(args)) + + # List[T] + if origin is list or expected_type is list: + item_type = args[0] if args else Any + if not isinstance(value, list): + raise ValueError(f'Expected list, got {type(value).__name__}') + return [_coerce_and_validate(v, item_type) for v in value] + + # Dict[K, V] + if origin is dict or expected_type is dict: + key_t = args[0] if len(args) > 0 else Any + val_t = args[1] if len(args) > 1 else Any + if not isinstance(value, dict): + raise ValueError(f'Expected dict, got {type(value).__name__}') + coerced: Dict[Any, Any] = {} + for k, v in value.items(): + ck = _coerce_and_validate(k, key_t) + cv = _coerce_and_validate(v, val_t) + coerced[ck] = cv + return coerced + + # Enums + if inspect.isclass(expected_type) and issubclass(expected_type, Enum): + return _coerce_enum(value, expected_type) + + # Dataclasses + if inspect.isclass(expected_type) and is_dataclass(expected_type): + if isinstance(value, expected_type) or value is None: + return value + raise ValueError( + f'Expected {expected_type.__name__} dataclass instance, got {type(value).__name__}' + ) + + # Plain classes (construct from dict using __init__ where possible) + if inspect.isclass(expected_type): + if isinstance(value, expected_type): + return value + if isinstance(value, dict): + try: + sig = inspect.signature(expected_type) + except Exception as e: + raise ValueError(f'Cannot inspect constructor for {expected_type.__name__}: {e}') + + # type hints from __init__ + try: + init_hints = get_type_hints(getattr(expected_type, '__init__', None)) + except Exception: + init_hints = {} + + kwargs: Dict[str, Any] = {} + missing: List[str] = [] + for pname, param in sig.parameters.items(): + if pname == 'self': + continue + if pname in value: + et = init_hints.get(pname, Any) + kwargs[pname] = _coerce_and_validate(value[pname], et) + else: + if param.default is inspect._empty and param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): + missing.append(pname) + if missing: + raise ValueError( + f"Missing required constructor arg(s) for {expected_type.__name__}: {', '.join(missing)}" + ) + try: + return expected_type(**kwargs) + except Exception as e: + raise ValueError(f'Failed constructing {expected_type.__name__} with {kwargs}: {e}') + # Not a dict or instance: fall through to isinstance check + + # Basic primitives + try: + return _coerce_scalar(value, expected_type) + except Exception: + # Fallback to isinstance check + if expected_type is Any or isinstance(value, expected_type): + return value + raise ValueError( + f"Expected {getattr(expected_type, '__name__', str(expected_type))}, got {type(value).__name__}" + ) + + def bind_params_to_func(fn: Callable[..., Any], params: Params): """Bind parameters to a function in the correct order. @@ -599,4 +994,20 @@ def bind_params_to_func(fn: Callable[..., Any], params: Params): raise ToolArgumentError('params must be a mapping (kwargs), sequence (positional), or None') bound.apply_defaults() + + # Coerce and validate according to type hints + try: + type_hints = get_type_hints(fn) + except Exception: + type_hints = {} + for name, value in list(bound.arguments.items()): + if name in type_hints: + expected = type_hints[name] + try: + bound.arguments[name] = _coerce_and_validate(value, expected) + except Exception as e: + raise ToolArgumentError( + f"Invalid value for parameter '{name}': expected {getattr(get_origin(expected) or expected, '__name__', str(expected))}, got {type(value).__name__} ({value!r}). Details: {e}" + ) from e + return bound diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 9fbc5d77..09594292 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -562,8 +562,9 @@ def create_assistant_message(text: str) -> ConversationMessage: ) -def create_tool_message(tool_id: str, name: str, content: str) -> ConversationMessage: +def create_tool_message(tool_id: str, name: str, content: Any) -> ConversationMessage: """Helper to create a tool message for Alpha2 responses (from client to LLM).""" + content = conv_helpers.stringify_tool_output(content) return ConversationMessage( of_tool=ConversationMessageOfTool( tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] diff --git a/examples/conversation/README.md b/examples/conversation/README.md index 376e1d92..3901d869 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -150,6 +150,15 @@ Recommended order of approaches: - Function-to-Schema (automatic schema from typed function) - JSON schema variants (fallbacks for dynamic/manual cases) +When using the Decorator or Function-to-Schema approach, you get the following benefits: + +- ✅ **Type Safety**: Full Python type hint support (str, int, List, Optional, Enum, etc.) +- ✅ **Auto-Documentation**: Docstring parsing for parameter descriptions +- ✅ **Ultimate DevEx**: Define functions, get tools automatically +- ✅ **90%+ less boilerplate** compared to manual schema creation +- ✅ **Automatic Tool Registration** this comes handy when you want to execute the tool when called by the LLM + + ### Decorator-based Tool Definition (Recommended) ```python from dapr.clients.grpc import conversation @@ -188,35 +197,10 @@ function = conversation.ConversationToolsFunction.from_function(get_weather) weather_tool = conversation.ConversationTools(function=function) ``` -Benefits: -- ✅ **Type Safety**: Full Python type hint support (str, int, List, Optional, Enum, etc.) -- ✅ **Auto-Documentation**: Docstring parsing for parameter descriptions -- ✅ **Ultimate DevEx**: Define functions, get tools automatically -- ✅ **90%+ less boilerplate** compared to manual schema creation - ### JSON Schema Variants (fallbacks) Use when you can't decorate or need to build tools dynamically. -#### Simple JSON Schema -```python -from dapr.clients.grpc import conversation - -function = conversation.ConversationToolsFunction( - name="get_weather", - description="Get weather", - parameters={ - "type": "object", - "properties": { - "location": {"type": "string", "description": "City"}, - "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} - }, - "required": ["location"] - } -) -weather_tool = conversation.ConversationTools(function=function) -``` - #### Complete JSON Schema (e.g., calculator) ```python from dapr.clients.grpc import conversation @@ -575,15 +559,16 @@ dapr run --app-id test-app --dapr-http-port 3500 --dapr-grpc-port 50001 --resour ## Features Overview -| Feature | Alpha1 | Alpha2 | -|---------|--------|--------| -| Basic Conversations | ✅ | ✅ | -| Tool Calling | ❌ | ✅ | -| Multi-turn Context | ❌ | ✅ | -| Advanced Message Types | ❌ | ✅ | -| Parameter Auto-conversion | ❌ | ✅ | -| Function-to-Schema | ❌ | ✅ | -| Async Support | ✅ | ✅ Enhanced | -| Real LLM Providers | ✅ | ✅ | +| Feature | Alpha1 | Alpha2 | +|---------------------------|--------|--------| +| Basic Conversations | ✅ | ✅ | +| Tool Calling | ❌ | ✅ | +| Multi-turn Context | ❌ | ✅ | +| Advanced Message Types | ❌ | ✅ | +| Parameter Auto-conversion | ❌ | ✅ | +| Function-to-Schema | ❌ | ✅ | +| Async Support | ✅ | ✅ | +| Real LLM Providers | ✅ | ✅ | +| Streaming | ❌ | ❌ | **Recommendation:** Use Alpha2 for new projects and consider migrating existing Alpha1 code to benefit from enhanced features and improved developer experience. \ No newline at end of file diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index efcf872b..c103007e 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -432,6 +432,7 @@ def test_basic_conversation_alpha2(self, provider_id: str) -> None: response = client.converse_alpha2( name=provider_id, inputs=[input_alpha2], + temperature=1, parameters={ 'temperature': 0.7, 'max_tokens': 100, @@ -472,8 +473,8 @@ def test_multi_turn_conversation_alpha2(self, provider_id: str) -> None: response = client.converse_alpha2( name=provider_id, inputs=[input_alpha2], + temperature=1, parameters={ - 'temperature': 0.5, 'max_tokens': 150, }, ) @@ -510,8 +511,8 @@ def test_tool_calling_alpha2(self, provider_id: str) -> None: inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, # Lower temperature for more consistent tool calling 'max_tokens': 500, }, ) @@ -584,9 +585,9 @@ def test_parameter_conversion(self, provider_id: str) -> None: name=provider_id, inputs=[input_alpha2], tools=[weather_tool, calc_tool, time_tool, search_tool], + temperature=1, parameters={ # Raw Python values - automatically converted to GrpcAny - 'temperature': 0.8, # float 'max_tokens': 200, # int 'top_p': 1.0, # float 'frequency_penalty': 0.0, # float @@ -636,8 +637,8 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: inputs=[input_alpha2_turn1], tools=[weather_tool], # Tools included in turn 1 tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -706,8 +707,8 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: name=provider_id, inputs=[input_alpha2_turn2], tools=[weather_tool], # Tools carried forward to turn 2 + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -746,8 +747,8 @@ def test_multi_turn_tool_calling_alpha2(self, provider_id: str) -> None: inputs=[input_alpha2_turn3], tools=[weather_tool], # Tools still available in turn 3 tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -834,13 +835,15 @@ def get_weather(location: str, unit: str = 'fahrenheit') -> str: inputs=[input_alpha2_turn1], tools=conversation.get_registered_tools(), # using registered tools (automatically registered by the decorator) tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) - def append_response_to_history(response: conversation.ConversationResponseAlpha2): + def append_response_to_history( + response: conversation.ConversationResponseAlpha2, skip_execution: bool = False + ): """Helper to append response to history and execute tool calls.""" for msg in response.to_assistant_messages(): conversation_history.append(msg) @@ -848,12 +851,17 @@ def append_response_to_history(response: conversation.ConversationResponseAlpha2 continue for _tool_call in msg.of_assistant.tool_calls: print(f'Executing tool call: {_tool_call.function.name}') - output = conversation.execute_registered_tool( - _tool_call.function.name, _tool_call.function.arguments - ) - print(f'Tool output: {output}') - # append result to history + # execute the tool called by the LLM + if not skip_execution: + output = conversation.execute_registered_tool( + _tool_call.function.name, _tool_call.function.arguments + ) + print(f'Tool output: {output}') + else: + output = 'tool execution skipped' + + # append a result to history conversation_history.append( conversation.create_tool_message( tool_id=_tool_call.id, @@ -875,8 +883,8 @@ def append_response_to_history(response: conversation.ConversationResponseAlpha2 name=provider_id, inputs=[input_alpha2_turn2], tools=conversation.get_registered_tools(), + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -903,8 +911,8 @@ def append_response_to_history(response: conversation.ConversationResponseAlpha2 inputs=[input_alpha2_turn3], tools=conversation.get_registered_tools(), tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -923,15 +931,18 @@ def append_response_to_history(response: conversation.ConversationResponseAlpha2 inputs=[input_alpha2_turn4], tools=conversation.get_registered_tools(), tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) - append_response_to_history(response4) + append_response_to_history(response4, skip_execution=False) print('Full conversation history trace:') + print(' Tools available:') + for tool in conversation.get_registered_tools(): + print(f' - {tool.function.name}({tool.function.parameters["properties"]})') for msg in conversation_history: msg.trace_print(2) @@ -962,8 +973,8 @@ def test_function_to_schema_approach(self, provider_id: str) -> None: inputs=[input_alpha2], tools=[restaurant_tool], tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -1012,8 +1023,8 @@ def test_tool_decorated_function_to_schema_approach(self, provider_id: str) -> N inputs=[input_alpha2], tools=conversation.get_registered_tools(), tool_choice='auto', + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -1054,8 +1065,8 @@ async def test_async_conversation_alpha2(self, provider_id: str) -> None: response = await client.converse_alpha2( name=provider_id, inputs=[input_alpha2], + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -1086,8 +1097,8 @@ async def test_async_tool_calling_alpha2(self, provider_id: str) -> None: name=provider_id, inputs=[input_alpha2], tools=[weather_tool], + temperature=1, parameters={ - 'temperature': 0.3, 'max_tokens': 500, }, ) @@ -1119,13 +1130,13 @@ def run_comprehensive_test(self, provider_id: str) -> None: print(f"{'='*60}") # Alpha2 Sync tests - # self.test_basic_conversation_alpha2(provider_id) - # self.test_multi_turn_conversation_alpha2(provider_id) - # self.test_tool_calling_alpha2(provider_id) - # self.test_parameter_conversion(provider_id) - # self.test_function_to_schema_approach(provider_id) - # self.test_tool_decorated_function_to_schema_approach(provider_id) - # self.test_multi_turn_tool_calling_alpha2(provider_id) + self.test_basic_conversation_alpha2(provider_id) + self.test_multi_turn_conversation_alpha2(provider_id) + self.test_tool_calling_alpha2(provider_id) + self.test_parameter_conversion(provider_id) + self.test_function_to_schema_approach(provider_id) + self.test_tool_decorated_function_to_schema_approach(provider_id) + self.test_multi_turn_tool_calling_alpha2(provider_id) self.test_multi_turn_tool_calling_alpha2_tool_helpers(provider_id) # Alpha2 Async tests @@ -1152,8 +1163,8 @@ def test_basic_conversation_alpha1_legacy(self, provider_id: str) -> None: response = client.converse_alpha1( name=provider_id, inputs=inputs, + temperature=1, parameters={ - 'temperature': 0.7, 'max_tokens': 100, }, ) diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py new file mode 100644 index 00000000..6dfa61f9 --- /dev/null +++ b/tests/clients/test_conversation_helpers.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- + +""" +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 json +import base64 +from dataclasses import dataclass +from enum import Enum + +from typing import Dict, List, Literal, Optional, Union, Set + + +from dapr.clients.grpc._conversation_helpers import ( + stringify_tool_output, + bind_params_to_func, + function_to_json_schema, +) + + +def test_string_passthrough(): + assert stringify_tool_output('hello') == 'hello' + + +def test_json_serialization_collections(): + data = {'a': 1, 'b': [2, 'x'], 'c': {'k': True}} + out = stringify_tool_output(data) + # Must be a JSON string we can parse back to the same structure + parsed = json.loads(out) + assert parsed == data + + +class Color(Enum): + RED = 'red' + BLUE = 'blue' + + +def test_enum_serialization_uses_value_and_is_json_string(): + out = stringify_tool_output(Color.RED) + # json.dumps on a string value yields a quoted JSON string + assert out == json.dumps('red', ensure_ascii=False) + + +@dataclass +class Point: + x: int + y: int + + +def test_dataclass_serialization_to_json_dict(): + p = Point(1, 2) + out = stringify_tool_output(p) + parsed = json.loads(out) + assert parsed == {'x': 1, 'y': 2} + + +def test_bytes_and_bytearray_to_base64_prefixed(): + b = bytes([0, 1, 2, 250, 255]) + expected = 'base64:' + base64.b64encode(b).decode('ascii') + assert stringify_tool_output(b) == expected + + ba = bytearray(b) + expected_ba = 'base64:' + base64.b64encode(bytes(ba)).decode('ascii') + assert stringify_tool_output(ba) == expected_ba + + +class WithDict: + def __init__(self): + self.x = 1 + self.y = 'y' + self.fn = lambda: 42 # callable should be filtered out + + +def test_object_with___dict___becomes_dict_without_callables(): + obj = WithDict() + out = stringify_tool_output(obj) + parsed = json.loads(out) + assert parsed == {'x': 1, 'y': 'y'} + + +class UnserializableButStr: + def __init__(self): + self.bad = {1, 2, 3} # set is not JSON serializable + + def __str__(self): + return 'myobj' + + +def test_fallback_to_str_when_json_fails(): + obj = UnserializableButStr() + out = stringify_tool_output(obj) + assert out == 'myobj' + + +class BadStr: + def __init__(self): + self.bad = {1, 2, 3} + + def __str__(self): + raise RuntimeError('boom') + + +def test_last_resort_unserializable_marker_when_str_raises(): + obj = BadStr() + out = stringify_tool_output(obj) + assert out == '' + + +def _example_get_flights( + *, + flight_data: List[str], + trip: Literal['round-trip', 'one-way', 'multi-city'], + passengers: int, + seat: Literal['economy', 'premium-economy', 'business', 'first'], + fetch_mode: Literal['common', 'fallback', 'force-fallback', 'local'] = 'common', + max_stops: Optional[int] = None, +): + return { + 'flight_data': flight_data, + 'trip': trip, + 'passengers': passengers, + 'seat': seat, + 'fetch_mode': fetch_mode, + 'max_stops': max_stops, + } + + +def test_bind_params_basic_coercion_from_examples(): + params = { + 'flight_data': ['AUS', 'OPO'], + 'trip': 'one-way', + 'passengers': '1', # should coerce to int + 'seat': 'economy', + 'fetch_mode': 'common', + 'max_stops': 0, + } + bound = bind_params_to_func(_example_get_flights, params) + # Ensure type coercion happened + assert isinstance(bound.kwargs['passengers'], int) + assert bound.kwargs['passengers'] == 1 + assert isinstance(bound.kwargs['max_stops'], int) + # Function should still run with coerced params + result = _example_get_flights(*bound.args, **bound.kwargs) + assert result['passengers'] == 1 + assert result['trip'] == 'one-way' + assert result['seat'] == 'economy' + + +def test_literal_schema_generation_from_examples(): + schema = function_to_json_schema(_example_get_flights) + props = schema['properties'] + + # flight_data -> array of strings + assert props['flight_data']['type'] == 'array' + assert props['flight_data']['items']['type'] == 'string' + + # trip -> enum of strings + assert props['trip']['type'] == 'string' + assert set(props['trip']['enum']) == {'round-trip', 'one-way', 'multi-city'} + + # passengers -> integer + assert props['passengers']['type'] == 'integer' + + # seat -> enum of strings + assert props['seat']['type'] == 'string' + assert set(props['seat']['enum']) == {'economy', 'premium-economy', 'business', 'first'} + + # fetch_mode -> enum with default provided in function (not necessarily in schema, but not required) + assert props['fetch_mode']['type'] == 'string' + assert set(props['fetch_mode']['enum']) == {'common', 'fallback', 'force-fallback', 'local'} + + # max_stops -> optional int (not required) + assert props['max_stops']['type'] == 'integer' + + # Required fields reflect parameters without defaults + # Note: order not guaranteed + required = set(schema['required']) + assert {'flight_data', 'trip', 'passengers', 'seat'}.issubset(required) + assert 'fetch_mode' not in required + assert 'max_stops' not in required + + +# Define minimal stand-in classes to test class coercion behavior +class FlightData: + def __init__( + self, date: str, from_airport: str, to_airport: str, max_stops: Optional[int] = None + ): + self.date = date + self.from_airport = from_airport + self.to_airport = to_airport + self.max_stops = max_stops + + +class Passengers: + def __init__(self, adults: int, children: int, infants_in_seat: int, infants_on_lap: int): + self.adults = adults + self.children = children + self.infants_in_seat = infants_in_seat + self.infants_on_lap = infants_on_lap + + +def _example_get_flights_with_classes( + *, + flight_data: List[FlightData], + trip: Literal['round-trip', 'one-way', 'multi-city'], + passengers: Passengers, + seat: Literal['economy', 'premium-economy', 'business', 'first'], + fetch_mode: Literal['common', 'fallback', 'force-fallback', 'local'] = 'common', + max_stops: Optional[int] = None, +): + return { + 'flight_data': flight_data, + 'trip': trip, + 'passengers': passengers, + 'seat': seat, + 'fetch_mode': fetch_mode, + 'max_stops': max_stops, + } + + +def test_class_coercion_and_schema_from_examples(): + # Verify schema generation includes class fields + schema = function_to_json_schema(_example_get_flights_with_classes) + props = schema['properties'] + + # flight_data is array of objects with class fields + fd_schema = props['flight_data']['items'] + assert fd_schema['type'] == 'object' + for key in ['date', 'from_airport', 'to_airport']: + assert key in fd_schema['properties'] + assert fd_schema['properties'][key]['type'] == 'string' + # Optional int field + assert fd_schema['properties']['max_stops']['type'] == 'integer' + + # passengers object has proper fields + p_schema = props['passengers'] + assert p_schema['type'] == 'object' + for key in ['adults', 'children', 'infants_in_seat', 'infants_on_lap']: + assert p_schema['properties'][key]['type'] == 'integer' + + # Provide dicts to be coerced into class instances + params = { + 'flight_data': [ + {'date': '2025-09-01', 'from_airport': 'AUS', 'to_airport': 'OPO', 'max_stops': '1'}, + {'date': '2025-09-10', 'from_airport': 'OPO', 'to_airport': 'AUS'}, + ], + 'trip': 'round-trip', + 'passengers': {'adults': 1, 'children': 0, 'infants_in_seat': 0, 'infants_on_lap': 0}, + 'seat': 'economy', + 'fetch_mode': 'common', + 'max_stops': 1, + } + + bound = bind_params_to_func(_example_get_flights_with_classes, params) + result = _example_get_flights_with_classes(*bound.args, **bound.kwargs) + + # Ensure coerced instances + assert all(isinstance(fd, FlightData) for fd in result['flight_data']) + assert isinstance(result['passengers'], Passengers) + # Ensure coercion of max_stops inside FlightData + assert result['flight_data'][0].max_stops == 1 + + +# ---- Additional function_to_json_schema tests for dataclass and other types ---- + + +@dataclass +class Person: + name: str + age: int = 0 # default -> not required in schema + + +def _fn_with_dataclass(user: Person, teammates: Optional[List[Person]] = None): + return True + + +def test_function_to_json_schema_with_dataclass_param(): + schema = function_to_json_schema(_fn_with_dataclass) + props = schema['properties'] + + # user -> dataclass object + assert props['user']['type'] == 'object' + assert set(props['user']['properties'].keys()) == {'name', 'age'} + assert props['user']['properties']['name']['type'] == 'string' + assert props['user']['properties']['age']['type'] == 'integer' + # required should include 'user' (function param) and within dataclass, field default logic is internal; + # for function level required, user has no default -> required + assert 'user' in schema['required'] + + # teammates -> Optional[List[Person]] + assert props['teammates']['type'] == 'array' + assert props['teammates']['items']['type'] == 'object' + assert set(props['teammates']['items']['properties'].keys()) == {'name', 'age'} + # teammates is Optional -> not required at top level + assert 'teammates' not in schema['required'] + + +class Pet(Enum): + DOG = 'dog' + CAT = 'cat' + + +def _fn_with_enum(pet: Pet): + return True + + +def test_function_to_json_schema_with_enum_param(): + schema = function_to_json_schema(_fn_with_enum) + pet_schema = schema['properties']['pet'] + assert pet_schema['type'] == 'string' + assert set(pet_schema['enum']) == {'dog', 'cat'} + + +def _fn_with_dict(meta: Dict[str, int]): + return True + + +def test_function_to_json_schema_with_dict_str_int(): + schema = function_to_json_schema(_fn_with_dict) + meta_schema = schema['properties']['meta'] + assert meta_schema['type'] == 'object' + assert meta_schema['additionalProperties']['type'] == 'integer' + + +def _fn_with_bytes(data: bytes): + return True + + +def test_function_to_json_schema_with_bytes(): + schema = function_to_json_schema(_fn_with_bytes) + data_schema = schema['properties']['data'] + assert data_schema['type'] == 'string' + assert data_schema.get('format') == 'byte' + + +def _fn_with_union(identifier: Union[int, str]): + return True + + +def test_function_to_json_schema_with_true_union_anyof(): + schema = function_to_json_schema(_fn_with_union) + id_schema = schema['properties']['identifier'] + assert 'anyOf' in id_schema + types = {opt.get('type') for opt in id_schema['anyOf']} + assert types == {'integer', 'string'} + + +def _fn_with_unsupported_type(s: Set[int]): + return True + + +def test_function_to_json_schema_unsupported_type_raises(): + try: + function_to_json_schema(_fn_with_unsupported_type) + assert False, 'Expected TypeError or ValueError for unsupported type' + except (TypeError, ValueError): + pass From fbc01950c1ac1675db998edfff8244323e8d6229 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:36:36 -0500 Subject: [PATCH 17/29] coalesce conv helper tests, fix typing lint Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/_conversation_helpers.py | 17 +- tests/clients/test_conversation_helpers.py | 1241 ++++++++++++++++++ tests/clients/test_grpc_helpers.py | 1312 -------------------- 3 files changed, 1251 insertions(+), 1319 deletions(-) delete mode 100644 tests/clients/test_grpc_helpers.py diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index a6a0bd57..9af1a89c 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -31,6 +31,7 @@ get_args, get_origin, get_type_hints, + cast, ) from dapr.clients.grpc.conversation import Params @@ -152,7 +153,7 @@ def _json_primitive_type(v: Any) -> str: # Handle Dict types if origin is dict or python_type is dict: - schema: Dict[str, Any] = {'type': 'object'} + schema = {'type': 'object'} if args and len(args) == 2: # Dict[str, ValueType] - add additionalProperties key_type, value_type = args @@ -197,7 +198,7 @@ def _json_primitive_type(v: Any) -> str: f"{getattr(python_type, '__name__', 'Enum')} (enum with {count} values). " f"Provide a valid value. Schema compacted to avoid oversized enum listing." ) - schema: Dict[str, Any] = {'type': 'string', 'description': desc} + schema = {'type': 'string', 'description': desc} if example_values: schema['examples'] = example_values return schema @@ -277,7 +278,7 @@ def _json_primitive_type(v: Any) -> str: if not properties: return {'type': 'object'} - schema: Dict[str, Any] = {'type': 'object', 'properties': properties} + schema = {'type': 'object', 'properties': properties} if required: schema['required'] = required return schema @@ -285,8 +286,8 @@ def _json_primitive_type(v: Any) -> str: # Fallback for unknown/unsupported types raise TypeError( f"Unsupported type in JSON schema conversion for field '{field_name}': {python_type}. " - f"Please use supported typing annotations (e.g., str, int, float, bool, bytes, List[T], Dict[str, V], Union, Optional, Literal, Enum, dataclass, or plain classes)." - f"You can report this issue for future support of this type. You can always create the json schema manually." + f'Please use supported typing annotations (e.g., str, int, float, bool, bytes, List[T], Dict[str, V], Union, Optional, Literal, Enum, dataclass, or plain classes).' + f'You can report this issue for future support of this type. You can always create the json schema manually.' ) @@ -705,6 +706,7 @@ def _default(o: Any): # Enum handling try: from enum import Enum as _Enum + if isinstance(o, _Enum): try: return o.value @@ -716,7 +718,8 @@ def _default(o: Any): # dataclass handling try: if is_dataclass(o): - return _asdict(o) + # mypy: asdict expects a DataclassInstance; after the runtime guard, this cast is safe + return _asdict(cast(Any, o)) except Exception: pass @@ -824,7 +827,7 @@ def _coerce_literal(value: Any, lit_args: List[Any]) -> Any: if value in lit_args: return value # Try string-to-number coercions if literal set is homogeneous numeric - try_coerced = [] + try_coerced: List[Any] = [] for target in lit_args: try: if isinstance(target, int) and not isinstance(target, bool) and isinstance(value, str): diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py index 6dfa61f9..717530ec 100644 --- a/tests/clients/test_conversation_helpers.py +++ b/tests/clients/test_conversation_helpers.py @@ -15,6 +15,8 @@ import json import base64 +import unittest +import warnings from dataclasses import dataclass from enum import Enum @@ -25,7 +27,11 @@ stringify_tool_output, bind_params_to_func, function_to_json_schema, + _extract_docstring_args, + _python_type_to_json_schema, + extract_docstring_summary, ) +from dapr.clients.grpc.conversation import ConversationToolsFunction def test_string_passthrough(): @@ -365,3 +371,1238 @@ def test_function_to_json_schema_unsupported_type_raises(): assert False, 'Expected TypeError or ValueError for unsupported type' except (TypeError, ValueError): pass + + +class TestPythonTypeToJsonSchema(unittest.TestCase): + """Test the _python_type_to_json_schema function.""" + + def test_basic_types(self): + """Test conversion of basic Python types.""" + test_cases = [ + (str, {'type': 'string'}), + (int, {'type': 'integer'}), + (float, {'type': 'number'}), + (bool, {'type': 'boolean'}), + (bytes, {'type': 'string', 'format': 'byte'}), + ] + + for python_type, expected in test_cases: + with self.subTest(python_type=python_type): + result = _python_type_to_json_schema(python_type) + self.assertEqual(result['type'], expected['type']) + if 'format' in expected: + self.assertEqual(result['format'], expected['format']) + + def test_optional_types(self): + """Test Optional[T] types (Union[T, None]).""" + # Optional[str] should resolve to string + result = _python_type_to_json_schema(Optional[str]) + self.assertEqual(result['type'], 'string') + + # Optional[int] should resolve to integer + result = _python_type_to_json_schema(Optional[int]) + self.assertEqual(result['type'], 'integer') + + def test_list_types(self): + """Test List[T] types.""" + # List[str] + result = _python_type_to_json_schema(List[str]) + expected = {'type': 'array', 'items': {'type': 'string'}} + self.assertEqual(result, expected) + + # List[int] + result = _python_type_to_json_schema(List[int]) + expected = {'type': 'array', 'items': {'type': 'integer'}} + self.assertEqual(result, expected) + + def test_dict_types(self): + """Test Dict[str, T] types.""" + result = _python_type_to_json_schema(Dict[str, int]) + expected = {'type': 'object', 'additionalProperties': {'type': 'integer'}} + self.assertEqual(result, expected) + + def test_enum_types(self): + """Test Enum types.""" + + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + result = _python_type_to_json_schema(Color) + expected = {'type': 'string', 'enum': ['red', 'green', 'blue']} + self.assertEqual(result['type'], expected['type']) + self.assertEqual(set(result['enum']), set(expected['enum'])) + + def test_union_types(self): + """Test Union types.""" + result = _python_type_to_json_schema(Union[str, int]) + self.assertIn('anyOf', result) + self.assertEqual(len(result['anyOf']), 2) + + # Should contain both string and integer schemas + types = [schema['type'] for schema in result['anyOf']] + self.assertIn('string', types) + self.assertIn('integer', types) + + def test_dataclass_types(self): + """Test dataclass types.""" + + @dataclass + class Person: + name: str + age: int = 25 + + result = _python_type_to_json_schema(Person) + + self.assertEqual(result['type'], 'object') + self.assertIn('properties', result) + self.assertIn('required', result) + + # Check properties + self.assertIn('name', result['properties']) + self.assertIn('age', result['properties']) + self.assertEqual(result['properties']['name']['type'], 'string') + self.assertEqual(result['properties']['age']['type'], 'integer') + + # Check required fields (name is required, age has default) + self.assertIn('name', result['required']) + self.assertNotIn('age', result['required']) + + def test_pydantic_models(self): + """Test Pydantic model types.""" + try: + from pydantic import BaseModel + + class SearchParams(BaseModel): + query: str + limit: int = 10 + include_images: bool = False + tags: Optional[List[str]] = None + + result = _python_type_to_json_schema(SearchParams) + + # Pydantic models should generate their own schema + self.assertIn('type', result) + # The exact structure depends on Pydantic version, but it should have properties + if 'properties' in result: + self.assertIn('query', result['properties']) + except ImportError: + self.skipTest('Pydantic not available for testing') + + def test_nested_types(self): + """Test complex nested type combinations.""" + # Optional[List[str]] + result = _python_type_to_json_schema(Optional[List[str]]) + self.assertEqual(result['type'], 'array') + self.assertEqual(result['items']['type'], 'string') + + # List[Optional[int]] + result = _python_type_to_json_schema(List[Optional[int]]) + self.assertEqual(result['type'], 'array') + self.assertEqual(result['items']['type'], 'integer') + + # Dict[str, List[int]] + result = _python_type_to_json_schema(Dict[str, List[int]]) + self.assertEqual(result['type'], 'object') + self.assertEqual(result['additionalProperties']['type'], 'array') + self.assertEqual(result['additionalProperties']['items']['type'], 'integer') + + def test_complex_dataclass_with_nested_types(self): + """Test dataclass with complex nested types.""" + + @dataclass + class Address: + street: str + city: str + zipcode: Optional[str] = None + + @dataclass + class Person: + name: str + addresses: List[Address] + metadata: Dict[str, str] + tags: Optional[List[str]] = None + + result = _python_type_to_json_schema(Person) + + self.assertEqual(result['type'], 'object') + self.assertIn('name', result['properties']) + self.assertIn('addresses', result['properties']) + self.assertIn('metadata', result['properties']) + self.assertIn('tags', result['properties']) + + # Check nested structures + self.assertEqual(result['properties']['addresses']['type'], 'array') + self.assertEqual(result['properties']['metadata']['type'], 'object') + self.assertEqual(result['properties']['tags']['type'], 'array') + + # Required fields + self.assertIn('name', result['required']) + self.assertIn('addresses', result['required']) + self.assertIn('metadata', result['required']) + self.assertNotIn('tags', result['required']) + + def test_enum_with_different_types(self): + """Test enums with different value types.""" + + class Status(Enum): + ACTIVE = 1 + INACTIVE = 0 + PENDING = 2 + + class Priority(Enum): + LOW = 'low' + MEDIUM = 'medium' + HIGH = 'high' + + # String enum + result = _python_type_to_json_schema(Priority) + self.assertEqual(result['type'], 'string') + self.assertEqual(set(result['enum']), {'low', 'medium', 'high'}) + + # Integer enum + result = _python_type_to_json_schema(Status) + self.assertEqual(result['type'], 'string') + self.assertEqual(set(result['enum']), {1, 0, 2}) + + def test_none_type(self): + """Test None type handling.""" + result = _python_type_to_json_schema(type(None)) + self.assertEqual(result['type'], 'null') + + def test_realistic_function_types(self): + """Test types from realistic function signatures.""" + # Weather function parameters + result = _python_type_to_json_schema(str) # location + self.assertEqual(result['type'], 'string') + + # Optional unit with enum + class TemperatureUnit(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + + result = _python_type_to_json_schema(Optional[TemperatureUnit]) + self.assertEqual(result['type'], 'string') + self.assertEqual(set(result['enum']), {'celsius', 'fahrenheit'}) + + # Search function with complex params + @dataclass + class SearchOptions: + max_results: int = 10 + include_metadata: bool = True + filters: Optional[Dict[str, str]] = None + + result = _python_type_to_json_schema(SearchOptions) + self.assertEqual(result['type'], 'object') + self.assertIn('max_results', result['properties']) + self.assertIn('include_metadata', result['properties']) + self.assertIn('filters', result['properties']) + + def test_list_without_type_args(self): + """Test bare List type without type arguments.""" + result = _python_type_to_json_schema(list) + self.assertEqual(result['type'], 'array') + self.assertNotIn('items', result) + + def test_dict_without_type_args(self): + """Test bare Dict type without type arguments.""" + result = _python_type_to_json_schema(dict) + self.assertEqual(result['type'], 'object') + self.assertNotIn('additionalProperties', result) + + +class TestExtractDocstringInfo(unittest.TestCase): + """Test the extract_docstring_info function.""" + + def test_google_style_docstring(self): + """Test Google-style docstring parsing.""" + + def sample_function(name: str, age: int) -> str: + """A sample function. + + Args: + name: The person's name + age: The person's age in years + """ + return f'{name} is {age}' + + result = _extract_docstring_args(sample_function) + expected = {'name': "The person's name", 'age': "The person's age in years"} + self.assertEqual(result, expected) + + def test_no_docstring(self): + """Test function with no docstring.""" + + def no_doc_function(param): + pass + + result = _extract_docstring_args(no_doc_function) + self.assertEqual(result, {}) + + def test_docstring_without_args(self): + """Test docstring without Args section.""" + + def simple_function(param): + """Just a simple function.""" + pass + + result = _extract_docstring_args(simple_function) + self.assertEqual(result, {}) + + def test_multiline_param_description(self): + """Test parameter descriptions that span multiple lines.""" + + def complex_function(param1: str) -> str: + """A complex function. + + Args: + param1: This is a long description + that spans multiple lines + for testing purposes + """ + return param1 + + result = _extract_docstring_args(complex_function) + expected = { + 'param1': 'This is a long description that spans multiple lines for testing purposes' + } + self.assertEqual(result, expected) + + def test_sphinx_style_docstring(self): + """Test Sphinx-style docstring parsing.""" + + def sphinx_function(location: str, unit: str) -> str: + """Get weather information. + + :param location: The city or location name + :param unit: Temperature unit (celsius or fahrenheit) + :type location: str + :type unit: str + :returns: Weather information string + :rtype: str + """ + return f'Weather in {location}' + + result = _extract_docstring_args(sphinx_function) + expected = { + 'location': 'The city or location name', + 'unit': 'Temperature unit (celsius or fahrenheit)', + } + self.assertEqual(result, expected) + + def test_sphinx_style_with_parameter_keyword(self): + """Test Sphinx-style with :parameter: instead of :param:.""" + + def sphinx_function2(query: str, limit: int) -> str: + """Search for data. + + :parameter query: The search query string + :parameter limit: Maximum number of results + """ + return f'Results for {query}' + + result = _extract_docstring_args(sphinx_function2) + expected = {'query': 'The search query string', 'limit': 'Maximum number of results'} + self.assertEqual(result, expected) + + def test_sphinx_style_multiline_descriptions(self): + """Test Sphinx-style with multi-line parameter descriptions.""" + + def sphinx_multiline_function(data: str) -> str: + """Process complex data. + + :param data: The input data to process, which can be + quite complex and may require special handling + for optimal results + :returns: Processed data + """ + return data + + result = _extract_docstring_args(sphinx_multiline_function) + expected = { + 'data': 'The input data to process, which can be quite complex and may require special handling for optimal results' + } + self.assertEqual(result, expected) + + def test_numpy_style_docstring(self): + """Test NumPy-style docstring parsing.""" + + def numpy_function(x: float, y: float) -> float: + """Calculate distance. + + Parameters + ---------- + x : float + The x coordinate + y : float + The y coordinate + + Returns + ------- + float + The calculated distance + """ + return (x**2 + y**2) ** 0.5 + + result = _extract_docstring_args(numpy_function) + expected = {'x': 'The x coordinate', 'y': 'The y coordinate'} + self.assertEqual(result, expected) + + def test_mixed_style_preference(self): + """Test that Sphinx-style takes precedence when both styles are present.""" + + def mixed_function(param1: str, param2: int) -> str: + """Function with mixed documentation styles. + + :param param1: Sphinx-style description for param1 + :param param2: Sphinx-style description for param2 + + Args: + param1: Google-style description for param1 + param2: Google-style description for param2 + """ + return f'{param1}: {param2}' + + result = _extract_docstring_args(mixed_function) + expected = { + 'param1': 'Sphinx-style description for param1', + 'param2': 'Sphinx-style description for param2', + } + self.assertEqual(result, expected) + + def test_unsupported_format_warning(self): + """Test that unsupported docstring formats trigger a warning.""" + + def unsupported_function(param1: str, param2: int) -> str: + """Function with unsupported parameter documentation format. + + This function takes param1 which is a string input, + and param2 which is an integer argument. + """ + return f'{param1}: {param2}' + + with self.assertWarns(UserWarning) as warning_context: + result = _extract_docstring_args(unsupported_function) + + # Should return empty dict since no supported format found + self.assertEqual(result, {}) + + # Check warning message content + warning_message = str(warning_context.warning) + self.assertIn('unsupported_function', warning_message) + self.assertIn('supported format', warning_message) + self.assertIn('Google, NumPy, or Sphinx style', warning_message) + + def test_informal_style_warning(self): + """Test that informal parameter documentation triggers a warning.""" + + def informal_function(filename: str, mode: str) -> str: + """Open and read a file. + + The filename parameter should be the path to the file. + The mode parameter controls how the file is opened. + """ + return f'Reading {filename} in {mode} mode' + + with self.assertWarns(UserWarning): + result = _extract_docstring_args(informal_function) + + self.assertEqual(result, {}) + + def test_no_warning_for_no_params(self): + """Test that functions without parameter docs don't trigger warnings.""" + + def simple_function() -> str: + """Simple function with no parameters documented.""" + return 'hello' + + # Should not raise any warnings + with warnings.catch_warnings(): + warnings.simplefilter('error') # Turn warnings into errors + result = _extract_docstring_args(simple_function) + + self.assertEqual(result, {}) + + def test_no_warning_for_valid_formats(self): + """Test that valid formats don't trigger warnings.""" + + def google_function(param: str) -> str: + """Function with Google-style docs. + + Args: + param: A parameter description + """ + return param + + # Should not raise any warnings + with warnings.catch_warnings(): + warnings.simplefilter('error') # Turn warnings into errors + result = _extract_docstring_args(google_function) + + self.assertEqual(result, {'param': 'A parameter description'}) + + +class TestFunctionToJsonSchema(unittest.TestCase): + """Test the function_to_json_schema function.""" + + def test_simple_function(self): + """Test a simple function with basic types.""" + + def get_weather(location: str, unit: str = 'fahrenheit') -> str: + """Get weather for a location. + + Args: + location: The city name + unit: Temperature unit + """ + return f'Weather in {location}' + + result = function_to_json_schema(get_weather) + + # Check structure + self.assertEqual(result['type'], 'object') + self.assertIn('properties', result) + self.assertIn('required', result) + + # Check properties + self.assertIn('location', result['properties']) + self.assertIn('unit', result['properties']) + self.assertEqual(result['properties']['location']['type'], 'string') + self.assertEqual(result['properties']['unit']['type'], 'string') + + # Check descriptions + self.assertEqual(result['properties']['location']['description'], 'The city name') + self.assertEqual(result['properties']['unit']['description'], 'Temperature unit') + + # Check required (location is required, unit has default) + self.assertIn('location', result['required']) + self.assertNotIn('unit', result['required']) + + def test_function_with_complex_types(self): + """Test function with complex type hints.""" + + def search_data( + query: str, + limit: int = 10, + filters: Optional[List[str]] = None, + metadata: Dict[str, str] = None, + ) -> Dict[str, any]: + """Search for data. + + Args: + query: Search query + limit: Maximum results + filters: Optional search filters + metadata: Additional metadata + """ + return {} + + result = function_to_json_schema(search_data) + + # Check all parameters are present + props = result['properties'] + self.assertIn('query', props) + self.assertIn('limit', props) + self.assertIn('filters', props) + self.assertIn('metadata', props) + + # Check types + self.assertEqual(props['query']['type'], 'string') + self.assertEqual(props['limit']['type'], 'integer') + self.assertEqual(props['filters']['type'], 'array') + self.assertEqual(props['filters']['items']['type'], 'string') + self.assertEqual(props['metadata']['type'], 'object') + + # Check required (only query is required) + self.assertEqual(result['required'], ['query']) + + def test_function_with_enum(self): + """Test function with Enum parameter.""" + + class Priority(Enum): + LOW = 'low' + HIGH = 'high' + + def create_task(name: str, priority: Priority = Priority.LOW) -> str: + """Create a task. + + Args: + name: Task name + priority: Task priority level + """ + return f'Task: {name}' + + result = function_to_json_schema(create_task) + + # Check enum handling + priority_prop = result['properties']['priority'] + self.assertEqual(priority_prop['type'], 'string') + self.assertIn('enum', priority_prop) + self.assertEqual(set(priority_prop['enum']), {'low', 'high'}) + + def test_function_no_parameters(self): + """Test function with no parameters.""" + + def get_time() -> str: + """Get current time.""" + return '12:00' + + result = function_to_json_schema(get_time) + + self.assertEqual(result['type'], 'object') + self.assertEqual(result['properties'], {}) + self.assertEqual(result['required'], []) + + def test_function_with_args_kwargs(self): + """Test function with *args and **kwargs (should be ignored).""" + + def flexible_function(name: str, *args, **kwargs) -> str: + """A flexible function.""" + return name + + result = function_to_json_schema(flexible_function) + + # Should only include 'name', not *args or **kwargs + self.assertEqual(list(result['properties'].keys()), ['name']) + self.assertEqual(result['required'], ['name']) + + def test_realistic_weather_function(self): + """Test realistic weather API function.""" + + class Units(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + + def get_weather( + location: str, unit: Units = Units.FAHRENHEIT, include_forecast: bool = False + ) -> str: + """Get current weather for a location. + + Args: + location: The city and state or country + unit: Temperature unit preference + include_forecast: Whether to include 5-day forecast + """ + return f'Weather in {location}' + + result = function_to_json_schema(get_weather) + + # Check structure + self.assertEqual(result['type'], 'object') + props = result['properties'] + + # Check location (required string) + self.assertEqual(props['location']['type'], 'string') + self.assertEqual(props['location']['description'], 'The city and state or country') + self.assertIn('location', result['required']) + + # Check unit (optional enum) + self.assertEqual(props['unit']['type'], 'string') + self.assertEqual(set(props['unit']['enum']), {'celsius', 'fahrenheit'}) + self.assertNotIn('unit', result['required']) + + # Check forecast flag (optional boolean) + self.assertEqual(props['include_forecast']['type'], 'boolean') + self.assertNotIn('include_forecast', result['required']) + + def test_realistic_search_function(self): + """Test realistic search function with complex parameters.""" + + @dataclass + class SearchFilters: + category: Optional[str] = None + price_min: Optional[float] = None + price_max: Optional[float] = None + + def search_products( + query: str, + max_results: int = 20, + sort_by: str = 'relevance', + filters: Optional[SearchFilters] = None, + include_metadata: bool = True, + ) -> List[Dict[str, str]]: + """Search for products in catalog. + + Args: + query: Search query string + max_results: Maximum number of results to return + sort_by: Sort order (relevance, price, rating) + filters: Optional search filters + include_metadata: Whether to include product metadata + """ + return [] + + result = function_to_json_schema(search_products) + + props = result['properties'] + + # Check required query + self.assertEqual(props['query']['type'], 'string') + self.assertIn('query', result['required']) + + # Check optional integer with default + self.assertEqual(props['max_results']['type'], 'integer') + self.assertNotIn('max_results', result['required']) + + # Check string with default + self.assertEqual(props['sort_by']['type'], 'string') + self.assertNotIn('sort_by', result['required']) + + # Check optional dataclass + self.assertEqual(props['filters']['type'], 'object') + self.assertNotIn('filters', result['required']) + + # Check boolean with default + self.assertEqual(props['include_metadata']['type'], 'boolean') + self.assertNotIn('include_metadata', result['required']) + + def test_realistic_database_function(self): + """Test realistic database query function.""" + + def query_users( + filter_conditions: Dict[str, str], + limit: int = 100, + offset: int = 0, + order_by: Optional[str] = None, + include_inactive: bool = False, + ) -> List[Dict[str, any]]: + """Query users from database. + + Args: + filter_conditions: Key-value pairs for filtering + limit: Maximum number of users to return + offset: Number of records to skip + order_by: Field to sort by + include_inactive: Whether to include inactive users + """ + return [] + + result = function_to_json_schema(query_users) + + props = result['properties'] + + # Check required dict parameter + self.assertEqual(props['filter_conditions']['type'], 'object') + self.assertIn('filter_conditions', result['required']) + + # Check integer parameters with defaults + for param in ['limit', 'offset']: + self.assertEqual(props[param]['type'], 'integer') + self.assertNotIn(param, result['required']) + + # Check optional string + self.assertEqual(props['order_by']['type'], 'string') + self.assertNotIn('order_by', result['required']) + + # Check boolean flag + self.assertEqual(props['include_inactive']['type'], 'boolean') + self.assertNotIn('include_inactive', result['required']) + + def test_pydantic_function_parameter(self): + """Test function with Pydantic model parameter.""" + try: + from pydantic import BaseModel + + class UserProfile(BaseModel): + name: str + email: str + age: Optional[int] = None + preferences: Dict[str, bool] = {} + + def update_user(user_id: str, profile: UserProfile, notify: bool = True) -> str: + """Update user profile. + + Args: + user_id: Unique user identifier + profile: User profile data + notify: Whether to send notification + """ + return 'updated' + + result = function_to_json_schema(update_user) + + props = result['properties'] + + # Check required string + self.assertEqual(props['user_id']['type'], 'string') + self.assertIn('user_id', result['required']) + + # Check Pydantic model (should have proper schema) + self.assertIn('profile', props) + self.assertIn('profile', result['required']) + + # Check boolean flag + self.assertEqual(props['notify']['type'], 'boolean') + self.assertNotIn('notify', result['required']) + + except ImportError: + self.skipTest('Pydantic not available for testing') + + +class TestExtractDocstringSummary(unittest.TestCase): + """Test the extract_docstring_summary function.""" + + def test_simple_docstring(self): + """Test function with simple one-line docstring.""" + + def simple_function(): + """Simple one-line description.""" + pass + + result = extract_docstring_summary(simple_function) + self.assertEqual(result, 'Simple one-line description.') + + def test_full_docstring_with_extended_summary(self): + """Test function with full docstring including extended summary.""" + + def complex_function(): + """Get weather information for a specific location. + + This function retrieves current weather data including temperature, + humidity, and precipitation for the given location. + + Args: + location: The city or location to get weather for + unit: Temperature unit (celsius or fahrenheit) + + Returns: + Weather information as a string + + Raises: + ValueError: If location is invalid + """ + pass + + result = extract_docstring_summary(complex_function) + expected = ( + 'Get weather information for a specific location. ' + 'This function retrieves current weather data including temperature, ' + 'humidity, and precipitation for the given location.' + ) + self.assertEqual(result, expected) + + def test_multiline_summary_before_args(self): + """Test function with multiline summary that stops at Args section.""" + + def multiline_summary_function(): + """Complex function that does many things. + + This is an extended description that spans multiple lines + and provides more context about what the function does. + + Args: + param1: First parameter + """ + pass + + result = extract_docstring_summary(multiline_summary_function) + expected = ( + 'Complex function that does many things. ' + 'This is an extended description that spans multiple lines ' + 'and provides more context about what the function does.' + ) + self.assertEqual(result, expected) + + def test_no_docstring(self): + """Test function without docstring.""" + + def no_docstring_function(): + pass + + result = extract_docstring_summary(no_docstring_function) + self.assertIsNone(result) + + def test_empty_docstring(self): + """Test function with empty docstring.""" + + def empty_docstring_function(): + """""" + pass + + result = extract_docstring_summary(empty_docstring_function) + self.assertIsNone(result) + + def test_docstring_with_only_whitespace(self): + """Test function with docstring containing only whitespace.""" + + def whitespace_docstring_function(): + """ """ + pass + + result = extract_docstring_summary(whitespace_docstring_function) + self.assertIsNone(result) + + def test_docstring_stops_at_various_sections(self): + """Test that summary extraction stops at various section headers.""" + + def function_with_returns(): + """Function description. + + Returns: + Something useful + """ + pass + + def function_with_raises(): + """Function description. + + Raises: + ValueError: If something goes wrong + """ + pass + + def function_with_note(): + """Function description. + + Note: + This is important to remember + """ + pass + + # Test each section header + for func in [function_with_returns, function_with_raises, function_with_note]: + result = extract_docstring_summary(func) + self.assertEqual(result, 'Function description.') + + def test_docstring_with_parameters_section(self): + """Test docstring with Parameters section (alternative to Args).""" + + def function_with_parameters(): + """Process data efficiently. + + Parameters: + data: Input data to process + options: Processing options + """ + pass + + result = extract_docstring_summary(function_with_parameters) + self.assertEqual(result, 'Process data efficiently.') + + def test_docstring_with_example_section(self): + """Test docstring with Example section.""" + + def function_with_example(): + """Calculate the area of a circle. + + Example: + >>> calculate_area(5) + 78.54 + """ + pass + + result = extract_docstring_summary(function_with_example) + self.assertEqual(result, 'Calculate the area of a circle.') + + def test_case_insensitive_section_headers(self): + """Test that section header matching is case insensitive.""" + + def function_with_uppercase_args(): + """Function with uppercase section. + + ARGS: + param: A parameter + """ + pass + + result = extract_docstring_summary(function_with_uppercase_args) + self.assertEqual(result, 'Function with uppercase section.') + + def test_sphinx_style_summary_extraction(self): + """Test that Sphinx-style docstrings stop at :param: sections.""" + + def sphinx_function(): + """Calculate mathematical operations. + + This function performs various mathematical calculations + with high precision and error handling. + + :param x: First number + :param y: Second number + :returns: Calculation result + """ + pass + + result = extract_docstring_summary(sphinx_function) + expected = ( + 'Calculate mathematical operations. ' + 'This function performs various mathematical calculations ' + 'with high precision and error handling.' + ) + self.assertEqual(result, expected) + + def test_mixed_sphinx_google_summary(self): + """Test summary extraction stops at first section marker (Sphinx or Google).""" + + def mixed_function(): + """Process data with multiple algorithms. + + This is an extended description that provides + more context about the processing methods. + + :param data: Input data + + Args: + additional: More parameters + """ + pass + + result = extract_docstring_summary(mixed_function) + expected = ( + 'Process data with multiple algorithms. ' + 'This is an extended description that provides ' + 'more context about the processing methods.' + ) + self.assertEqual(result, expected) + + +class TestConversationToolsFunctionFromFunction(unittest.TestCase): + """Test the ConversationToolsFunction.from_function method.""" + + def test_from_function_basic(self): + """Test creating ConversationToolsFunction from a basic function.""" + + def test_function(param1: str, param2: int = 10): + """Test function for conversion. + + Args: + param1: First parameter + param2: Second parameter with default + """ + return f'{param1}: {param2}' + + result = ConversationToolsFunction.from_function(test_function) + + # Check basic properties + self.assertEqual(result.name, 'test_function') + self.assertEqual(result.description, 'Test function for conversion.') + self.assertIsInstance(result.parameters, dict) + + # Check that parameters schema was generated + self.assertEqual(result.parameters['type'], 'object') + self.assertIn('properties', result.parameters) + self.assertIn('required', result.parameters) + + def test_from_function_with_complex_docstring(self): + """Test from_function with complex docstring extracts only summary.""" + + def complex_function(location: str): + """Get weather information for a location. + + This function provides comprehensive weather data including + current conditions and forecasts. + + Args: + location: The location to get weather for + + Returns: + str: Weather information + + Raises: + ValueError: If location is invalid + + Example: + >>> get_weather("New York") + "Sunny, 72°F" + """ + return f'Weather for {location}' + + result = ConversationToolsFunction.from_function(complex_function) + + expected_description = ( + 'Get weather information for a location. ' + 'This function provides comprehensive weather data including ' + 'current conditions and forecasts.' + ) + self.assertEqual(result.description, expected_description) + + def test_from_function_no_docstring(self): + """Test from_function with function that has no docstring.""" + + def no_doc_function(param): + return param + + result = ConversationToolsFunction.from_function(no_doc_function) + + self.assertEqual(result.name, 'no_doc_function') + self.assertIsNone(result.description) + self.assertIsInstance(result.parameters, dict) + + def test_from_function_simple_docstring(self): + """Test from_function with simple one-line docstring.""" + + def simple_function(): + """Simple function description.""" + pass + + result = ConversationToolsFunction.from_function(simple_function) + + self.assertEqual(result.name, 'simple_function') + self.assertEqual(result.description, 'Simple function description.') + + def test_from_function_sphinx_style_summary(self): + """Test from_function extracts only summary from Sphinx-style docstring.""" + + def sphinx_function(location: str): + """Get weather information for a location. + + This function provides comprehensive weather data including + current conditions and forecasts using various APIs. + + :param location: The location to get weather for + :type location: str + :returns: Weather information string + :rtype: str + :raises ValueError: If location is invalid + """ + return f'Weather for {location}' + + result = ConversationToolsFunction.from_function(sphinx_function) + + expected_description = ( + 'Get weather information for a location. ' + 'This function provides comprehensive weather data including ' + 'current conditions and forecasts using various APIs.' + ) + self.assertEqual(result.description, expected_description) + + def test_from_function_google_style_summary(self): + """Test from_function extracts only summary from Google-style docstring.""" + + def google_function(data: str): + """Process input data efficiently. + + This function handles various data formats and applies + multiple processing algorithms for optimal results. + + Args: + data: The input data to process + + Returns: + str: Processed data string + + Raises: + ValueError: If data format is invalid + """ + return f'Processed {data}' + + result = ConversationToolsFunction.from_function(google_function) + + expected_description = ( + 'Process input data efficiently. ' + 'This function handles various data formats and applies ' + 'multiple processing algorithms for optimal results.' + ) + self.assertEqual(result.description, expected_description) + + +class TestIntegrationScenarios(unittest.TestCase): + """Test real-world integration scenarios.""" + + def test_restaurant_finder_scenario(self): + """Test the restaurant finder example from the documentation.""" + from enum import Enum + from typing import List, Optional + + class PriceRange(Enum): + BUDGET = 'budget' + MODERATE = 'moderate' + EXPENSIVE = 'expensive' + + def find_restaurants( + location: str, + cuisine: str = 'any', + price_range: PriceRange = PriceRange.MODERATE, + max_results: int = 5, + dietary_restrictions: Optional[List[str]] = None, + ) -> str: + """Find restaurants in a specific location. + + Args: + location: The city or neighborhood to search + cuisine: Type of cuisine (italian, chinese, mexican, etc.) + price_range: Budget preference for dining + max_results: Maximum number of restaurant recommendations + dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) + """ + return f'Found restaurants in {location}' + + schema = function_to_json_schema(find_restaurants) + + # Comprehensive validation + self.assertEqual(schema['type'], 'object') + + # Check all properties exist + props = schema['properties'] + self.assertIn('location', props) + self.assertIn('cuisine', props) + self.assertIn('price_range', props) + self.assertIn('max_results', props) + self.assertIn('dietary_restrictions', props) + + # Check types + self.assertEqual(props['location']['type'], 'string') + self.assertEqual(props['cuisine']['type'], 'string') + self.assertEqual(props['price_range']['type'], 'string') + self.assertEqual(props['max_results']['type'], 'integer') + self.assertEqual(props['dietary_restrictions']['type'], 'array') + self.assertEqual(props['dietary_restrictions']['items']['type'], 'string') + + # Check enum values + self.assertEqual(set(props['price_range']['enum']), {'budget', 'moderate', 'expensive'}) + + # Check descriptions + self.assertIn('description', props['location']) + self.assertIn('description', props['cuisine']) + self.assertIn('description', props['price_range']) + + # Check required (only location is required) + self.assertEqual(schema['required'], ['location']) + + def test_weather_api_scenario(self): + """Test a weather API scenario with validation.""" + from enum import Enum + from typing import Optional + + class Units(Enum): + CELSIUS = 'celsius' + FAHRENHEIT = 'fahrenheit' + KELVIN = 'kelvin' + + def get_weather_forecast( + latitude: float, + longitude: float, + units: Units = Units.CELSIUS, + days: int = 7, + include_hourly: bool = False, + api_key: Optional[str] = None, + ) -> Dict[str, any]: + """Get weather forecast for coordinates. + + Args: + latitude: Latitude coordinate + longitude: Longitude coordinate + units: Temperature units for response + days: Number of forecast days + include_hourly: Whether to include hourly forecasts + api_key: Optional API key override + """ + return {'forecast': []} + + schema = function_to_json_schema(get_weather_forecast) + + # Check numeric types + self.assertEqual(schema['properties']['latitude']['type'], 'number') + self.assertEqual(schema['properties']['longitude']['type'], 'number') + self.assertEqual(schema['properties']['days']['type'], 'integer') + self.assertEqual(schema['properties']['include_hourly']['type'], 'boolean') + + # Check enum + self.assertEqual(schema['properties']['units']['type'], 'string') + self.assertEqual( + set(schema['properties']['units']['enum']), {'celsius', 'fahrenheit', 'kelvin'} + ) + + # Check required fields + self.assertEqual(set(schema['required']), {'latitude', 'longitude'}) diff --git a/tests/clients/test_grpc_helpers.py b/tests/clients/test_grpc_helpers.py deleted file mode 100644 index 7520397d..00000000 --- a/tests/clients/test_grpc_helpers.py +++ /dev/null @@ -1,1312 +0,0 @@ -#!/usr/bin/env python3 - -""" -Tests for dapr.clients.grpc._helpers module. - -This module tests the function-to-JSON-schema helpers that provide -automatic tool creation from typed Python functions. -""" - -import unittest -import sys -import os -import warnings -from typing import Optional, List, Dict, Union -from enum import Enum -from dataclasses import dataclass - -# Add the project root to sys.path to import helpers directly -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - -# Import the helper functions from the new module to avoid circular imports -try: - from dapr.clients.grpc._schema_helpers import ( - python_type_to_json_schema, - extract_docstring_args, - function_to_json_schema, - extract_docstring_summary, - ) - from dapr.clients.grpc._conversation import ConversationToolsFunction - - HELPERS_AVAILABLE = True -except ImportError as e: - HELPERS_AVAILABLE = False - print(f'Warning: Could not import schema helpers: {e}') - - -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestPythonTypeToJsonSchema(unittest.TestCase): - """Test the python_type_to_json_schema function.""" - - def test_basic_types(self): - """Test conversion of basic Python types.""" - test_cases = [ - (str, {'type': 'string'}), - (int, {'type': 'integer'}), - (float, {'type': 'number'}), - (bool, {'type': 'boolean'}), - (bytes, {'type': 'string', 'format': 'byte'}), - ] - - for python_type, expected in test_cases: - with self.subTest(python_type=python_type): - result = python_type_to_json_schema(python_type) - self.assertEqual(result['type'], expected['type']) - if 'format' in expected: - self.assertEqual(result['format'], expected['format']) - - def test_optional_types(self): - """Test Optional[T] types (Union[T, None]).""" - # Optional[str] should resolve to string - result = python_type_to_json_schema(Optional[str]) - self.assertEqual(result['type'], 'string') - - # Optional[int] should resolve to integer - result = python_type_to_json_schema(Optional[int]) - self.assertEqual(result['type'], 'integer') - - def test_list_types(self): - """Test List[T] types.""" - # List[str] - result = python_type_to_json_schema(List[str]) - expected = {'type': 'array', 'items': {'type': 'string'}} - self.assertEqual(result, expected) - - # List[int] - result = python_type_to_json_schema(List[int]) - expected = {'type': 'array', 'items': {'type': 'integer'}} - self.assertEqual(result, expected) - - def test_dict_types(self): - """Test Dict[str, T] types.""" - result = python_type_to_json_schema(Dict[str, int]) - expected = {'type': 'object', 'additionalProperties': {'type': 'integer'}} - self.assertEqual(result, expected) - - def test_enum_types(self): - """Test Enum types.""" - - class Color(Enum): - RED = 'red' - GREEN = 'green' - BLUE = 'blue' - - result = python_type_to_json_schema(Color) - expected = {'type': 'string', 'enum': ['red', 'green', 'blue']} - self.assertEqual(result['type'], expected['type']) - self.assertEqual(set(result['enum']), set(expected['enum'])) - - def test_union_types(self): - """Test Union types.""" - result = python_type_to_json_schema(Union[str, int]) - self.assertIn('anyOf', result) - self.assertEqual(len(result['anyOf']), 2) - - # Should contain both string and integer schemas - types = [schema['type'] for schema in result['anyOf']] - self.assertIn('string', types) - self.assertIn('integer', types) - - def test_dataclass_types(self): - """Test dataclass types.""" - - @dataclass - class Person: - name: str - age: int = 25 - - result = python_type_to_json_schema(Person) - - self.assertEqual(result['type'], 'object') - self.assertIn('properties', result) - self.assertIn('required', result) - - # Check properties - self.assertIn('name', result['properties']) - self.assertIn('age', result['properties']) - self.assertEqual(result['properties']['name']['type'], 'string') - self.assertEqual(result['properties']['age']['type'], 'integer') - - # Check required fields (name is required, age has default) - self.assertIn('name', result['required']) - self.assertNotIn('age', result['required']) - - def test_pydantic_models(self): - """Test Pydantic model types.""" - try: - from pydantic import BaseModel - - class SearchParams(BaseModel): - query: str - limit: int = 10 - include_images: bool = False - tags: Optional[List[str]] = None - - result = python_type_to_json_schema(SearchParams) - - # Pydantic models should generate their own schema - self.assertIn('type', result) - # The exact structure depends on Pydantic version, but it should have properties - if 'properties' in result: - self.assertIn('query', result['properties']) - except ImportError: - self.skipTest('Pydantic not available for testing') - - def test_nested_types(self): - """Test complex nested type combinations.""" - # Optional[List[str]] - result = python_type_to_json_schema(Optional[List[str]]) - self.assertEqual(result['type'], 'array') - self.assertEqual(result['items']['type'], 'string') - - # List[Optional[int]] - result = python_type_to_json_schema(List[Optional[int]]) - self.assertEqual(result['type'], 'array') - self.assertEqual(result['items']['type'], 'integer') - - # Dict[str, List[int]] - result = python_type_to_json_schema(Dict[str, List[int]]) - self.assertEqual(result['type'], 'object') - self.assertEqual(result['additionalProperties']['type'], 'array') - self.assertEqual(result['additionalProperties']['items']['type'], 'integer') - - def test_complex_dataclass_with_nested_types(self): - """Test dataclass with complex nested types.""" - - @dataclass - class Address: - street: str - city: str - zipcode: Optional[str] = None - - @dataclass - class Person: - name: str - addresses: List[Address] - metadata: Dict[str, str] - tags: Optional[List[str]] = None - - result = python_type_to_json_schema(Person) - - self.assertEqual(result['type'], 'object') - self.assertIn('name', result['properties']) - self.assertIn('addresses', result['properties']) - self.assertIn('metadata', result['properties']) - self.assertIn('tags', result['properties']) - - # Check nested structures - self.assertEqual(result['properties']['addresses']['type'], 'array') - self.assertEqual(result['properties']['metadata']['type'], 'object') - self.assertEqual(result['properties']['tags']['type'], 'array') - - # Required fields - self.assertIn('name', result['required']) - self.assertIn('addresses', result['required']) - self.assertIn('metadata', result['required']) - self.assertNotIn('tags', result['required']) - - def test_enum_with_different_types(self): - """Test enums with different value types.""" - - class Status(Enum): - ACTIVE = 1 - INACTIVE = 0 - PENDING = 2 - - class Priority(Enum): - LOW = 'low' - MEDIUM = 'medium' - HIGH = 'high' - - # String enum - result = python_type_to_json_schema(Priority) - self.assertEqual(result['type'], 'string') - self.assertEqual(set(result['enum']), {'low', 'medium', 'high'}) - - # Integer enum - result = python_type_to_json_schema(Status) - self.assertEqual(result['type'], 'string') - self.assertEqual(set(result['enum']), {1, 0, 2}) - - def test_none_type(self): - """Test None type handling.""" - result = python_type_to_json_schema(type(None)) - self.assertEqual(result['type'], 'null') - - def test_unknown_type_fallback(self): - """Test fallback for unknown types.""" - - class CustomClass: - pass - - result = python_type_to_json_schema(CustomClass) - self.assertEqual(result['type'], 'string') - self.assertIn('Unknown type', result['description']) - - def test_realistic_function_types(self): - """Test types from realistic function signatures.""" - # Weather function parameters - result = python_type_to_json_schema(str) # location - self.assertEqual(result['type'], 'string') - - # Optional unit with enum - class TemperatureUnit(Enum): - CELSIUS = 'celsius' - FAHRENHEIT = 'fahrenheit' - - result = python_type_to_json_schema(Optional[TemperatureUnit]) - self.assertEqual(result['type'], 'string') - self.assertEqual(set(result['enum']), {'celsius', 'fahrenheit'}) - - # Search function with complex params - @dataclass - class SearchOptions: - max_results: int = 10 - include_metadata: bool = True - filters: Optional[Dict[str, str]] = None - - result = python_type_to_json_schema(SearchOptions) - self.assertEqual(result['type'], 'object') - self.assertIn('max_results', result['properties']) - self.assertIn('include_metadata', result['properties']) - self.assertIn('filters', result['properties']) - - def test_list_without_type_args(self): - """Test bare List type without type arguments.""" - result = python_type_to_json_schema(list) - self.assertEqual(result['type'], 'array') - self.assertNotIn('items', result) - - def test_dict_without_type_args(self): - """Test bare Dict type without type arguments.""" - result = python_type_to_json_schema(dict) - self.assertEqual(result['type'], 'object') - self.assertNotIn('additionalProperties', result) - - -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestExtractDocstringInfo(unittest.TestCase): - """Test the extract_docstring_info function.""" - - def test_google_style_docstring(self): - """Test Google-style docstring parsing.""" - - def sample_function(name: str, age: int) -> str: - """A sample function. - - Args: - name: The person's name - age: The person's age in years - """ - return f'{name} is {age}' - - result = extract_docstring_args(sample_function) - expected = {'name': "The person's name", 'age': "The person's age in years"} - self.assertEqual(result, expected) - - def test_no_docstring(self): - """Test function with no docstring.""" - - def no_doc_function(param): - pass - - result = extract_docstring_args(no_doc_function) - self.assertEqual(result, {}) - - def test_docstring_without_args(self): - """Test docstring without Args section.""" - - def simple_function(param): - """Just a simple function.""" - pass - - result = extract_docstring_args(simple_function) - self.assertEqual(result, {}) - - def test_multiline_param_description(self): - """Test parameter descriptions that span multiple lines.""" - - def complex_function(param1: str) -> str: - """A complex function. - - Args: - param1: This is a long description - that spans multiple lines - for testing purposes - """ - return param1 - - result = extract_docstring_args(complex_function) - expected = { - 'param1': 'This is a long description that spans multiple lines for testing purposes' - } - self.assertEqual(result, expected) - - def test_sphinx_style_docstring(self): - """Test Sphinx-style docstring parsing.""" - - def sphinx_function(location: str, unit: str) -> str: - """Get weather information. - - :param location: The city or location name - :param unit: Temperature unit (celsius or fahrenheit) - :type location: str - :type unit: str - :returns: Weather information string - :rtype: str - """ - return f'Weather in {location}' - - result = extract_docstring_args(sphinx_function) - expected = { - 'location': 'The city or location name', - 'unit': 'Temperature unit (celsius or fahrenheit)', - } - self.assertEqual(result, expected) - - def test_sphinx_style_with_parameter_keyword(self): - """Test Sphinx-style with :parameter: instead of :param:.""" - - def sphinx_function2(query: str, limit: int) -> str: - """Search for data. - - :parameter query: The search query string - :parameter limit: Maximum number of results - """ - return f'Results for {query}' - - result = extract_docstring_args(sphinx_function2) - expected = {'query': 'The search query string', 'limit': 'Maximum number of results'} - self.assertEqual(result, expected) - - def test_sphinx_style_multiline_descriptions(self): - """Test Sphinx-style with multi-line parameter descriptions.""" - - def sphinx_multiline_function(data: str) -> str: - """Process complex data. - - :param data: The input data to process, which can be - quite complex and may require special handling - for optimal results - :returns: Processed data - """ - return data - - result = extract_docstring_args(sphinx_multiline_function) - expected = { - 'data': 'The input data to process, which can be quite complex and may require special handling for optimal results' - } - self.assertEqual(result, expected) - - def test_numpy_style_docstring(self): - """Test NumPy-style docstring parsing.""" - - def numpy_function(x: float, y: float) -> float: - """Calculate distance. - - Parameters - ---------- - x : float - The x coordinate - y : float - The y coordinate - - Returns - ------- - float - The calculated distance - """ - return (x**2 + y**2) ** 0.5 - - result = extract_docstring_args(numpy_function) - expected = {'x': 'The x coordinate', 'y': 'The y coordinate'} - self.assertEqual(result, expected) - - def test_mixed_style_preference(self): - """Test that Sphinx-style takes precedence when both styles are present.""" - - def mixed_function(param1: str, param2: int) -> str: - """Function with mixed documentation styles. - - :param param1: Sphinx-style description for param1 - :param param2: Sphinx-style description for param2 - - Args: - param1: Google-style description for param1 - param2: Google-style description for param2 - """ - return f'{param1}: {param2}' - - result = extract_docstring_args(mixed_function) - expected = { - 'param1': 'Sphinx-style description for param1', - 'param2': 'Sphinx-style description for param2', - } - self.assertEqual(result, expected) - - def test_unsupported_format_warning(self): - """Test that unsupported docstring formats trigger a warning.""" - - def unsupported_function(param1: str, param2: int) -> str: - """Function with unsupported parameter documentation format. - - This function takes param1 which is a string input, - and param2 which is an integer argument. - """ - return f'{param1}: {param2}' - - with self.assertWarns(UserWarning) as warning_context: - result = extract_docstring_args(unsupported_function) - - # Should return empty dict since no supported format found - self.assertEqual(result, {}) - - # Check warning message content - warning_message = str(warning_context.warning) - self.assertIn('unsupported_function', warning_message) - self.assertIn('supported format', warning_message) - self.assertIn('Google, NumPy, or Sphinx style', warning_message) - - def test_informal_style_warning(self): - """Test that informal parameter documentation triggers a warning.""" - - def informal_function(filename: str, mode: str) -> str: - """Open and read a file. - - The filename parameter should be the path to the file. - The mode parameter controls how the file is opened. - """ - return f'Reading {filename} in {mode} mode' - - with self.assertWarns(UserWarning): - result = extract_docstring_args(informal_function) - - self.assertEqual(result, {}) - - def test_no_warning_for_no_params(self): - """Test that functions without parameter docs don't trigger warnings.""" - - def simple_function() -> str: - """Simple function with no parameters documented.""" - return 'hello' - - # Should not raise any warnings - with warnings.catch_warnings(): - warnings.simplefilter('error') # Turn warnings into errors - result = extract_docstring_args(simple_function) - - self.assertEqual(result, {}) - - def test_no_warning_for_valid_formats(self): - """Test that valid formats don't trigger warnings.""" - - def google_function(param: str) -> str: - """Function with Google-style docs. - - Args: - param: A parameter description - """ - return param - - # Should not raise any warnings - with warnings.catch_warnings(): - warnings.simplefilter('error') # Turn warnings into errors - result = extract_docstring_args(google_function) - - self.assertEqual(result, {'param': 'A parameter description'}) - - -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestFunctionToJsonSchema(unittest.TestCase): - """Test the function_to_json_schema function.""" - - def test_simple_function(self): - """Test a simple function with basic types.""" - - def get_weather(location: str, unit: str = 'fahrenheit') -> str: - """Get weather for a location. - - Args: - location: The city name - unit: Temperature unit - """ - return f'Weather in {location}' - - result = function_to_json_schema(get_weather) - - # Check structure - self.assertEqual(result['type'], 'object') - self.assertIn('properties', result) - self.assertIn('required', result) - - # Check properties - self.assertIn('location', result['properties']) - self.assertIn('unit', result['properties']) - self.assertEqual(result['properties']['location']['type'], 'string') - self.assertEqual(result['properties']['unit']['type'], 'string') - - # Check descriptions - self.assertEqual(result['properties']['location']['description'], 'The city name') - self.assertEqual(result['properties']['unit']['description'], 'Temperature unit') - - # Check required (location is required, unit has default) - self.assertIn('location', result['required']) - self.assertNotIn('unit', result['required']) - - def test_function_with_complex_types(self): - """Test function with complex type hints.""" - - def search_data( - query: str, - limit: int = 10, - filters: Optional[List[str]] = None, - metadata: Dict[str, str] = None, - ) -> Dict[str, any]: - """Search for data. - - Args: - query: Search query - limit: Maximum results - filters: Optional search filters - metadata: Additional metadata - """ - return {} - - result = function_to_json_schema(search_data) - - # Check all parameters are present - props = result['properties'] - self.assertIn('query', props) - self.assertIn('limit', props) - self.assertIn('filters', props) - self.assertIn('metadata', props) - - # Check types - self.assertEqual(props['query']['type'], 'string') - self.assertEqual(props['limit']['type'], 'integer') - self.assertEqual(props['filters']['type'], 'array') - self.assertEqual(props['filters']['items']['type'], 'string') - self.assertEqual(props['metadata']['type'], 'object') - - # Check required (only query is required) - self.assertEqual(result['required'], ['query']) - - def test_function_with_enum(self): - """Test function with Enum parameter.""" - - class Priority(Enum): - LOW = 'low' - HIGH = 'high' - - def create_task(name: str, priority: Priority = Priority.LOW) -> str: - """Create a task. - - Args: - name: Task name - priority: Task priority level - """ - return f'Task: {name}' - - result = function_to_json_schema(create_task) - - # Check enum handling - priority_prop = result['properties']['priority'] - self.assertEqual(priority_prop['type'], 'string') - self.assertIn('enum', priority_prop) - self.assertEqual(set(priority_prop['enum']), {'low', 'high'}) - - def test_function_no_parameters(self): - """Test function with no parameters.""" - - def get_time() -> str: - """Get current time.""" - return '12:00' - - result = function_to_json_schema(get_time) - - self.assertEqual(result['type'], 'object') - self.assertEqual(result['properties'], {}) - self.assertEqual(result['required'], []) - - def test_function_with_args_kwargs(self): - """Test function with *args and **kwargs (should be ignored).""" - - def flexible_function(name: str, *args, **kwargs) -> str: - """A flexible function.""" - return name - - result = function_to_json_schema(flexible_function) - - # Should only include 'name', not *args or **kwargs - self.assertEqual(list(result['properties'].keys()), ['name']) - self.assertEqual(result['required'], ['name']) - - def test_realistic_weather_function(self): - """Test realistic weather API function.""" - - class Units(Enum): - CELSIUS = 'celsius' - FAHRENHEIT = 'fahrenheit' - - def get_weather( - location: str, unit: Units = Units.FAHRENHEIT, include_forecast: bool = False - ) -> str: - """Get current weather for a location. - - Args: - location: The city and state or country - unit: Temperature unit preference - include_forecast: Whether to include 5-day forecast - """ - return f'Weather in {location}' - - result = function_to_json_schema(get_weather) - - # Check structure - self.assertEqual(result['type'], 'object') - props = result['properties'] - - # Check location (required string) - self.assertEqual(props['location']['type'], 'string') - self.assertEqual(props['location']['description'], 'The city and state or country') - self.assertIn('location', result['required']) - - # Check unit (optional enum) - self.assertEqual(props['unit']['type'], 'string') - self.assertEqual(set(props['unit']['enum']), {'celsius', 'fahrenheit'}) - self.assertNotIn('unit', result['required']) - - # Check forecast flag (optional boolean) - self.assertEqual(props['include_forecast']['type'], 'boolean') - self.assertNotIn('include_forecast', result['required']) - - def test_realistic_search_function(self): - """Test realistic search function with complex parameters.""" - - @dataclass - class SearchFilters: - category: Optional[str] = None - price_min: Optional[float] = None - price_max: Optional[float] = None - - def search_products( - query: str, - max_results: int = 20, - sort_by: str = 'relevance', - filters: Optional[SearchFilters] = None, - include_metadata: bool = True, - ) -> List[Dict[str, str]]: - """Search for products in catalog. - - Args: - query: Search query string - max_results: Maximum number of results to return - sort_by: Sort order (relevance, price, rating) - filters: Optional search filters - include_metadata: Whether to include product metadata - """ - return [] - - result = function_to_json_schema(search_products) - - props = result['properties'] - - # Check required query - self.assertEqual(props['query']['type'], 'string') - self.assertIn('query', result['required']) - - # Check optional integer with default - self.assertEqual(props['max_results']['type'], 'integer') - self.assertNotIn('max_results', result['required']) - - # Check string with default - self.assertEqual(props['sort_by']['type'], 'string') - self.assertNotIn('sort_by', result['required']) - - # Check optional dataclass - self.assertEqual(props['filters']['type'], 'object') - self.assertNotIn('filters', result['required']) - - # Check boolean with default - self.assertEqual(props['include_metadata']['type'], 'boolean') - self.assertNotIn('include_metadata', result['required']) - - def test_realistic_database_function(self): - """Test realistic database query function.""" - - def query_users( - filter_conditions: Dict[str, str], - limit: int = 100, - offset: int = 0, - order_by: Optional[str] = None, - include_inactive: bool = False, - ) -> List[Dict[str, any]]: - """Query users from database. - - Args: - filter_conditions: Key-value pairs for filtering - limit: Maximum number of users to return - offset: Number of records to skip - order_by: Field to sort by - include_inactive: Whether to include inactive users - """ - return [] - - result = function_to_json_schema(query_users) - - props = result['properties'] - - # Check required dict parameter - self.assertEqual(props['filter_conditions']['type'], 'object') - self.assertIn('filter_conditions', result['required']) - - # Check integer parameters with defaults - for param in ['limit', 'offset']: - self.assertEqual(props[param]['type'], 'integer') - self.assertNotIn(param, result['required']) - - # Check optional string - self.assertEqual(props['order_by']['type'], 'string') - self.assertNotIn('order_by', result['required']) - - # Check boolean flag - self.assertEqual(props['include_inactive']['type'], 'boolean') - self.assertNotIn('include_inactive', result['required']) - - def test_pydantic_function_parameter(self): - """Test function with Pydantic model parameter.""" - try: - from pydantic import BaseModel - - class UserProfile(BaseModel): - name: str - email: str - age: Optional[int] = None - preferences: Dict[str, bool] = {} - - def update_user(user_id: str, profile: UserProfile, notify: bool = True) -> str: - """Update user profile. - - Args: - user_id: Unique user identifier - profile: User profile data - notify: Whether to send notification - """ - return 'updated' - - result = function_to_json_schema(update_user) - - props = result['properties'] - - # Check required string - self.assertEqual(props['user_id']['type'], 'string') - self.assertIn('user_id', result['required']) - - # Check Pydantic model (should have proper schema) - self.assertIn('profile', props) - self.assertIn('profile', result['required']) - - # Check boolean flag - self.assertEqual(props['notify']['type'], 'boolean') - self.assertNotIn('notify', result['required']) - - except ImportError: - self.skipTest('Pydantic not available for testing') - - -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestExtractDocstringSummary(unittest.TestCase): - """Test the extract_docstring_summary function.""" - - def test_simple_docstring(self): - """Test function with simple one-line docstring.""" - - def simple_function(): - """Simple one-line description.""" - pass - - result = extract_docstring_summary(simple_function) - self.assertEqual(result, 'Simple one-line description.') - - def test_full_docstring_with_extended_summary(self): - """Test function with full docstring including extended summary.""" - - def complex_function(): - """Get weather information for a specific location. - - This function retrieves current weather data including temperature, - humidity, and precipitation for the given location. - - Args: - location: The city or location to get weather for - unit: Temperature unit (celsius or fahrenheit) - - Returns: - Weather information as a string - - Raises: - ValueError: If location is invalid - """ - pass - - result = extract_docstring_summary(complex_function) - expected = ( - 'Get weather information for a specific location. ' - 'This function retrieves current weather data including temperature, ' - 'humidity, and precipitation for the given location.' - ) - self.assertEqual(result, expected) - - def test_multiline_summary_before_args(self): - """Test function with multiline summary that stops at Args section.""" - - def multiline_summary_function(): - """Complex function that does many things. - - This is an extended description that spans multiple lines - and provides more context about what the function does. - - Args: - param1: First parameter - """ - pass - - result = extract_docstring_summary(multiline_summary_function) - expected = ( - 'Complex function that does many things. ' - 'This is an extended description that spans multiple lines ' - 'and provides more context about what the function does.' - ) - self.assertEqual(result, expected) - - def test_no_docstring(self): - """Test function without docstring.""" - - def no_docstring_function(): - pass - - result = extract_docstring_summary(no_docstring_function) - self.assertIsNone(result) - - def test_empty_docstring(self): - """Test function with empty docstring.""" - - def empty_docstring_function(): - """""" - pass - - result = extract_docstring_summary(empty_docstring_function) - self.assertIsNone(result) - - def test_docstring_with_only_whitespace(self): - """Test function with docstring containing only whitespace.""" - - def whitespace_docstring_function(): - """ """ - pass - - result = extract_docstring_summary(whitespace_docstring_function) - self.assertIsNone(result) - - def test_docstring_stops_at_various_sections(self): - """Test that summary extraction stops at various section headers.""" - - def function_with_returns(): - """Function description. - - Returns: - Something useful - """ - pass - - def function_with_raises(): - """Function description. - - Raises: - ValueError: If something goes wrong - """ - pass - - def function_with_note(): - """Function description. - - Note: - This is important to remember - """ - pass - - # Test each section header - for func in [function_with_returns, function_with_raises, function_with_note]: - result = extract_docstring_summary(func) - self.assertEqual(result, 'Function description.') - - def test_docstring_with_parameters_section(self): - """Test docstring with Parameters section (alternative to Args).""" - - def function_with_parameters(): - """Process data efficiently. - - Parameters: - data: Input data to process - options: Processing options - """ - pass - - result = extract_docstring_summary(function_with_parameters) - self.assertEqual(result, 'Process data efficiently.') - - def test_docstring_with_example_section(self): - """Test docstring with Example section.""" - - def function_with_example(): - """Calculate the area of a circle. - - Example: - >>> calculate_area(5) - 78.54 - """ - pass - - result = extract_docstring_summary(function_with_example) - self.assertEqual(result, 'Calculate the area of a circle.') - - def test_case_insensitive_section_headers(self): - """Test that section header matching is case insensitive.""" - - def function_with_uppercase_args(): - """Function with uppercase section. - - ARGS: - param: A parameter - """ - pass - - result = extract_docstring_summary(function_with_uppercase_args) - self.assertEqual(result, 'Function with uppercase section.') - - def test_sphinx_style_summary_extraction(self): - """Test that Sphinx-style docstrings stop at :param: sections.""" - - def sphinx_function(): - """Calculate mathematical operations. - - This function performs various mathematical calculations - with high precision and error handling. - - :param x: First number - :param y: Second number - :returns: Calculation result - """ - pass - - result = extract_docstring_summary(sphinx_function) - expected = ( - 'Calculate mathematical operations. ' - 'This function performs various mathematical calculations ' - 'with high precision and error handling.' - ) - self.assertEqual(result, expected) - - def test_mixed_sphinx_google_summary(self): - """Test summary extraction stops at first section marker (Sphinx or Google).""" - - def mixed_function(): - """Process data with multiple algorithms. - - This is an extended description that provides - more context about the processing methods. - - :param data: Input data - - Args: - additional: More parameters - """ - pass - - result = extract_docstring_summary(mixed_function) - expected = ( - 'Process data with multiple algorithms. ' - 'This is an extended description that provides ' - 'more context about the processing methods.' - ) - self.assertEqual(result, expected) - - -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestConversationToolsFunctionFromFunction(unittest.TestCase): - """Test the ConversationToolsFunction.from_function method.""" - - def test_from_function_basic(self): - """Test creating ConversationToolsFunction from a basic function.""" - - def test_function(param1: str, param2: int = 10): - """Test function for conversion. - - Args: - param1: First parameter - param2: Second parameter with default - """ - return f'{param1}: {param2}' - - result = ConversationToolsFunction.from_function(test_function) - - # Check basic properties - self.assertEqual(result.name, 'test_function') - self.assertEqual(result.description, 'Test function for conversion.') - self.assertIsInstance(result.parameters, dict) - - # Check that parameters schema was generated - self.assertEqual(result.parameters['type'], 'object') - self.assertIn('properties', result.parameters) - self.assertIn('required', result.parameters) - - def test_from_function_with_complex_docstring(self): - """Test from_function with complex docstring extracts only summary.""" - - def complex_function(location: str): - """Get weather information for a location. - - This function provides comprehensive weather data including - current conditions and forecasts. - - Args: - location: The location to get weather for - - Returns: - str: Weather information - - Raises: - ValueError: If location is invalid - - Example: - >>> get_weather("New York") - "Sunny, 72°F" - """ - return f'Weather for {location}' - - result = ConversationToolsFunction.from_function(complex_function) - - expected_description = ( - 'Get weather information for a location. ' - 'This function provides comprehensive weather data including ' - 'current conditions and forecasts.' - ) - self.assertEqual(result.description, expected_description) - - def test_from_function_no_docstring(self): - """Test from_function with function that has no docstring.""" - - def no_doc_function(param): - return param - - result = ConversationToolsFunction.from_function(no_doc_function) - - self.assertEqual(result.name, 'no_doc_function') - self.assertIsNone(result.description) - self.assertIsInstance(result.parameters, dict) - - def test_from_function_simple_docstring(self): - """Test from_function with simple one-line docstring.""" - - def simple_function(): - """Simple function description.""" - pass - - result = ConversationToolsFunction.from_function(simple_function) - - self.assertEqual(result.name, 'simple_function') - self.assertEqual(result.description, 'Simple function description.') - - def test_from_function_sphinx_style_summary(self): - """Test from_function extracts only summary from Sphinx-style docstring.""" - - def sphinx_function(location: str): - """Get weather information for a location. - - This function provides comprehensive weather data including - current conditions and forecasts using various APIs. - - :param location: The location to get weather for - :type location: str - :returns: Weather information string - :rtype: str - :raises ValueError: If location is invalid - """ - return f'Weather for {location}' - - result = ConversationToolsFunction.from_function(sphinx_function) - - expected_description = ( - 'Get weather information for a location. ' - 'This function provides comprehensive weather data including ' - 'current conditions and forecasts using various APIs.' - ) - self.assertEqual(result.description, expected_description) - - def test_from_function_google_style_summary(self): - """Test from_function extracts only summary from Google-style docstring.""" - - def google_function(data: str): - """Process input data efficiently. - - This function handles various data formats and applies - multiple processing algorithms for optimal results. - - Args: - data: The input data to process - - Returns: - str: Processed data string - - Raises: - ValueError: If data format is invalid - """ - return f'Processed {data}' - - result = ConversationToolsFunction.from_function(google_function) - - expected_description = ( - 'Process input data efficiently. ' - 'This function handles various data formats and applies ' - 'multiple processing algorithms for optimal results.' - ) - self.assertEqual(result.description, expected_description) - - -@unittest.skipIf(not HELPERS_AVAILABLE, 'Helpers not available due to import issues') -class TestIntegrationScenarios(unittest.TestCase): - """Test real-world integration scenarios.""" - - def test_restaurant_finder_scenario(self): - """Test the restaurant finder example from the documentation.""" - from enum import Enum - from typing import List, Optional - - class PriceRange(Enum): - BUDGET = 'budget' - MODERATE = 'moderate' - EXPENSIVE = 'expensive' - - def find_restaurants( - location: str, - cuisine: str = 'any', - price_range: PriceRange = PriceRange.MODERATE, - max_results: int = 5, - dietary_restrictions: Optional[List[str]] = None, - ) -> str: - """Find restaurants in a specific location. - - Args: - location: The city or neighborhood to search - cuisine: Type of cuisine (italian, chinese, mexican, etc.) - price_range: Budget preference for dining - max_results: Maximum number of restaurant recommendations - dietary_restrictions: Special dietary needs (vegetarian, gluten-free, etc.) - """ - return f'Found restaurants in {location}' - - schema = function_to_json_schema(find_restaurants) - - # Comprehensive validation - self.assertEqual(schema['type'], 'object') - - # Check all properties exist - props = schema['properties'] - self.assertIn('location', props) - self.assertIn('cuisine', props) - self.assertIn('price_range', props) - self.assertIn('max_results', props) - self.assertIn('dietary_restrictions', props) - - # Check types - self.assertEqual(props['location']['type'], 'string') - self.assertEqual(props['cuisine']['type'], 'string') - self.assertEqual(props['price_range']['type'], 'string') - self.assertEqual(props['max_results']['type'], 'integer') - self.assertEqual(props['dietary_restrictions']['type'], 'array') - self.assertEqual(props['dietary_restrictions']['items']['type'], 'string') - - # Check enum values - self.assertEqual(set(props['price_range']['enum']), {'budget', 'moderate', 'expensive'}) - - # Check descriptions - self.assertIn('description', props['location']) - self.assertIn('description', props['cuisine']) - self.assertIn('description', props['price_range']) - - # Check required (only location is required) - self.assertEqual(schema['required'], ['location']) - - def test_weather_api_scenario(self): - """Test a weather API scenario with validation.""" - from enum import Enum - from typing import Optional - - class Units(Enum): - CELSIUS = 'celsius' - FAHRENHEIT = 'fahrenheit' - KELVIN = 'kelvin' - - def get_weather_forecast( - latitude: float, - longitude: float, - units: Units = Units.CELSIUS, - days: int = 7, - include_hourly: bool = False, - api_key: Optional[str] = None, - ) -> Dict[str, any]: - """Get weather forecast for coordinates. - - Args: - latitude: Latitude coordinate - longitude: Longitude coordinate - units: Temperature units for response - days: Number of forecast days - include_hourly: Whether to include hourly forecasts - api_key: Optional API key override - """ - return {'forecast': []} - - schema = function_to_json_schema(get_weather_forecast) - - # Check numeric types - self.assertEqual(schema['properties']['latitude']['type'], 'number') - self.assertEqual(schema['properties']['longitude']['type'], 'number') - self.assertEqual(schema['properties']['days']['type'], 'integer') - self.assertEqual(schema['properties']['include_hourly']['type'], 'boolean') - - # Check enum - self.assertEqual(schema['properties']['units']['type'], 'string') - self.assertEqual( - set(schema['properties']['units']['enum']), {'celsius', 'fahrenheit', 'kelvin'} - ) - - # Check required fields - self.assertEqual(set(schema['required']), {'latitude', 'longitude'}) - - -class TestFallbackWhenImportsUnavailable(unittest.TestCase): - """Test fallback behavior when imports are not available.""" - - def test_imports_status(self): - """Test and report the status of helper imports.""" - if HELPERS_AVAILABLE: - print('✅ Helper functions successfully imported') - print('✅ All function-to-schema features are testable') - else: - print('⚠️ Helper functions not available due to circular imports') - print("⚠️ This is expected during development and doesn't affect functionality") - print('✅ Tests are properly skipped when imports fail') - - # This test always passes - it's just for reporting - self.assertTrue(True) - - -if __name__ == '__main__': - # Print import status - if HELPERS_AVAILABLE: - print('🧪 Running comprehensive tests for function-to-JSON-schema helpers') - else: - print('⚠️ Running limited tests due to import issues (this is expected)') - - unittest.main(verbosity=2) From 0d275e4376c11523a6db13af9c5454ec55c1564c Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:37:34 -0500 Subject: [PATCH 18/29] make indent line method doc more dev friendly Co-authored-by: Elena Kolevska Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/conversation.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 09594292..2a979a12 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -49,7 +49,16 @@ class ConversationMessageContent: def _indent_lines(title: str, text: str, indent: int) -> str: - """Indent lines of text.""" + """ + Indent lines of text. + Example: + >>> print("foo") + foo + >>> print(_indent_lines("Description", "This is a long\nmultiline\ntext block", 4)) + Description: This is a long + multiline + text block + """ indent_after_first_line = indent + len(title) + 2 lines = text.splitlines() if text is not None else [''] first = lines[0] if lines else '' From a41a413b47a2efea276045dce07387482585a95e Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 25 Aug 2025 08:49:02 -0500 Subject: [PATCH 19/29] tackle some feedback, still missing unit tests Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/__init__.py | 0 dapr/aio/clients/grpc/client.py | 45 +++-------- dapr/clients/grpc/_conversation_helpers.py | 19 ++--- dapr/clients/grpc/client.py | 39 +--------- dapr/clients/grpc/conversation.py | 78 +++++++++++++------- dapr/conf/global_settings.py | 8 ++ examples/conversation/conversation_alpha2.py | 22 +----- 7 files changed, 84 insertions(+), 127 deletions(-) delete mode 100644 dapr/__init__.py diff --git a/dapr/__init__.py b/dapr/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index a8e88757..19062edd 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -42,7 +42,6 @@ from dapr.aio.clients.grpc.subscription import Subscription from dapr.clients.exceptions import DaprInternalError, DaprGrpcError -from dapr.clients.grpc._conversation_helpers import _generate_unique_tool_call_id from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus @@ -105,14 +104,6 @@ StartWorkflowResponse, TopicEventResponse, ) -from dapr.clients.grpc.conversation import ( - ConversationResult, - ConversationResultAlpha2Message, - ConversationResultAlpha2Choices, - ConversationResultAlpha2, - ConversationResponse, - ConversationResponseAlpha2, -) class DaprGrpcClientAsync: @@ -1739,7 +1730,7 @@ async def converse_alpha1( metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, - ) -> ConversationResponse: + ) -> conversation.ConversationResponseAlpha1: """Invoke an LLM using the conversation API (Alpha). Args: @@ -1779,11 +1770,11 @@ async def converse_alpha1( response = await self._stub.ConverseAlpha1(request) outputs = [ - ConversationResult(result=output.result, parameters=output.parameters) + conversation.ConversationResultAlpha1(result=output.result, parameters=output.parameters) for output in response.outputs ] - return ConversationResponse(context_id=response.contextID, outputs=outputs) + return conversation.ConversationResponseAlpha1(context_id=response.contextID, outputs=outputs) except grpc.aio.AioRpcError as err: raise DaprGrpcError(err) from err @@ -1794,13 +1785,13 @@ async def converse_alpha2( inputs: List[conversation.ConversationInputAlpha2], *, context_id: Optional[str] = None, - parameters: Optional[Dict[str, GrpcAny]] = None, + parameters: Optional[Dict[str, Union[GrpcAny, Any]]] = None, metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, tools: Optional[List[conversation.ConversationTools]] = None, tool_choice: Optional[str] = None, - ) -> ConversationResponseAlpha2: + ) -> conversation.ConversationResponseAlpha2: """Invoke an LLM using the conversation API (Alpha2) with tool calling support. Args: @@ -1870,28 +1861,12 @@ async def converse_alpha2( request.tool_choice = tool_choice try: - response = await self._stub.ConverseAlpha2(request) - - outputs = [] - for output in response.outputs: - choices = [] - for choice in output.choices: - # workaround for some issues with missing tool ID in some providers (ie: Gemini) - for i, tool_call in enumerate(choice.message.tool_calls): - if not tool_call.id: - choice.message.tool_calls[i].id = _generate_unique_tool_call_id() - choices.append( - ConversationResultAlpha2Choices( - finish_reason=choice.finish_reason, - index=choice.index, - message=ConversationResultAlpha2Message( - content=choice.message.content, tool_calls=choice.message.tool_calls - ), - ) - ) - outputs.append(ConversationResultAlpha2(choices=choices)) + response, call = await self.retry_policy.run_rpc_async( + self._stub.ConverseAlpha2, request) + + outputs = conversation._get_outputs_from_grpc_response(response) - return ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) + return conversation.ConversationResponseAlpha2(context_id=response.context_id, outputs=outputs) except grpc.aio.AioRpcError as err: raise DaprGrpcError(err) from err diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index 9af1a89c..40a0e8d6 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -34,8 +34,10 @@ cast, ) -from dapr.clients.grpc.conversation import Params -import os +from dapr.conf import settings + +# duplicated from conversation to avoid circular import +Params = Union[Mapping[str, Any], Sequence[Any], None] """ Tool Calling Helpers for Dapr Conversation API. @@ -47,13 +49,6 @@ having to manually define the JSON schema for each tool. """ -# Configuration for handling large enums to avoid massive JSON schemas that can exceed LLM token limits -_MAX_ENUM_ITEMS = int(os.getenv('DAPR_CONVERSATION_MAX_ENUM_ITEMS', '100')) -_LARGE_ENUM_BEHAVIOR = os.getenv('DAPR_CONVERSATION_LARGE_ENUM_BEHAVIOR', 'string').lower() -# Allowed behaviors: 'string' (default), 'error' -if _LARGE_ENUM_BEHAVIOR not in {'string', 'error'}: - _LARGE_ENUM_BEHAVIOR = 'string' - def _python_type_to_json_schema(python_type: Any, field_name: str = '') -> Dict[str, Any]: """Convert a Python type hint to JSON schema format. @@ -183,13 +178,13 @@ def _json_primitive_type(v: Any) -> str: members = [] count = len(members) # If enum is small enough, include full enum list (current behavior) - if count <= _MAX_ENUM_ITEMS: + if count <= settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS: return {'type': 'string', 'enum': [item.value for item in members]} # Large enum handling - if _LARGE_ENUM_BEHAVIOR == 'error': + if settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR == 'error': raise ValueError( f"Enum '{getattr(python_type, '__name__', str(python_type))}' has {count} members, " - f"exceeding DAPR_CONVERSATION_MAX_ENUM_ITEMS={_MAX_ENUM_ITEMS}. " + f"exceeding DAPR_CONVERSATION_MAX_ENUM_ITEMS={settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS}. " f"Either reduce the enum size or set DAPR_CONVERSATION_LARGE_ENUM_BEHAVIOR=string to allow compact schema." ) # Default behavior: compact schema as a string with helpful context and a few examples diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index bc9d53bc..e50d89e8 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -40,7 +40,6 @@ ) from dapr.clients.exceptions import DaprInternalError, DaprGrpcError -from dapr.clients.grpc._conversation_helpers import _generate_unique_tool_call_id from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions from dapr.clients.grpc.subscription import Subscription, StreamInactiveError @@ -1734,7 +1733,7 @@ def converse_alpha1( metadata: Optional[Dict[str, str]] = None, scrub_pii: Optional[bool] = None, temperature: Optional[float] = None, - ) -> conversation.ConversationResponse: + ) -> conversation.ConversationResponseAlpha1: """Invoke an LLM using the conversation API (Alpha). Args: @@ -1775,11 +1774,11 @@ def converse_alpha1( response, call = self.retry_policy.run_rpc(self._stub.ConverseAlpha1.with_call, request) outputs = [ - conversation.ConversationResult(result=output.result, parameters=output.parameters) + conversation.ConversationResultAlpha1(result=output.result, parameters=output.parameters) for output in response.outputs ] - return conversation.ConversationResponse(context_id=response.contextID, outputs=outputs) + return conversation.ConversationResponseAlpha1(context_id=response.contextID, outputs=outputs) except RpcError as err: raise DaprGrpcError(err) from err @@ -1869,37 +1868,7 @@ def converse_alpha2( response, call = self.retry_policy.run_rpc(self._stub.ConverseAlpha2.with_call, request) # Convert response to our format - outputs = [] - for output in response.outputs: - choices = [] - for choice in output.choices: - # Convert tool calls from response - tool_calls = [] - for tool_call in choice.message.tool_calls: - function_call = conversation.ConversationToolCallsOfFunction( - name=tool_call.function.name, arguments=tool_call.function.arguments - ) - if not tool_call.id: - tool_call.id = _generate_unique_tool_call_id() - tool_calls.append( - conversation.ConversationToolCalls( - id=tool_call.id, function=function_call - ) - ) - - result_message = conversation.ConversationResultAlpha2Message( - content=choice.message.content, tool_calls=tool_calls - ) - - choices.append( - conversation.ConversationResultAlpha2Choices( - finish_reason=choice.finish_reason, - index=choice.index, - message=result_message, - ) - ) - - outputs.append(conversation.ConversationResultAlpha2(choices=choices)) + outputs = conversation._get_outputs_from_grpc_response(response) return conversation.ConversationResponseAlpha2( context_id=response.context_id, outputs=outputs diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 2a979a12..97642fba 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -23,6 +23,7 @@ from google.protobuf.any_pb2 import Any as GrpcAny from dapr.clients.grpc import _conversation_helpers as conv_helpers +from dapr.clients.grpc._conversation_helpers import _generate_unique_tool_call_id from dapr.proto import api_v1 Params = Union[Mapping[str, Any], Sequence[Any], None] @@ -68,13 +69,8 @@ def _indent_lines(title: str, text: str, indent: int) -> str: return f'{indent * " "}{title}: {first}{rest}' -@dataclass -class ConversationMessageOfDeveloper: - """Developer message content.""" - - name: Optional[str] = None - content: List[ConversationMessageContent] = field(default_factory=list) - +class UserTracePrintMixin: + """Mixin for trace_print for text based message content from user to LLM.""" def trace_print(self, indent: int = 0) -> None: base = ' ' * indent if self.name: @@ -84,34 +80,28 @@ def trace_print(self, indent: int = 0) -> None: @dataclass -class ConversationMessageOfSystem: - """System message content.""" +class ConversationMessageOfDeveloper(UserTracePrintMixin): + """Developer message content.""" name: Optional[str] = None content: List[ConversationMessageContent] = field(default_factory=list) - def trace_print(self, indent: int = 0) -> None: - base = ' ' * indent - if self.name: - print(f'{base}name: {self.name}') - for i, c in enumerate(self.content): - print(_indent_lines(f'content[{i}]', c.text, indent)) + +@dataclass +class ConversationMessageOfSystem(UserTracePrintMixin): + """System message content.""" + + name: Optional[str] = None + content: List[ConversationMessageContent] = field(default_factory=list) @dataclass -class ConversationMessageOfUser: +class ConversationMessageOfUser(UserTracePrintMixin): """User message content.""" name: Optional[str] = None content: List[ConversationMessageContent] = field(default_factory=list) - def trace_print(self, indent: int = 0) -> None: - base = ' ' * indent - if self.name: - print(f'{base}name: {self.name}') - for i, c in enumerate(self.content): - print(_indent_lines(f'content[{i}]', c.text, indent)) - @dataclass class ConversationToolCallsOfFunction: @@ -315,7 +305,7 @@ def from_function(cls, func: Callable, register: bool = True) -> 'ConversationTo @dataclass -class ConversationResult: +class ConversationResultAlpha1: """One of the outputs to a request to the conversation API.""" result: str @@ -347,11 +337,11 @@ class ConversationResultAlpha2: @dataclass -class ConversationResponse: +class ConversationResponseAlpha1: """Response to a request from the conversation API.""" context_id: Optional[str] - outputs: List[ConversationResult] + outputs: List[ConversationResultAlpha1] @dataclass @@ -579,3 +569,39 @@ def create_tool_message(tool_id: str, name: str, content: Any) -> ConversationMe tool_id=tool_id, name=name, content=[ConversationMessageContent(text=content)] ) ) + + +def _get_outputs_from_grpc_response(response: api_v1.ConversationResponseAlpha2) -> List[ConversationResultAlpha2]: + """Helper to get outputs from a Converse gRPC response from dapr sidecar.""" + outputs: List[ConversationResultAlpha2] = [] + for output in response.outputs: + choices = [] + for choice in output.choices: + # Convert tool calls from response + tool_calls = [] + for tool_call in choice.message.tool_calls: + function_call = ConversationToolCallsOfFunction( + name=tool_call.function.name, arguments=tool_call.function.arguments + ) + if not tool_call.id: + tool_call.id = _generate_unique_tool_call_id() + tool_calls.append( + ConversationToolCalls( + id=tool_call.id, function=function_call + ) + ) + + result_message = ConversationResultAlpha2Message( + content=choice.message.content, tool_calls=tool_calls + ) + + choices.append( + ConversationResultAlpha2Choices( + finish_reason=choice.finish_reason, + index=choice.index, + message=result_message, + ) + ) + + outputs.append(ConversationResultAlpha2(choices=choices)) + return outputs diff --git a/dapr/conf/global_settings.py b/dapr/conf/global_settings.py index 43bb51f6..5a64e5d4 100644 --- a/dapr/conf/global_settings.py +++ b/dapr/conf/global_settings.py @@ -33,3 +33,11 @@ DAPR_API_METHOD_INVOCATION_PROTOCOL = 'http' DAPR_HTTP_TIMEOUT_SECONDS = 60 + +# ----- Conversation API settings ------ + +# Configuration for handling large enums to avoid massive JSON schemas that can exceed LLM token limits +DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 100 +# What to do when an enum has more than DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS items. Convert to String message or raise an exception +# possible values: 'string' (default), 'error' +DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'string' diff --git a/examples/conversation/conversation_alpha2.py b/examples/conversation/conversation_alpha2.py index 7d3c7111..a17148f9 100644 --- a/examples/conversation/conversation_alpha2.py +++ b/examples/conversation/conversation_alpha2.py @@ -13,31 +13,15 @@ from dapr.clients import DaprClient from dapr.clients.grpc.conversation import ( ConversationInputAlpha2, - ConversationMessage, - ConversationMessageContent, - ConversationMessageOfUser, + create_user_message, ) with DaprClient() as d: inputs = [ ConversationInputAlpha2( - messages=[ - ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text="What's Dapr?")] - ) - ) - ] - ), + messages=[create_user_message("What's Dapr?")], scrub_pii=True), ConversationInputAlpha2( - messages=[ - ConversationMessage( - of_user=ConversationMessageOfUser( - content=[ConversationMessageContent(text='Give a brief overview.')] - ) - ) - ] - ), + messages=[create_user_message('Give a brief overview.')], scrub_pii=True), ] metadata = { From 5db8a56e9e69030c94a91e67f04e8124fef904e8 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:32:12 -0500 Subject: [PATCH 20/29] add unit test to convert_value_to_struct Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- tests/clients/test_dappr_grpc_helpers.py | 109 +++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/clients/test_dappr_grpc_helpers.py diff --git a/tests/clients/test_dappr_grpc_helpers.py b/tests/clients/test_dappr_grpc_helpers.py new file mode 100644 index 00000000..ec60dff5 --- /dev/null +++ b/tests/clients/test_dappr_grpc_helpers.py @@ -0,0 +1,109 @@ +import base64 +import unittest + +from google.protobuf.struct_pb2 import Struct +from google.protobuf import json_format +from google.protobuf.json_format import ParseError + +from dapr.clients.grpc._helpers import convert_value_to_struct + + +class TestConvertValueToStruct(unittest.TestCase): + def test_struct_passthrough_same_instance(self): + # Prepare a Struct + original = Struct() + json_format.ParseDict({"a": 1, "b": "x"}, original) + + # It should return the exact same instance + result = convert_value_to_struct(original) + self.assertIs(result, original) + + def test_simple_and_nested_dict_conversion(self): + payload = { + "a": "b", + "n": 3, + "t": True, + "f": 1.5, + "none": None, + "list": [1, "x", False, None], + "obj": {"k": "v", "inner": {"i": 2, "j": None}}, + } + struct = convert_value_to_struct(payload) + + # Convert back to dict to assert equivalence + back = json_format.MessageToDict(struct, preserving_proto_field_name=True) + self.assertEqual(back, { + "a": "b", + "n": 3, + "t": True, + "f": 1.5, + "none": None, + "list": [1, "x", False, None], + "obj": {"k": "v", "inner": {"i": 2, "j": None}}, + }) + + def test_invalid_non_dict_non_bytes_types_raise(self): + for bad in [ + "str", + 42, + 3.14, + True, + None, + [1, 2, 3], + ]: + with self.subTest(value=bad): + with self.assertRaises(ValueError) as ctx: + convert_value_to_struct(bad) # type: ignore[arg-type] + self.assertIn("Value must be a dictionary, got", str(ctx.exception)) + + def test_bytes_input_raises_parse_error(self): + data = b"hello world" + # The implementation base64-encodes bytes then attempts to ParseDict a string, + # which results in a ParseError from protobuf's json_format. + with self.assertRaises(ParseError) as ctx: + convert_value_to_struct(data) + msg = str(ctx.exception) + # Ensure the base64 string is what would have been produced (implementation detail) + expected_b64 = base64.b64encode(data).decode("utf-8") + self.assertIn(expected_b64, msg) + + def test_dict_with_non_string_key_raises_wrapped_value_error(self): + # Struct JSON object keys must be strings; non-string key should cause parse error + bad_dict = {1: "a", "ok": 2} # type: ignore[dict-item] + with self.assertRaises(ValueError) as ctx: + convert_value_to_struct(bad_dict) # type: ignore[arg-type] + self.assertIn("Unsupported parameter type or value", str(ctx.exception)) + + def test_json_roundtrip_struct_to_dict_to_json(self): + import json + + # Start with a JSON string (could come from any external source) + original_json = json.dumps({ + "a": "b", + "n": 3, + "t": True, + "f": 1.5, + "none": None, + "list": [1, "x", False, None], + "obj": {"k": "v", "inner": {"i": 2, "j": None}}, + }) + + # JSON -> dict + original_dict = json.loads(original_json) + + # dict -> Struct + struct = convert_value_to_struct(original_dict) + + # Struct -> dict + back_to_dict = json_format.MessageToDict(struct, preserving_proto_field_name=True) + + # dict -> JSON + final_json = json.dumps(back_to_dict, separators=(",", ":"), sort_keys=True) + + # Validate: parsing final_json should yield the original_dict structure + # Note: We compare dicts to avoid key-order issues and formatting differences + self.assertEqual(json.loads(final_json), original_dict) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 6e3d2bae40488f8a1f40a938ee4ab66b77d01309 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:51:15 -0500 Subject: [PATCH 21/29] more unit tests per feedback Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/conversation.py | 8 +- tests/clients/test_conversation.py | 43 ++++++++++ ...c_helpers.py => test_dapr_grpc_helpers.py} | 79 ++++++++++++++++++- 3 files changed, 127 insertions(+), 3 deletions(-) rename tests/clients/{test_dappr_grpc_helpers.py => test_dapr_grpc_helpers.py} (61%) diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 97642fba..323f177c 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -69,7 +69,13 @@ def _indent_lines(title: str, text: str, indent: int) -> str: return f'{indent * " "}{title}: {first}{rest}' -class UserTracePrintMixin: +class HasNameAndContent: + """Mixin Protocol for name and content typing.""" + name: Optional[str] = None + content: List[ConversationMessageContent] = field(default_factory=list) + + +class UserTracePrintMixin(HasNameAndContent): """Mixin for trace_print for text based message content from user to LLM.""" def trace_print(self, indent: int = 0) -> None: base = ' ' * indent diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index 7e79800a..b8650f4b 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -927,5 +927,48 @@ async def echo(value: str, delay: float = 0.0) -> str: unregister_tool(tool_name) +class TestIndentLines(unittest.TestCase): + def test_single_line_with_indent(self): + result = conversation._indent_lines("Note", "Hello", 2) + self.assertEqual(result, " Note: Hello") + + def test_multiline_example(self): + text = "This is a long\nmultiline\ntext block" + result = conversation._indent_lines("Description", text, 4) + expected = ( + " Description: This is a long\n" + " multiline\n" + " text block" + ) + self.assertEqual(result, expected) + + def test_zero_indent(self): + result = conversation._indent_lines("Title", "Line one\nLine two", 0) + expected = ( + "Title: Line one\n" + " Line two" + ) + self.assertEqual(result, expected) + + def test_empty_string(self): + result = conversation._indent_lines("Empty", "", 3) + # Should end with a space after colon + self.assertEqual(result, " Empty: ") + + def test_none_text(self): + result = conversation._indent_lines("NoneCase", None, 1) + self.assertEqual(result, " NoneCase: ") + + def test_title_length_affects_indent(self): + # Title length is 1, indent_after_first_line should be indent + len(title) + 2 + # indent=2, len(title)=1 => 2 + 1 + 2 = 5 spaces on continuation lines + result = conversation._indent_lines("T", "a\nb", 2) + expected = ( + " T: a\n" + " b" + ) + self.assertEqual(result, expected) + + if __name__ == '__main__': unittest.main() diff --git a/tests/clients/test_dappr_grpc_helpers.py b/tests/clients/test_dapr_grpc_helpers.py similarity index 61% rename from tests/clients/test_dappr_grpc_helpers.py rename to tests/clients/test_dapr_grpc_helpers.py index ec60dff5..25de711e 100644 --- a/tests/clients/test_dappr_grpc_helpers.py +++ b/tests/clients/test_dapr_grpc_helpers.py @@ -4,8 +4,20 @@ from google.protobuf.struct_pb2 import Struct from google.protobuf import json_format from google.protobuf.json_format import ParseError - -from dapr.clients.grpc._helpers import convert_value_to_struct +from google.protobuf.any_pb2 import Any as GrpcAny +from google.protobuf.wrappers_pb2 import ( + BoolValue, + StringValue, + Int32Value, + Int64Value, + DoubleValue, + BytesValue, +) + +from dapr.clients.grpc._helpers import ( + convert_value_to_struct, + convert_dict_to_grpc_dict_of_any, +) class TestConvertValueToStruct(unittest.TestCase): @@ -105,5 +117,68 @@ def test_json_roundtrip_struct_to_dict_to_json(self): self.assertEqual(json.loads(final_json), original_dict) +class TestConvertDictToGrpcDictOfAny(unittest.TestCase): + def test_none_and_empty_return_empty_dict(self): + self.assertEqual(convert_dict_to_grpc_dict_of_any(None), {}) + self.assertEqual(convert_dict_to_grpc_dict_of_any({}), {}) + + def test_basic_types_conversion(self): + params = { + "s": "hello", + "b": True, + "i32": 123, + "i64": 2**40, + "f": 3.14, + "bytes": b"abc", + } + result = convert_dict_to_grpc_dict_of_any(params) + + # Ensure all keys present + self.assertEqual(set(result.keys()), set(params.keys())) + + # Check each Any contains the proper wrapper with correct value + sv = StringValue() + self.assertTrue(result["s"].Unpack(sv)) + self.assertEqual(sv.value, "hello") + + bv = BoolValue() + self.assertTrue(result["b"].Unpack(bv)) + self.assertEqual(bv.value, True) + + i32v = Int32Value() + self.assertTrue(result["i32"].Unpack(i32v)) + self.assertEqual(i32v.value, 123) + + i64v = Int64Value() + self.assertTrue(result["i64"].Unpack(i64v)) + self.assertEqual(i64v.value, 2**40) + + dv = DoubleValue() + self.assertTrue(result["f"].Unpack(dv)) + self.assertAlmostEqual(dv.value, 3.14) + + byv = BytesValue() + self.assertTrue(result["bytes"].Unpack(byv)) + self.assertEqual(byv.value, b"abc") + + def test_pass_through_existing_any_instances(self): + # Prepare Any values + any_s = GrpcAny() + any_s.Pack(StringValue(value="x")) + any_i = GrpcAny() + any_i.Pack(Int64Value(value=9999999999)) + + params = {"s": any_s, "i": any_i} + result = convert_dict_to_grpc_dict_of_any(params) + + # Should be the exact same Any instances + self.assertIs(result["s"], any_s) + self.assertIs(result["i"], any_i) + + def test_unsupported_type_raises_value_error(self): + with self.assertRaises(ValueError): + convert_dict_to_grpc_dict_of_any({"bad": [1, 2, 3]}) + + if __name__ == "__main__": unittest.main(verbosity=2) From a4870a921bfd974ee1883bc5efe6bbd3e9f49139 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:12:29 -0500 Subject: [PATCH 22/29] make async version of unit test conversation Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- tests/clients/test_dapr_grpc_client_async.py | 297 ++++++++++++++++++- 1 file changed, 292 insertions(+), 5 deletions(-) diff --git a/tests/clients/test_dapr_grpc_client_async.py b/tests/clients/test_dapr_grpc_client_async.py index de8e0fb0..64fa6b16 100644 --- a/tests/clients/test_dapr_grpc_client_async.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -30,7 +30,7 @@ from dapr.conf import settings from dapr.clients.grpc._helpers import to_bytes from dapr.clients.grpc._request import TransactionalStateOperation -from dapr.clients.grpc.conversation import ConversationInput +from dapr.clients.grpc import conversation from dapr.clients.grpc._jobs import Job from dapr.clients.grpc._state import StateOptions, Consistency, Concurrency, StateItem from dapr.clients.grpc._crypto import EncryptOptions, DecryptOptions @@ -1120,8 +1120,8 @@ async def test_converse_alpha1_basic(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') inputs = [ - ConversationInput(content='Hello', role='user'), - ConversationInput(content='How are you?', role='user'), + conversation.ConversationInput(content='Hello', role='user'), + conversation.ConversationInput(content='How are you?', role='user'), ] response = await dapr.converse_alpha1(name='test-llm', inputs=inputs) @@ -1136,7 +1136,7 @@ async def test_converse_alpha1_basic(self): async def test_converse_alpha1_with_options(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') - inputs = [ConversationInput(content='Hello', role='user', scrub_pii=True)] + inputs = [conversation.ConversationInput(content='Hello', role='user', scrub_pii=True)] response = await dapr.converse_alpha1( name='test-llm', @@ -1160,13 +1160,300 @@ async def test_converse_alpha1_error_handling(self): status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Invalid argument') ) - inputs = [ConversationInput(content='Hello', role='user')] + inputs = [conversation.ConversationInput(content='Hello', role='user')] with self.assertRaises(DaprGrpcError) as context: await dapr.converse_alpha1(name='test-llm', inputs=inputs) self.assertTrue('Invalid argument' in str(context.exception)) await dapr.close() + async def test_converse_alpha2_basic_user_message(self): + """Test basic Alpha2 conversation with user messages (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + name='TestUser', + content=[conversation.ConversationMessageContent(text='Hello, how are you?')], + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message], scrub_pii=False) + + response = await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 1) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'stop') + self.assertEqual(choice.index, 0) + self.assertEqual(choice.message.content, 'Response to user: Hello, how are you?') + self.assertEqual(len(choice.message.tool_calls), 0) + await dapr.close() + + async def test_converse_alpha2_with_tools_weather_request(self): + """Test Alpha2 conversation with tool calling for weather requests (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + weather_tool = conversation.ConversationTools( + function=conversation.ConversationToolsFunction( + name='get_weather', + description='Get current weather information', + parameters={ + 'type': 'object', + 'properties': { + 'location': {'type': 'string', 'description': 'Location for weather info'} + }, + 'required': ['location'], + }, + ) + ) + + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text="What's the weather like?")] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) + + response = await dapr.converse_alpha2( + name='test-llm', inputs=[input_alpha2], tools=[weather_tool], tool_choice='auto' + ) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 1) + choice = response.outputs[0].choices[0] + self.assertEqual(choice.finish_reason, 'tool_calls') + self.assertEqual(choice.index, 0) + self.assertEqual(choice.message.content, "I'll check the weather for you.") + self.assertEqual(len(choice.message.tool_calls), 1) + tool_call = choice.message.tool_calls[0] + self.assertEqual(tool_call.function.name, 'get_weather') + self.assertEqual(tool_call.function.arguments, '{"location": "San Francisco", "unit": "celsius"}') + self.assertTrue(tool_call.id.startswith('call_')) + await dapr.close() + + async def test_converse_alpha2_system_message(self): + """Test Alpha2 conversation with system messages (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + system_message = conversation.ConversationMessage( + of_system=conversation.ConversationMessageOfSystem( + content=[conversation.ConversationMessageContent(text='You are a helpful assistant.')] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[system_message]) + + response = await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual( + choice.message.content, 'System acknowledged: You are a helpful assistant.' + ) + await dapr.close() + + async def test_converse_alpha2_developer_message(self): + """Test Alpha2 conversation with developer messages (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + developer_message = conversation.ConversationMessage( + of_developer=conversation.ConversationMessageOfDeveloper( + name='DevTeam', + content=[ + conversation.ConversationMessageContent(text='Debug: Processing user input') + ], + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[developer_message]) + + response = await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual( + choice.message.content, 'Developer note processed: Debug: Processing user input' + ) + await dapr.close() + + async def test_converse_alpha2_tool_message(self): + """Test Alpha2 conversation with tool messages (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + tool_message = conversation.ConversationMessage( + of_tool=conversation.ConversationMessageOfTool( + tool_id='call_123', + name='get_weather', + content=[ + conversation.ConversationMessageContent( + text='{"temperature": 22, "condition": "sunny"}' + ) + ], + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[tool_message]) + + response = await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual( + choice.message.content, + 'Tool result processed: {"temperature": 22, "condition": "sunny"}', + ) + await dapr.close() + + async def test_converse_alpha2_assistant_message(self): + """Test Alpha2 conversation with assistant messages (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + assistant_message = conversation.ConversationMessage( + of_assistant=conversation.ConversationMessageOfAssistant( + content=[conversation.ConversationMessageContent(text='I understand your request.')] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[assistant_message]) + + response = await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + self.assertEqual( + choice.message.content, 'Assistant continued: I understand your request.' + ) + await dapr.close() + + async def test_converse_alpha2_multiple_messages(self): + """Test Alpha2 conversation with multiple messages in one input (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + system_message = conversation.ConversationMessage( + of_system=conversation.ConversationMessageOfSystem( + content=[conversation.ConversationMessageContent(text='You are helpful.')] + ) + ) + + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text='Hello!')] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[system_message, user_message]) + + response = await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + + self.assertIsNotNone(response) + self.assertEqual(len(response.outputs), 1) + self.assertEqual(len(response.outputs[0].choices), 2) + self.assertEqual( + response.outputs[0].choices[0].message.content, + 'System acknowledged: You are helpful.', + ) + self.assertEqual( + response.outputs[0].choices[1].message.content, 'Response to user: Hello!' + ) + await dapr.close() + + async def test_converse_alpha2_with_context_and_options(self): + """Test Alpha2 conversation with context ID and various options (async).""" + from google.protobuf.any_pb2 import Any as GrpcAny + + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text='Continue our conversation')] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message], scrub_pii=True) + + params = {'custom_param': GrpcAny(value=b'{"setting": "value"}')} + + response = await dapr.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + context_id='chat-session-123', + parameters=params, + metadata={'env': 'test'}, + scrub_pii=True, + temperature=0.7, + tool_choice='none', + ) + + self.assertIsNotNone(response) + self.assertEqual(response.context_id, 'chat-session-123') + choice = response.outputs[0].choices[0] + self.assertEqual( + choice.message.content, 'Response to user: Continue our conversation' + ) + await dapr.close() + + async def test_converse_alpha2_error_handling(self): + """Test Alpha2 conversation error handling (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + self._fake_dapr_server.raise_exception_on_next_call( + status_pb2.Status(code=code_pb2.INVALID_ARGUMENT, message='Alpha2 Invalid argument') + ) + + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text='Test error')] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) + + with self.assertRaises(DaprGrpcError) as context: + await dapr.converse_alpha2(name='test-llm', inputs=[input_alpha2]) + self.assertTrue('Alpha2 Invalid argument' in str(context.exception)) + await dapr.close() + + async def test_converse_alpha2_tool_choice_specific(self): + """Test Alpha2 conversation with specific tool choice (async).""" + dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') + + weather_tool = conversation.ConversationTools( + function=conversation.ConversationToolsFunction( + name='get_weather', description='Get weather information' + ) + ) + calculator_tool = conversation.ConversationTools( + function=conversation.ConversationToolsFunction( + name='calculate', description='Perform calculations' + ) + ) + + user_message = conversation.ConversationMessage( + of_user=conversation.ConversationMessageOfUser( + content=[conversation.ConversationMessageContent(text="What's the weather today?")] + ) + ) + + input_alpha2 = conversation.ConversationInputAlpha2(messages=[user_message]) + + response = await dapr.converse_alpha2( + name='test-llm', + inputs=[input_alpha2], + tools=[weather_tool, calculator_tool], + tool_choice='get_weather', + ) + + self.assertIsNotNone(response) + choice = response.outputs[0].choices[0] + if 'weather' in choice.message.content.lower(): + self.assertEqual(choice.finish_reason, 'tool_calls') + await dapr.close() + # # Tests for Jobs API (Alpha) - Async # From 5bec64e3af068b89174d76c2c10ee0a69e32a1fa Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:14:07 -0500 Subject: [PATCH 23/29] add some information how to run markdown tests with a different runtime Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- README.md | 12 ++++++++++-- examples/conversation/README.md | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ed06b88..58812e81 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,20 @@ tox -e type 8. Run examples -The tests in examples are using Mechanical Markdown (MM) from https://github.com/dapr/mechanical-markdown. - ```bash tox -e examples ``` +[Dapr Mechanical Markdown](https://github.com/dapr/mechanical-markdown) is used to test the examples. + +If you need to run the examples against a development version of the runtime, you can use the following command: +- Get your daprd runtime binary from [here](https://github.com/dapr/dapr/releases) for your platform. +- Copy the binary to a folder, for example `examples/.dapr/bin/` directory. +- In your example README, change the `dapr run` command and add a line `--runtime-path ./examples \`. +- Copy a dapr config file `config.yaml` file to the `examples/.dapr` directory. This file is usually in your $(HOME)/.dapr directory if you had installed dapr cli before. +- Now you can run the example with `tox -e examples`. + + ## Documentation Documentation is generated using Sphinx. Extensions used are mainly Napoleon (To process the Google Comment Style) and Autodocs (For automatically generating documentation). The `.rst` files are generated using Sphinx-Apidocs. diff --git a/examples/conversation/README.md b/examples/conversation/README.md index 3901d869..1d7789b3 100644 --- a/examples/conversation/README.md +++ b/examples/conversation/README.md @@ -20,6 +20,11 @@ The Conversation API supports real LLM providers including: ``` 2. **Run the simple conversation on the Alpha V1 version (dapr 1.15)** + + This is a basic example that uses the Conversation API to get a response from a bot. + It also uses the `echo` provider that just echoes back the message. + In the echo provider, a multi-input message is returned as a single output separated by newlines. + LLM[assistant]:') + self.assertEqual(out[2], ' name: dev') + self.assertEqual(out[3], ' content[0]: turn on feature x') + # System header and content + self.assertEqual(out[4], 'client[system] --------------> LLM[assistant]:') + self.assertEqual(out[5], ' name: policy') + self.assertEqual(out[6], ' content[0]: Follow company policy.') + # Delegated lines for user (name first, then content) + self.assertEqual(out[7], 'client[user] --------------> LLM[assistant]:') + self.assertEqual(out[8], ' name: bob') + self.assertEqual(out[9], ' content[0]: hi') + # Assistant header and content + self.assertEqual(out[10], 'client <------------- LLM[assistant]:') + self.assertIn(' content[0]: hello', out[11]) + # Tool header and content + self.assertEqual(out[12], 'client[tool] -------------> LLM[assistant]:') + self.assertEqual(out[13], ' tool_id: t1') + self.assertEqual(out[14], ' name: tool.fn') + self.assertEqual(out[15], ' content[0]: ok') diff --git a/tests/clients/test_conversation_helpers_more.py b/tests/clients/test_conversation_helpers_more.py new file mode 100644 index 00000000..39d43f25 --- /dev/null +++ b/tests/clients/test_conversation_helpers_more.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- +""" +Additional targeted unit tests to increase coverage of +_dapr.clients.grpc._conversation_helpers. +""" +import base64 +import json +import unittest +import warnings +from dataclasses import dataclass +from typing import List, Literal, Optional, Any, Dict, Union + +from dapr.conf import settings +from dapr.clients.grpc._conversation_helpers import ( + ToolArgumentError, + _python_type_to_json_schema, + bind_params_to_func, + stringify_tool_output, +) + +from enum import Enum + + +class TestLargeEnumBehavior(unittest.TestCase): + def setUp(self): + # Save originals + self._orig_max = settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS + self._orig_beh = settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR + + def tearDown(self): + # Restore + settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = self._orig_max + settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = self._orig_beh + + def test_large_enum_compacted_to_string(self): + # Make threshold tiny to trigger large-enum path + settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 2 + settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'string' + + class BigEnum(Enum): + A = 'a' + B = 'b' + C = 'c' + D = 'd' + + schema = _python_type_to_json_schema(BigEnum) + # Should be compacted to string with description and examples + self.assertEqual(schema.get('type'), 'string') + self.assertIn('description', schema) + self.assertIn('examples', schema) + self.assertTrue(len(schema['examples']) > 0) + + def test_large_enum_error_mode(self): + settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 1 + settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'error' + + from enum import Enum + + class BigEnum(Enum): + A = 'a' + B = 'b' + + with self.assertRaises(ValueError): + _python_type_to_json_schema(BigEnum) + + +class TestCoercionsAndBinding(unittest.TestCase): + def test_coerce_bool_variants(self): + def f(flag: bool) -> bool: + return flag + + # True-ish variants + for v in ['true', 'True', 'YES', '1', 'on', ' y ']: + bound = bind_params_to_func(f, {'flag': v}) + self.assertIs(f(*bound.args, **bound.kwargs), True) + + # False-ish variants + for v in ['false', 'False', 'NO', '0', 'off', ' n ']: + bound = bind_params_to_func(f, {'flag': v}) + self.assertIs(f(*bound.args, **bound.kwargs), False) + + # Invalid + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'flag': 'maybe'}) + + def test_literal_numeric_from_string(self): + def g(x: Literal[1, 2, 3]) -> int: + return x # type: ignore[return-value] + + bound = bind_params_to_func(g, {'x': '2'}) + self.assertEqual(g(*bound.args, **bound.kwargs), 2) + + def test_unexpected_kwarg_is_rejected(self): + def h(a: int) -> int: + return a + + with self.assertRaises(Exception): + bind_params_to_func(h, {'a': 1, 'extra': 2}) + + def test_dataclass_arg_validation(self): + @dataclass + class P: + x: int + y: str + + def k(p: P) -> str: + return p.y + + # Passing an instance is fine + p = P(1, 'ok') + bound = bind_params_to_func(k, {'p': p}) + self.assertEqual(k(*bound.args, **bound.kwargs), 'ok') + + # Passing a dict should fail for dataclass per implementation + with self.assertRaises(ToolArgumentError): + bind_params_to_func(k, {'p': {'x': 1, 'y': 'nope'}}) + + +class TestPlainClassSchema(unittest.TestCase): + def test_plain_class_init_signature(self): + class C: + def __init__(self, a: int, b: str = 'x'): + self.a = a + self.b = b + + schema = _python_type_to_json_schema(C) + self.assertEqual(schema['type'], 'object') + props = schema['properties'] + self.assertIn('a', props) + self.assertIn('b', props) + # Only 'a' is required + self.assertIn('required', schema) + self.assertEqual(schema['required'], ['a']) + + def test_plain_class_slots_fallback(self): + class D: + __slots__ = ('m', 'n') + m: int + n: Optional[str] + + schema = _python_type_to_json_schema(D) + # Implementation builds properties from __slots__ with required for non-optional + self.assertEqual(schema['type'], 'object') + self.assertIn('properties', schema) + self.assertIn('m', schema['properties']) + self.assertIn('n', schema['properties']) + self.assertEqual(schema['properties']['m']['type'], 'integer') + self.assertEqual(schema['properties']['n']['type'], 'string') + self.assertIn('required', schema) + self.assertEqual(schema['required'], ['m']) + + +class TestDocstringUnsupportedWarning(unittest.TestCase): + def test_informal_param_info_warning(self): + def unsupported(x: int, y: str): + """Do something. + + The x parameter should be an integer indicating repetitions. The y parameter is used for labeling. + """ + return x, y + + # _extract_docstring_args is used via function_to_json_schema or directly. Use direct import path + from dapr.clients.grpc._conversation_helpers import _extract_docstring_args + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + res = _extract_docstring_args(unsupported) + self.assertEqual(res, {}) + self.assertTrue( + any('appears to contain parameter information' in str(wi.message) for wi in w) + ) + + +class TestLiteralSchemaMapping(unittest.TestCase): + def test_literal_strings_schema(self): + T = Literal['a', 'b', 'c'] + schema = _python_type_to_json_schema(T) + self.assertEqual(schema.get('type'), 'string') + self.assertEqual(set(schema['enum']), {'a', 'b', 'c'}) + + def test_literal_ints_schema(self): + T = Literal[1, 2, 3] + schema = _python_type_to_json_schema(T) + self.assertEqual(schema.get('type'), 'integer') + self.assertEqual(set(schema['enum']), {1, 2, 3}) + + def test_literal_nullable_string_schema(self): + T = Literal[None, 'x', 'y'] + schema = _python_type_to_json_schema(T) + # non-null types only string, should set 'type' to 'string' and include None in enum + self.assertEqual(schema.get('type'), 'string') + self.assertIn(None, schema['enum']) + self.assertIn('x', schema['enum']) + self.assertIn('y', schema['enum']) + + def test_literal_mixed_types_no_unified_type(self): + T = Literal['x', 1] + schema = _python_type_to_json_schema(T) + # Mixed non-null types -> no unified 'type' should be set + self.assertNotIn('type', schema) + self.assertEqual(set(schema['enum']), {'x', 1}) + + def test_literal_enum_members_normalized(self): + from enum import Enum + + class Mode(Enum): + FAST = 'fast' + SLOW = 'slow' + + T = Literal[Mode.FAST, Mode.SLOW] + schema = _python_type_to_json_schema(T) + self.assertEqual(schema.get('type'), 'string') + self.assertEqual(set(schema['enum']), {'fast', 'slow'}) + + def test_literal_bytes_and_bytearray_schema(self): + T = Literal[b'a', bytearray(b'b')] + schema = _python_type_to_json_schema(T) + # bytes/bytearray are coerced to string type for schema typing + self.assertEqual(schema.get('type'), 'string') + # The enum preserves the literal values as provided + self.assertIn(b'a', schema['enum']) + self.assertIn(bytearray(b'b'), schema['enum']) + + +# --- Helpers for Coercion tests + + +class Mode(Enum): + RED = 'red' + BLUE = 'blue' + + +@dataclass +class DC: + x: int + y: str + + +class Plain: + def __init__(self, a: int, b: str = 'x') -> None: + self.a = a + self.b = b + + +class TestScalarCoercions(unittest.TestCase): + def test_int_from_str_and_float_and_invalid(self): + def f(a: int) -> int: + return a + + # str -> int + bound = bind_params_to_func(f, {'a': ' 42 '}) + self.assertEqual(f(*bound.args, **bound.kwargs), 42) + + # float integral -> int + bound = bind_params_to_func(f, {'a': 3.0}) + self.assertEqual(f(*bound.args, **bound.kwargs), 3) + + # float non-integral -> error + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'a': 3.14}) + + def test_float_from_int_and_str(self): + def g(x: float) -> float: + return x + + bound = bind_params_to_func(g, {'x': 2}) + self.assertEqual(g(*bound.args, **bound.kwargs), 2.0) + + bound = bind_params_to_func(g, {'x': ' 3.5 '}) + self.assertEqual(g(*bound.args, **bound.kwargs), 3.5) + + def test_str_from_non_str(self): + def h(s: str) -> str: + return s + + bound = bind_params_to_func(h, {'s': 123}) + self.assertEqual(h(*bound.args, **bound.kwargs), '123') + + def test_bool_variants_and_invalid(self): + def b(flag: bool) -> bool: + return flag + + for v in ['true', 'False', 'YES', 'no', '1', '0', 'on', 'off']: + bound = bind_params_to_func(b, {'flag': v}) + # Ensure conversion yields actual bool + self.assertIsInstance(b(*bound.args, **bound.kwargs), bool) + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(b, {'flag': 'maybe'}) + + +class TestEnumCoercions(unittest.TestCase): + def test_enum_by_value_and_name_and_case_insensitive(self): + def f(m: Mode) -> Mode: + return m + + # by value + bound = bind_params_to_func(f, {'m': 'red'}) + self.assertEqual(f(*bound.args, **bound.kwargs), Mode.RED) + + # by exact name + bound = bind_params_to_func(f, {'m': 'BLUE'}) + self.assertEqual(f(*bound.args, **bound.kwargs), Mode.BLUE) + + # by case-insensitive name + bound = bind_params_to_func(f, {'m': 'red'}) # value already tested; use name lower + self.assertEqual(f(*bound.args, **bound.kwargs), Mode.RED) + + # invalid + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'m': 'green'}) + + +class TestCoerceAndValidateBranches(unittest.TestCase): + def test_optional_and_union(self): + def f(a: Optional[int], b: Union[str, int]) -> tuple: + return a, b + + bound = bind_params_to_func(f, {'a': '2', 'b': 5}) + # Union[str, int] tries str first; 5 is coerced to '5' + self.assertEqual(f(*bound.args, **bound.kwargs), (2, '5')) + + bound = bind_params_to_func(f, {'a': None, 'b': 'hello'}) + self.assertEqual(f(*bound.args, **bound.kwargs), (None, 'hello')) + + def test_list_and_dict_coercion(self): + def g(xs: List[int], mapping: Dict[int, float]) -> tuple: + return xs, mapping + + bound = bind_params_to_func(g, {'xs': ['1', '2', '3'], 'mapping': {'1': '2.5', 3: 4}}) + xs, mapping = g(*bound.args, **bound.kwargs) + self.assertEqual(xs, [1, 2, 3]) + self.assertEqual(mapping, {1: 2.5, 3: 4.0}) + + # Wrong type for list + with self.assertRaises(ToolArgumentError): + bind_params_to_func(g, {'xs': 'not-a-list', 'mapping': {}}) + + # Wrong type for dict + with self.assertRaises(ToolArgumentError): + bind_params_to_func(g, {'xs': [1], 'mapping': 'not-a-dict'}) + + def test_dataclass_optional_and_rejection_of_dict(self): + def f(p: Optional[DC]) -> Optional[str]: + return None if p is None else p.y + + inst = DC(1, 'ok') + bound = bind_params_to_func(f, {'p': inst}) + self.assertEqual(f(*bound.args, **bound.kwargs), 'ok') + + bound = bind_params_to_func(f, {'p': None}) + self.assertIsNone(f(*bound.args, **bound.kwargs)) + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'p': {'x': 1, 'y': 'no'}}) + + def test_plain_class_construction_from_dict_and_missing_arg(self): + def f(p: Plain) -> int: + return p.a + + # Construct from dict with coercion + bound = bind_params_to_func(f, {'p': {'a': '3'}}) + res = f(*bound.args, **bound.kwargs) + self.assertEqual(res, 3) + self.assertIsInstance(bound.arguments['p'], Plain) + self.assertEqual(bound.arguments['p'].b, 'x') # default applied + + # Missing required arg + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'p': {}}) + + def test_any_and_isinstance_fallback(self): + class C: + ... + + def f(a: Any, c: C) -> tuple: + return a, c + + c = C() + with self.assertRaises(ToolArgumentError) as ctx: + bind_params_to_func(f, {'a': object(), 'c': c}) + # _coerce_and_validate raises TypeError for Any; bind wraps it in ToolArgumentError + self.assertIsInstance(ctx.exception.__cause__, TypeError) + + +# ---- Helpers for test stringify + + +class Shade(Enum): + LIGHT = 'light' + DARK = 'dark' + + +@dataclass +class Pair: + a: int + b: str + + +class PlainWithDict: + def __init__(self): + self.x = 10 + self.y = 'y' + self.fn = lambda: 1 # callable should be filtered out + + +class TestStringifyToolOutputMore(unittest.TestCase): + def test_bytes_and_bytearray_branch(self): + raw = bytes([1, 2, 3, 254, 255]) + expected = 'base64:' + base64.b64encode(raw).decode('ascii') + self.assertEqual(stringify_tool_output(raw), expected) + + ba = bytearray(raw) + expected_ba = 'base64:' + base64.b64encode(bytes(ba)).decode('ascii') + self.assertEqual(stringify_tool_output(ba), expected_ba) + + def test_default_encoder_enum_dataclass_and___dict__(self): + # Enum -> value via default encoder (JSON string) + out_enum = stringify_tool_output(Shade.DARK) + self.assertEqual(out_enum, json.dumps('dark', ensure_ascii=False)) + + # Dataclass -> asdict via default encoder + p = Pair(3, 'z') + out_dc = stringify_tool_output(p) + self.assertEqual(json.loads(out_dc), {'a': 3, 'b': 'z'}) + + # __dict__ plain object -> filtered dict via default encoder + obj = PlainWithDict() + out_obj = stringify_tool_output(obj) + self.assertEqual(json.loads(out_obj), {'x': 10, 'y': 'y'}) + + +if __name__ == '__main__': + unittest.main() From 924541cbf825d56db56c01de54c130eab5848557 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:38:30 -0500 Subject: [PATCH 26/29] add more information on execute registered tools, also added more tests for them to validate Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- README.md | 7 +- dapr/clients/grpc/_conversation_helpers.py | 2 +- dapr/clients/grpc/conversation.py | 57 +++++++++- tests/clients/test_conversation.py | 118 +++++++++++++++++++++ 4 files changed, 175 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 65ab4cfd..df576494 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,10 @@ tox -e examples [Dapr Mechanical Markdown](https://github.com/dapr/mechanical-markdown) is used to test the examples. -If you need to run the examples against a development version of the runtime, you can use the following command: +If you need to run the examples against a pre-released version of the runtime, you can use the following command: - Get your daprd runtime binary from [here](https://github.com/dapr/dapr/releases) for your platform. -- Copy the binary to a folder, for example `examples/.dapr/bin/` directory. -- In your example README, change the `dapr run` command and add a line `--runtime-path ./examples \`. -- Copy a dapr config file `config.yaml` file to the `examples/.dapr` directory. This file is usually in your $(HOME)/.dapr directory if you had installed dapr cli before. +- Copy the binary to your dapr home folder at $HOME/.dapr/bin/daprd. +Or using dapr cli directly: `dapr init --runtime-version ` - Now you can run the example with `tox -e examples`. diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index d51e1802..36e39b14 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -859,7 +859,7 @@ def _coerce_and_validate(value: Any, expected_type: Any) -> Any: args = get_args(expected_type) if expected_type is Any: - raise TypeError(f'We cannot handle parameters with type Any') + raise TypeError('We cannot handle parameters with type Any') # Optional[T] -> Union[T, None] if origin is Union: diff --git a/dapr/clients/grpc/conversation.py b/dapr/clients/grpc/conversation.py index 0b667ae3..1da02dac 100644 --- a/dapr/clients/grpc/conversation.py +++ b/dapr/clients/grpc/conversation.py @@ -452,6 +452,14 @@ def tool( ): """ Decorate a callable as a conversation tool. + + Security note: + - Register only trusted functions. Tool calls may be triggered from LLM outputs and receive + untrusted parameters. + - Use precise type annotations and docstrings for your function; we derive a JSON schema used by + the binder to coerce types and reject unexpected/invalid arguments. + - Add your own guardrails if the tool can perform side effects (filesystem, network, subprocess). + - You can set register=False and call register_tool later to control registration explicitly. """ def _decorate(f: Callable): @@ -481,19 +489,35 @@ def _decorate(f: Callable): @dataclass class ConversationTools: - """Tools available for conversation.""" + """Tools available for conversation. + + Notes on safety and validation: + - Tools execute arbitrary Python callables. Register only trusted functions and be mindful of + side effects (filesystem, network, subprocesses). + - Parameters provided by an LLM are untrusted. The invocation path uses bind_params_to_func to + coerce types based on your function annotations and to reject unexpected/invalid arguments. + - Consider adding your own validation/guardrails in your tool implementation. + """ # currently only function is supported function: ConversationToolsFunction backend: Optional[ToolBackend] = None def invoke(self, params: Params = None) -> Any: - """execute the tool with params""" + """Execute the tool with params (synchronous). + + params may be: + - Mapping[str, Any]: passed as keyword arguments + - Sequence[Any]: passed as positional arguments + - None: no arguments + Detailed validation and coercion are performed by the backend via bind_params_to_func. + """ if not self.backend: raise conv_helpers.ToolExecutionError('Tool backend not set') return self.backend.invoke(self.function, params) async def ainvoke(self, params: Params = None, *, timeout: Union[float, None] = None) -> Any: + """Execute the tool asynchronously. See invoke() for parameter shape and safety notes.""" if not self.backend: raise conv_helpers.ToolExecutionError('Tool backend not set') return await self.backend.ainvoke(self.function, params, timeout=timeout) @@ -528,18 +552,43 @@ def _get_tool(name: str) -> ConversationTools: def execute_registered_tool(name: str, params: Union[Params, str] = None) -> Any: - """Execute a registered tool.""" + """Execute a registered tool. + + Security considerations: + - A registered tool typically executes user-defined code (or code imported from libraries). Only + register and execute tools you trust. Treat model-provided params as untrusted input. + - Prefer defining a JSON schema for your tool function parameters (ConversationToolsFunction + is created from your function’s signature and annotations). The internal binder performs + type coercion and rejects unexpected/invalid arguments. + - Add your own guardrails if the tool can perform side effects (filesystem, network, subprocess, etc.). + """ if isinstance(params, str): params = json.loads(params) + # Minimal upfront shape check; detailed validation happens in bind_params_to_func + if params is not None and not isinstance(params, (Mapping, Sequence)): + raise conv_helpers.ToolArgumentError( + 'params must be a mapping (kwargs), a sequence (args), or None' + ) return _get_tool(name).invoke(params) async def execute_registered_tool_async( name: str, params: Union[Params, str] = None, *, timeout: Union[float, None] = None ) -> Any: - """Execute a registered tool asynchronously.""" + """Execute a registered tool asynchronously. + + Security considerations: + - Only execute trusted tools; treat model-provided params as untrusted input. + - Prefer well-typed function signatures and schemas for parameter validation. The binder will + coerce and validate, rejecting unexpected arguments. + - For async tools, consider timeouts and guardrails to limit side effects. + """ if isinstance(params, str): params = json.loads(params) + if params is not None and not isinstance(params, (Mapping, Sequence)): + raise conv_helpers.ToolArgumentError( + 'params must be a mapping (kwargs), a sequence (args), or None' + ) return await _get_tool(name).ainvoke(params, timeout=timeout) diff --git a/tests/clients/test_conversation.py b/tests/clients/test_conversation.py index 45acfc0a..8a6cc697 100644 --- a/tests/clients/test_conversation.py +++ b/tests/clients/test_conversation.py @@ -15,6 +15,7 @@ import asyncio +import json import unittest import uuid @@ -27,6 +28,7 @@ from dapr.clients.grpc._conversation_helpers import ( ToolArgumentError, ToolExecutionError, + ToolNotFoundError, ) from dapr.clients.grpc.conversation import ( ConversationInput, @@ -50,6 +52,7 @@ ConversationMessageOfAssistant, ConversationToolCalls, ConversationToolCallsOfFunction, + execute_registered_tool, ) from dapr.clients.grpc.conversation import ( tool as tool_decorator, @@ -1105,5 +1108,120 @@ def test_empty_and_none_outputs(self): self.assertEqual(response_none.to_assistant_messages(), []) +class ExecuteRegisteredToolSyncTests(unittest.TestCase): + def tearDown(self): + # Cleanup all tools we may have registered by name prefix + # (names are randomized per test to avoid collisions) + pass # Names are unique per test; we explicitly unregister in tests + + def test_sync_success_with_kwargs_and_sequence_and_json(self): + name = f'test_add_{uuid.uuid4().hex[:8]}' + + @tool_decorator(name=name) + def add(a: int, b: int) -> int: + return a + b + + try: + # kwargs mapping + out = execute_registered_tool(name, {'a': 2, 'b': 3}) + self.assertEqual(out, 5) + + # sequence args + out2 = execute_registered_tool(name, [10, 5]) + self.assertEqual(out2, 15) + + # JSON string params + out3 = execute_registered_tool(name, json.dumps({'a': '7', 'b': '8'})) + self.assertEqual(out3, 15) + finally: + unregister_tool(name) + + def test_sync_invalid_params_type_raises(self): + name = f'test_echo_{uuid.uuid4().hex[:8]}' + + @tool_decorator(name=name) + def echo(x: str) -> str: + return x + + try: + with self.assertRaises(ToolArgumentError): + execute_registered_tool(name, 123) # not Mapping/Sequence/None + finally: + unregister_tool(name) + + def test_sync_unregistered_tool_raises(self): + name = f'does_not_exist_{uuid.uuid4().hex[:8]}' + with self.assertRaises(ToolNotFoundError): + execute_registered_tool(name, {'a': 1}) + + def test_sync_tool_exception_wrapped(self): + name = f'test_fail_{uuid.uuid4().hex[:8]}' + + @tool_decorator(name=name) + def fail() -> None: + raise ValueError('boom') + + try: + with self.assertRaises(ToolExecutionError): + execute_registered_tool(name) + finally: + unregister_tool(name) + + +class ExecuteRegisteredToolAsyncTests(unittest.IsolatedAsyncioTestCase): + async def asyncTearDown(self): + # Nothing persistent; individual tests unregister. + pass + + async def test_async_success_and_json_params(self): + name = f'test_async_echo_{uuid.uuid4().hex[:8]}' + + @tool_decorator(name=name) + async def echo(value: str) -> str: + await asyncio.sleep(0) + return value + + try: + out = await execute_registered_tool_async(name, {'value': 'hi'}) + self.assertEqual(out, 'hi') + + out2 = await execute_registered_tool_async(name, json.dumps({'value': 'ok'})) + self.assertEqual(out2, 'ok') + finally: + unregister_tool(name) + + async def test_async_invalid_params_type_raises(self): + name = f'test_async_inv_{uuid.uuid4().hex[:8]}' + + @tool_decorator(name=name) + async def one(x: int) -> int: + return x + + try: + with self.assertRaises(ToolArgumentError): + await execute_registered_tool_async(name, 3.14) # invalid type + finally: + unregister_tool(name) + + async def test_async_unregistered_tool_raises(self): + name = f'does_not_exist_{uuid.uuid4().hex[:8]}' + with self.assertRaises(ToolNotFoundError): + await execute_registered_tool_async(name, None) + + async def test_async_tool_exception_wrapped(self): + name = f'test_async_fail_{uuid.uuid4().hex[:8]}' + + @tool_decorator(name=name) + async def fail_async() -> None: + await asyncio.sleep(0) + raise RuntimeError('nope') + + try: + with self.assertRaises(ToolExecutionError): + await execute_registered_tool_async(name) + finally: + unregister_tool(name) + + if __name__ == '__main__': unittest.main() From 9dd3bbfb730351d3020f17f166d7ca6fcb91b2ec Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:51:57 -0500 Subject: [PATCH 27/29] fix test failing on py 1.13. Merge two unit test files per feedback Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/_conversation_helpers.py | 17 +- tests/clients/test_conversation_helpers.py | 420 ++++++++++++++++- .../clients/test_conversation_helpers_more.py | 434 ------------------ 3 files changed, 428 insertions(+), 443 deletions(-) delete mode 100644 tests/clients/test_conversation_helpers_more.py diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index 36e39b14..ac8bfbb9 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -18,6 +18,7 @@ import string from dataclasses import fields, is_dataclass from enum import Enum +from types import UnionType from typing import ( Any, Callable, @@ -854,20 +855,26 @@ def _coerce_literal(value: Any, lit_args: List[Any]) -> Any: raise ValueError(f'{value!r} not in allowed literals {lit_args!r}') +def _is_union(t) -> bool: + origin = get_origin(t) + return origin in (Union, UnionType) # handles 3.8–3.13+ + + def _coerce_and_validate(value: Any, expected_type: Any) -> Any: - origin = get_origin(expected_type) args = get_args(expected_type) if expected_type is Any: raise TypeError('We cannot handle parameters with type Any') # Optional[T] -> Union[T, None] - if origin is Union: + if _is_union(expected_type): # try each option last_err: Optional[Exception] = None for opt in args: - if opt is type(None) and value is None: - return None + if opt is type(None): + if value is None: + return None + continue try: return _coerce_and_validate(value, opt) except Exception as e: @@ -877,6 +884,8 @@ def _coerce_and_validate(value: Any, expected_type: Any) -> Any: str(last_err) if last_err else f'Cannot coerce {value!r} to {expected_type}' ) + origin = get_origin(expected_type) + # Literal if origin is Literal: return _coerce_literal(value, list(args)) diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py index 269a8e01..c9ed3415 100644 --- a/tests/clients/test_conversation_helpers.py +++ b/tests/clients/test_conversation_helpers.py @@ -20,10 +20,8 @@ from contextlib import redirect_stdout from dataclasses import dataclass from enum import Enum - -from typing import Dict, List, Literal, Optional, Union, Set - - +from typing import Any, Dict, List, Literal, Optional, Union, Set +from dapr.conf import settings from dapr.clients.grpc._conversation_helpers import ( stringify_tool_output, bind_params_to_func, @@ -31,6 +29,7 @@ _extract_docstring_args, _python_type_to_json_schema, extract_docstring_summary, + ToolArgumentError, ) from dapr.clients.grpc.conversation import ( ConversationToolsFunction, @@ -45,7 +44,6 @@ ConversationMessageOfSystem, ) - def test_string_passthrough(): assert stringify_tool_output('hello') == 'hello' @@ -1739,3 +1737,415 @@ def test_conversation_message_headers_for_all_roles(self): self.assertEqual(out[13], ' tool_id: t1') self.assertEqual(out[14], ' name: tool.fn') self.assertEqual(out[15], ' content[0]: ok') + + +class TestLargeEnumBehavior(unittest.TestCase): + def setUp(self): + # Save originals + self._orig_max = settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS + self._orig_beh = settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR + + def tearDown(self): + # Restore + settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = self._orig_max + settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = self._orig_beh + + def test_large_enum_compacted_to_string(self): + # Make threshold tiny to trigger large-enum path + settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 2 + settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'string' + + class BigEnum(Enum): + A = 'a' + B = 'b' + C = 'c' + D = 'd' + + schema = _python_type_to_json_schema(BigEnum) + # Should be compacted to string with description and examples + self.assertEqual(schema.get('type'), 'string') + self.assertIn('description', schema) + self.assertIn('examples', schema) + self.assertTrue(len(schema['examples']) > 0) + + def test_large_enum_error_mode(self): + settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 1 + settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'error' + + from enum import Enum + + class BigEnum(Enum): + A = 'a' + B = 'b' + + with self.assertRaises(ValueError): + _python_type_to_json_schema(BigEnum) + + +class TestCoercionsAndBinding(unittest.TestCase): + def test_coerce_bool_variants(self): + def f(flag: bool) -> bool: + return flag + + # True-ish variants + for v in ['true', 'True', 'YES', '1', 'on', ' y ']: + bound = bind_params_to_func(f, {'flag': v}) + self.assertIs(f(*bound.args, **bound.kwargs), True) + + # False-ish variants + for v in ['false', 'False', 'NO', '0', 'off', ' n ']: + bound = bind_params_to_func(f, {'flag': v}) + self.assertIs(f(*bound.args, **bound.kwargs), False) + + # Invalid + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'flag': 'maybe'}) + + def test_literal_numeric_from_string(self): + def g(x: Literal[1, 2, 3]) -> int: + return x # type: ignore[return-value] + + bound = bind_params_to_func(g, {'x': '2'}) + self.assertEqual(g(*bound.args, **bound.kwargs), 2) + + def test_unexpected_kwarg_is_rejected(self): + def h(a: int) -> int: + return a + + with self.assertRaises(Exception): + bind_params_to_func(h, {'a': 1, 'extra': 2}) + + def test_dataclass_arg_validation(self): + @dataclass + class P: + x: int + y: str + + def k(p: P) -> str: + return p.y + + # Passing an instance is fine + p = P(1, 'ok') + bound = bind_params_to_func(k, {'p': p}) + self.assertEqual(k(*bound.args, **bound.kwargs), 'ok') + + # Passing a dict should fail for dataclass per implementation + with self.assertRaises(ToolArgumentError): + bind_params_to_func(k, {'p': {'x': 1, 'y': 'nope'}}) + + +class TestPlainClassSchema(unittest.TestCase): + def test_plain_class_init_signature(self): + class C: + def __init__(self, a: int, b: str = 'x'): + self.a = a + self.b = b + + schema = _python_type_to_json_schema(C) + self.assertEqual(schema['type'], 'object') + props = schema['properties'] + self.assertIn('a', props) + self.assertIn('b', props) + # Only 'a' is required + self.assertIn('required', schema) + self.assertEqual(schema['required'], ['a']) + + def test_plain_class_slots_fallback(self): + class D: + __slots__ = ('m', 'n') + m: int + n: Optional[str] + + schema = _python_type_to_json_schema(D) + # Implementation builds properties from __slots__ with required for non-optional + self.assertEqual(schema['type'], 'object') + self.assertIn('properties', schema) + self.assertIn('m', schema['properties']) + self.assertIn('n', schema['properties']) + self.assertEqual(schema['properties']['m']['type'], 'integer') + self.assertEqual(schema['properties']['n']['type'], 'string') + self.assertIn('required', schema) + self.assertEqual(schema['required'], ['m']) + + +class TestDocstringUnsupportedWarning(unittest.TestCase): + def test_informal_param_info_warning(self): + def unsupported(x: int, y: str): + """Do something. + + The x parameter should be an integer indicating repetitions. The y parameter is used for labeling. + """ + return x, y + + # _extract_docstring_args is used via function_to_json_schema or directly. Use direct import path + from dapr.clients.grpc._conversation_helpers import _extract_docstring_args + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + res = _extract_docstring_args(unsupported) + self.assertEqual(res, {}) + self.assertTrue( + any('appears to contain parameter information' in str(wi.message) for wi in w) + ) + + +class TestLiteralSchemaMapping(unittest.TestCase): + def test_literal_strings_schema(self): + T = Literal['a', 'b', 'c'] + schema = _python_type_to_json_schema(T) + self.assertEqual(schema.get('type'), 'string') + self.assertEqual(set(schema['enum']), {'a', 'b', 'c'}) + + def test_literal_ints_schema(self): + T = Literal[1, 2, 3] + schema = _python_type_to_json_schema(T) + self.assertEqual(schema.get('type'), 'integer') + self.assertEqual(set(schema['enum']), {1, 2, 3}) + + def test_literal_nullable_string_schema(self): + T = Literal[None, 'x', 'y'] + schema = _python_type_to_json_schema(T) + # non-null types only string, should set 'type' to 'string' and include None in enum + self.assertEqual(schema.get('type'), 'string') + self.assertIn(None, schema['enum']) + self.assertIn('x', schema['enum']) + self.assertIn('y', schema['enum']) + + def test_literal_mixed_types_no_unified_type(self): + T = Literal['x', 1] + schema = _python_type_to_json_schema(T) + # Mixed non-null types -> no unified 'type' should be set + self.assertNotIn('type', schema) + self.assertEqual(set(schema['enum']), {'x', 1}) + + def test_literal_enum_members_normalized(self): + from enum import Enum + + class Mode(Enum): + FAST = 'fast' + SLOW = 'slow' + + T = Literal[Mode.FAST, Mode.SLOW] + schema = _python_type_to_json_schema(T) + self.assertEqual(schema.get('type'), 'string') + self.assertEqual(set(schema['enum']), {'fast', 'slow'}) + + def test_literal_bytes_and_bytearray_schema(self): + T = Literal[b'a', bytearray(b'b')] + schema = _python_type_to_json_schema(T) + # bytes/bytearray are coerced to string type for schema typing + self.assertEqual(schema.get('type'), 'string') + # The enum preserves the literal values as provided + self.assertIn(b'a', schema['enum']) + self.assertIn(bytearray(b'b'), schema['enum']) + + +# --- Helpers for Coercion tests + + +class Mode(Enum): + RED = 'red' + BLUE = 'blue' + + +@dataclass +class DC: + x: int + y: str + + +class Plain: + def __init__(self, a: int, b: str = 'x') -> None: + self.a = a + self.b = b + + +class TestScalarCoercions(unittest.TestCase): + def test_int_from_str_and_float_and_invalid(self): + def f(a: int) -> int: + return a + + # str -> int + bound = bind_params_to_func(f, {'a': ' 42 '}) + self.assertEqual(f(*bound.args, **bound.kwargs), 42) + + # float integral -> int + bound = bind_params_to_func(f, {'a': 3.0}) + self.assertEqual(f(*bound.args, **bound.kwargs), 3) + + # float non-integral -> error + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'a': 3.14}) + + def test_float_from_int_and_str(self): + def g(x: float) -> float: + return x + + bound = bind_params_to_func(g, {'x': 2}) + self.assertEqual(g(*bound.args, **bound.kwargs), 2.0) + + bound = bind_params_to_func(g, {'x': ' 3.5 '}) + self.assertEqual(g(*bound.args, **bound.kwargs), 3.5) + + def test_str_from_non_str(self): + def h(s: str) -> str: + return s + + bound = bind_params_to_func(h, {'s': 123}) + self.assertEqual(h(*bound.args, **bound.kwargs), '123') + + def test_bool_variants_and_invalid(self): + def b(flag: bool) -> bool: + return flag + + for v in ['true', 'False', 'YES', 'no', '1', '0', 'on', 'off']: + bound = bind_params_to_func(b, {'flag': v}) + # Ensure conversion yields actual bool + self.assertIsInstance(b(*bound.args, **bound.kwargs), bool) + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(b, {'flag': 'maybe'}) + + +class TestEnumCoercions(unittest.TestCase): + def test_enum_by_value_and_name_and_case_insensitive(self): + def f(m: Mode) -> Mode: + return m + + # by value + bound = bind_params_to_func(f, {'m': 'red'}) + self.assertEqual(f(*bound.args, **bound.kwargs), Mode.RED) + + # by exact name + bound = bind_params_to_func(f, {'m': 'BLUE'}) + self.assertEqual(f(*bound.args, **bound.kwargs), Mode.BLUE) + + # by case-insensitive name + bound = bind_params_to_func(f, {'m': 'red'}) # value already tested; use name lower + self.assertEqual(f(*bound.args, **bound.kwargs), Mode.RED) + + # invalid + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'m': 'green'}) + + +class TestCoerceAndValidateBranches(unittest.TestCase): + def test_optional_and_union(self): + def f(a: Optional[int], b: Union[str, int]) -> tuple: + return a, b + + bound = bind_params_to_func(f, {'a': '2', 'b': 5}) + # Union[str, int] tries str first; 5 is coerced to '5' + self.assertEqual(f(*bound.args, **bound.kwargs), (2, '5')) + + bound = bind_params_to_func(f, {'a': None, 'b': 'hello'}) + self.assertEqual(f(*bound.args, **bound.kwargs), (None, 'hello')) + + def test_list_and_dict_coercion(self): + def g(xs: List[int], mapping: Dict[int, float]) -> tuple: + return xs, mapping + + bound = bind_params_to_func(g, {'xs': ['1', '2', '3'], 'mapping': {'1': '2.5', 3: 4}}) + xs, mapping = g(*bound.args, **bound.kwargs) + self.assertEqual(xs, [1, 2, 3]) + self.assertEqual(mapping, {1: 2.5, 3: 4.0}) + + # Wrong type for list + with self.assertRaises(ToolArgumentError): + bind_params_to_func(g, {'xs': 'not-a-list', 'mapping': {}}) + + # Wrong type for dict + with self.assertRaises(ToolArgumentError): + bind_params_to_func(g, {'xs': [1], 'mapping': 'not-a-dict'}) + + def test_dataclass_optional_and_rejection_of_dict(self): + def f(p: Optional[DC]) -> Optional[str]: + return None if p is None else p.y + + # inst = DC(1, 'ok') + # bound = bind_params_to_func(f, {'p': inst}) + # self.assertEqual(f(*bound.args, **bound.kwargs), 'ok') + # + # bound = bind_params_to_func(f, {'p': None}) + # self.assertIsNone(f(*bound.args, **bound.kwargs)) + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'p': {'x': 1, 'y': 'no'}}) + + def test_plain_class_construction_from_dict_and_missing_arg(self): + def f(p: Plain) -> int: + return p.a + + # Construct from dict with coercion + bound = bind_params_to_func(f, {'p': {'a': '3'}}) + res = f(*bound.args, **bound.kwargs) + self.assertEqual(res, 3) + self.assertIsInstance(bound.arguments['p'], Plain) + self.assertEqual(bound.arguments['p'].b, 'x') # default applied + + # Missing required arg + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'p': {}}) + + def test_any_and_isinstance_fallback(self): + class C: + ... + + def f(a: Any, c: C) -> tuple: + return a, c + + c = C() + with self.assertRaises(ToolArgumentError) as ctx: + bind_params_to_func(f, {'a': object(), 'c': c}) + # _coerce_and_validate raises TypeError for Any; bind wraps it in ToolArgumentError + self.assertIsInstance(ctx.exception.__cause__, TypeError) + + +# ---- Helpers for test stringify + + +class Shade(Enum): + LIGHT = 'light' + DARK = 'dark' + + +@dataclass +class Pair: + a: int + b: str + + +class PlainWithDict: + def __init__(self): + self.x = 10 + self.y = 'y' + self.fn = lambda: 1 # callable should be filtered out + + +class TestStringifyToolOutputMore(unittest.TestCase): + def test_bytes_and_bytearray_branch(self): + raw = bytes([1, 2, 3, 254, 255]) + expected = 'base64:' + base64.b64encode(raw).decode('ascii') + self.assertEqual(stringify_tool_output(raw), expected) + + ba = bytearray(raw) + expected_ba = 'base64:' + base64.b64encode(bytes(ba)).decode('ascii') + self.assertEqual(stringify_tool_output(ba), expected_ba) + + def test_default_encoder_enum_dataclass_and___dict__(self): + # Enum -> value via default encoder (JSON string) + out_enum = stringify_tool_output(Shade.DARK) + self.assertEqual(out_enum, json.dumps('dark', ensure_ascii=False)) + + # Dataclass -> asdict via default encoder + p = Pair(3, 'z') + out_dc = stringify_tool_output(p) + self.assertEqual(json.loads(out_dc), {'a': 3, 'b': 'z'}) + + # __dict__ plain object -> filtered dict via default encoder + obj = PlainWithDict() + out_obj = stringify_tool_output(obj) + self.assertEqual(json.loads(out_obj), {'x': 10, 'y': 'y'}) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/clients/test_conversation_helpers_more.py b/tests/clients/test_conversation_helpers_more.py deleted file mode 100644 index 39d43f25..00000000 --- a/tests/clients/test_conversation_helpers_more.py +++ /dev/null @@ -1,434 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Additional targeted unit tests to increase coverage of -_dapr.clients.grpc._conversation_helpers. -""" -import base64 -import json -import unittest -import warnings -from dataclasses import dataclass -from typing import List, Literal, Optional, Any, Dict, Union - -from dapr.conf import settings -from dapr.clients.grpc._conversation_helpers import ( - ToolArgumentError, - _python_type_to_json_schema, - bind_params_to_func, - stringify_tool_output, -) - -from enum import Enum - - -class TestLargeEnumBehavior(unittest.TestCase): - def setUp(self): - # Save originals - self._orig_max = settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS - self._orig_beh = settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR - - def tearDown(self): - # Restore - settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = self._orig_max - settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = self._orig_beh - - def test_large_enum_compacted_to_string(self): - # Make threshold tiny to trigger large-enum path - settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 2 - settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'string' - - class BigEnum(Enum): - A = 'a' - B = 'b' - C = 'c' - D = 'd' - - schema = _python_type_to_json_schema(BigEnum) - # Should be compacted to string with description and examples - self.assertEqual(schema.get('type'), 'string') - self.assertIn('description', schema) - self.assertIn('examples', schema) - self.assertTrue(len(schema['examples']) > 0) - - def test_large_enum_error_mode(self): - settings.DAPR_CONVERSATION_TOOLS_MAX_ENUM_ITEMS = 1 - settings.DAPR_CONVERSATION_TOOLS_LARGE_ENUM_BEHAVIOR = 'error' - - from enum import Enum - - class BigEnum(Enum): - A = 'a' - B = 'b' - - with self.assertRaises(ValueError): - _python_type_to_json_schema(BigEnum) - - -class TestCoercionsAndBinding(unittest.TestCase): - def test_coerce_bool_variants(self): - def f(flag: bool) -> bool: - return flag - - # True-ish variants - for v in ['true', 'True', 'YES', '1', 'on', ' y ']: - bound = bind_params_to_func(f, {'flag': v}) - self.assertIs(f(*bound.args, **bound.kwargs), True) - - # False-ish variants - for v in ['false', 'False', 'NO', '0', 'off', ' n ']: - bound = bind_params_to_func(f, {'flag': v}) - self.assertIs(f(*bound.args, **bound.kwargs), False) - - # Invalid - with self.assertRaises(ToolArgumentError): - bind_params_to_func(f, {'flag': 'maybe'}) - - def test_literal_numeric_from_string(self): - def g(x: Literal[1, 2, 3]) -> int: - return x # type: ignore[return-value] - - bound = bind_params_to_func(g, {'x': '2'}) - self.assertEqual(g(*bound.args, **bound.kwargs), 2) - - def test_unexpected_kwarg_is_rejected(self): - def h(a: int) -> int: - return a - - with self.assertRaises(Exception): - bind_params_to_func(h, {'a': 1, 'extra': 2}) - - def test_dataclass_arg_validation(self): - @dataclass - class P: - x: int - y: str - - def k(p: P) -> str: - return p.y - - # Passing an instance is fine - p = P(1, 'ok') - bound = bind_params_to_func(k, {'p': p}) - self.assertEqual(k(*bound.args, **bound.kwargs), 'ok') - - # Passing a dict should fail for dataclass per implementation - with self.assertRaises(ToolArgumentError): - bind_params_to_func(k, {'p': {'x': 1, 'y': 'nope'}}) - - -class TestPlainClassSchema(unittest.TestCase): - def test_plain_class_init_signature(self): - class C: - def __init__(self, a: int, b: str = 'x'): - self.a = a - self.b = b - - schema = _python_type_to_json_schema(C) - self.assertEqual(schema['type'], 'object') - props = schema['properties'] - self.assertIn('a', props) - self.assertIn('b', props) - # Only 'a' is required - self.assertIn('required', schema) - self.assertEqual(schema['required'], ['a']) - - def test_plain_class_slots_fallback(self): - class D: - __slots__ = ('m', 'n') - m: int - n: Optional[str] - - schema = _python_type_to_json_schema(D) - # Implementation builds properties from __slots__ with required for non-optional - self.assertEqual(schema['type'], 'object') - self.assertIn('properties', schema) - self.assertIn('m', schema['properties']) - self.assertIn('n', schema['properties']) - self.assertEqual(schema['properties']['m']['type'], 'integer') - self.assertEqual(schema['properties']['n']['type'], 'string') - self.assertIn('required', schema) - self.assertEqual(schema['required'], ['m']) - - -class TestDocstringUnsupportedWarning(unittest.TestCase): - def test_informal_param_info_warning(self): - def unsupported(x: int, y: str): - """Do something. - - The x parameter should be an integer indicating repetitions. The y parameter is used for labeling. - """ - return x, y - - # _extract_docstring_args is used via function_to_json_schema or directly. Use direct import path - from dapr.clients.grpc._conversation_helpers import _extract_docstring_args - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - res = _extract_docstring_args(unsupported) - self.assertEqual(res, {}) - self.assertTrue( - any('appears to contain parameter information' in str(wi.message) for wi in w) - ) - - -class TestLiteralSchemaMapping(unittest.TestCase): - def test_literal_strings_schema(self): - T = Literal['a', 'b', 'c'] - schema = _python_type_to_json_schema(T) - self.assertEqual(schema.get('type'), 'string') - self.assertEqual(set(schema['enum']), {'a', 'b', 'c'}) - - def test_literal_ints_schema(self): - T = Literal[1, 2, 3] - schema = _python_type_to_json_schema(T) - self.assertEqual(schema.get('type'), 'integer') - self.assertEqual(set(schema['enum']), {1, 2, 3}) - - def test_literal_nullable_string_schema(self): - T = Literal[None, 'x', 'y'] - schema = _python_type_to_json_schema(T) - # non-null types only string, should set 'type' to 'string' and include None in enum - self.assertEqual(schema.get('type'), 'string') - self.assertIn(None, schema['enum']) - self.assertIn('x', schema['enum']) - self.assertIn('y', schema['enum']) - - def test_literal_mixed_types_no_unified_type(self): - T = Literal['x', 1] - schema = _python_type_to_json_schema(T) - # Mixed non-null types -> no unified 'type' should be set - self.assertNotIn('type', schema) - self.assertEqual(set(schema['enum']), {'x', 1}) - - def test_literal_enum_members_normalized(self): - from enum import Enum - - class Mode(Enum): - FAST = 'fast' - SLOW = 'slow' - - T = Literal[Mode.FAST, Mode.SLOW] - schema = _python_type_to_json_schema(T) - self.assertEqual(schema.get('type'), 'string') - self.assertEqual(set(schema['enum']), {'fast', 'slow'}) - - def test_literal_bytes_and_bytearray_schema(self): - T = Literal[b'a', bytearray(b'b')] - schema = _python_type_to_json_schema(T) - # bytes/bytearray are coerced to string type for schema typing - self.assertEqual(schema.get('type'), 'string') - # The enum preserves the literal values as provided - self.assertIn(b'a', schema['enum']) - self.assertIn(bytearray(b'b'), schema['enum']) - - -# --- Helpers for Coercion tests - - -class Mode(Enum): - RED = 'red' - BLUE = 'blue' - - -@dataclass -class DC: - x: int - y: str - - -class Plain: - def __init__(self, a: int, b: str = 'x') -> None: - self.a = a - self.b = b - - -class TestScalarCoercions(unittest.TestCase): - def test_int_from_str_and_float_and_invalid(self): - def f(a: int) -> int: - return a - - # str -> int - bound = bind_params_to_func(f, {'a': ' 42 '}) - self.assertEqual(f(*bound.args, **bound.kwargs), 42) - - # float integral -> int - bound = bind_params_to_func(f, {'a': 3.0}) - self.assertEqual(f(*bound.args, **bound.kwargs), 3) - - # float non-integral -> error - with self.assertRaises(ToolArgumentError): - bind_params_to_func(f, {'a': 3.14}) - - def test_float_from_int_and_str(self): - def g(x: float) -> float: - return x - - bound = bind_params_to_func(g, {'x': 2}) - self.assertEqual(g(*bound.args, **bound.kwargs), 2.0) - - bound = bind_params_to_func(g, {'x': ' 3.5 '}) - self.assertEqual(g(*bound.args, **bound.kwargs), 3.5) - - def test_str_from_non_str(self): - def h(s: str) -> str: - return s - - bound = bind_params_to_func(h, {'s': 123}) - self.assertEqual(h(*bound.args, **bound.kwargs), '123') - - def test_bool_variants_and_invalid(self): - def b(flag: bool) -> bool: - return flag - - for v in ['true', 'False', 'YES', 'no', '1', '0', 'on', 'off']: - bound = bind_params_to_func(b, {'flag': v}) - # Ensure conversion yields actual bool - self.assertIsInstance(b(*bound.args, **bound.kwargs), bool) - - with self.assertRaises(ToolArgumentError): - bind_params_to_func(b, {'flag': 'maybe'}) - - -class TestEnumCoercions(unittest.TestCase): - def test_enum_by_value_and_name_and_case_insensitive(self): - def f(m: Mode) -> Mode: - return m - - # by value - bound = bind_params_to_func(f, {'m': 'red'}) - self.assertEqual(f(*bound.args, **bound.kwargs), Mode.RED) - - # by exact name - bound = bind_params_to_func(f, {'m': 'BLUE'}) - self.assertEqual(f(*bound.args, **bound.kwargs), Mode.BLUE) - - # by case-insensitive name - bound = bind_params_to_func(f, {'m': 'red'}) # value already tested; use name lower - self.assertEqual(f(*bound.args, **bound.kwargs), Mode.RED) - - # invalid - with self.assertRaises(ToolArgumentError): - bind_params_to_func(f, {'m': 'green'}) - - -class TestCoerceAndValidateBranches(unittest.TestCase): - def test_optional_and_union(self): - def f(a: Optional[int], b: Union[str, int]) -> tuple: - return a, b - - bound = bind_params_to_func(f, {'a': '2', 'b': 5}) - # Union[str, int] tries str first; 5 is coerced to '5' - self.assertEqual(f(*bound.args, **bound.kwargs), (2, '5')) - - bound = bind_params_to_func(f, {'a': None, 'b': 'hello'}) - self.assertEqual(f(*bound.args, **bound.kwargs), (None, 'hello')) - - def test_list_and_dict_coercion(self): - def g(xs: List[int], mapping: Dict[int, float]) -> tuple: - return xs, mapping - - bound = bind_params_to_func(g, {'xs': ['1', '2', '3'], 'mapping': {'1': '2.5', 3: 4}}) - xs, mapping = g(*bound.args, **bound.kwargs) - self.assertEqual(xs, [1, 2, 3]) - self.assertEqual(mapping, {1: 2.5, 3: 4.0}) - - # Wrong type for list - with self.assertRaises(ToolArgumentError): - bind_params_to_func(g, {'xs': 'not-a-list', 'mapping': {}}) - - # Wrong type for dict - with self.assertRaises(ToolArgumentError): - bind_params_to_func(g, {'xs': [1], 'mapping': 'not-a-dict'}) - - def test_dataclass_optional_and_rejection_of_dict(self): - def f(p: Optional[DC]) -> Optional[str]: - return None if p is None else p.y - - inst = DC(1, 'ok') - bound = bind_params_to_func(f, {'p': inst}) - self.assertEqual(f(*bound.args, **bound.kwargs), 'ok') - - bound = bind_params_to_func(f, {'p': None}) - self.assertIsNone(f(*bound.args, **bound.kwargs)) - - with self.assertRaises(ToolArgumentError): - bind_params_to_func(f, {'p': {'x': 1, 'y': 'no'}}) - - def test_plain_class_construction_from_dict_and_missing_arg(self): - def f(p: Plain) -> int: - return p.a - - # Construct from dict with coercion - bound = bind_params_to_func(f, {'p': {'a': '3'}}) - res = f(*bound.args, **bound.kwargs) - self.assertEqual(res, 3) - self.assertIsInstance(bound.arguments['p'], Plain) - self.assertEqual(bound.arguments['p'].b, 'x') # default applied - - # Missing required arg - with self.assertRaises(ToolArgumentError): - bind_params_to_func(f, {'p': {}}) - - def test_any_and_isinstance_fallback(self): - class C: - ... - - def f(a: Any, c: C) -> tuple: - return a, c - - c = C() - with self.assertRaises(ToolArgumentError) as ctx: - bind_params_to_func(f, {'a': object(), 'c': c}) - # _coerce_and_validate raises TypeError for Any; bind wraps it in ToolArgumentError - self.assertIsInstance(ctx.exception.__cause__, TypeError) - - -# ---- Helpers for test stringify - - -class Shade(Enum): - LIGHT = 'light' - DARK = 'dark' - - -@dataclass -class Pair: - a: int - b: str - - -class PlainWithDict: - def __init__(self): - self.x = 10 - self.y = 'y' - self.fn = lambda: 1 # callable should be filtered out - - -class TestStringifyToolOutputMore(unittest.TestCase): - def test_bytes_and_bytearray_branch(self): - raw = bytes([1, 2, 3, 254, 255]) - expected = 'base64:' + base64.b64encode(raw).decode('ascii') - self.assertEqual(stringify_tool_output(raw), expected) - - ba = bytearray(raw) - expected_ba = 'base64:' + base64.b64encode(bytes(ba)).decode('ascii') - self.assertEqual(stringify_tool_output(ba), expected_ba) - - def test_default_encoder_enum_dataclass_and___dict__(self): - # Enum -> value via default encoder (JSON string) - out_enum = stringify_tool_output(Shade.DARK) - self.assertEqual(out_enum, json.dumps('dark', ensure_ascii=False)) - - # Dataclass -> asdict via default encoder - p = Pair(3, 'z') - out_dc = stringify_tool_output(p) - self.assertEqual(json.loads(out_dc), {'a': 3, 'b': 'z'}) - - # __dict__ plain object -> filtered dict via default encoder - obj = PlainWithDict() - out_obj = stringify_tool_output(obj) - self.assertEqual(json.loads(out_obj), {'x': 10, 'y': 'y'}) - - -if __name__ == '__main__': - unittest.main() From 5e1bab33bc0d52859d42b807653eb71d145be48d Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Thu, 4 Sep 2025 11:06:57 +0200 Subject: [PATCH 28/29] Linter Signed-off-by: Elena Kolevska --- tests/clients/test_conversation_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py index c9ed3415..62f2f69a 100644 --- a/tests/clients/test_conversation_helpers.py +++ b/tests/clients/test_conversation_helpers.py @@ -44,6 +44,7 @@ ConversationMessageOfSystem, ) + def test_string_passthrough(): assert stringify_tool_output('hello') == 'hello' @@ -2147,5 +2148,6 @@ def test_default_encoder_enum_dataclass_and___dict__(self): out_obj = stringify_tool_output(obj) self.assertEqual(json.loads(out_obj), {'x': 10, 'y': 'y'}) + if __name__ == '__main__': unittest.main() From bc3dcf04632c7429dad7c415f473df0606317530 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:11:51 -0500 Subject: [PATCH 29/29] fix typing issue with UnionType in py3.9 Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/_conversation_helpers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index ac8bfbb9..37bb81c1 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -18,7 +18,6 @@ import string from dataclasses import fields, is_dataclass from enum import Enum -from types import UnionType from typing import ( Any, Callable, @@ -37,6 +36,12 @@ from dapr.conf import settings +import types + +# Make mypy happy. Runtime handle: real class on 3.10+, else None. +# TODO: Python 3.9 is about to be end-of-life, so we can drop this at some point next year (2026) +UnionType: Any = getattr(types, 'UnionType', None) + # duplicated from conversation to avoid circular import Params = Union[Mapping[str, Any], Sequence[Any], None] @@ -857,7 +862,9 @@ def _coerce_literal(value: Any, lit_args: List[Any]) -> Any: def _is_union(t) -> bool: origin = get_origin(t) - return origin in (Union, UnionType) # handles 3.8–3.13+ + if origin is Union: + return True + return UnionType is not None and origin is UnionType def _coerce_and_validate(value: Any, expected_type: Any) -> Any: