# # Copyright 2026 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 logging from datetime import date, datetime from typing import Any, Dict, Iterable, List, Literal, Optional, Type, Union from pydantic import BaseModel, ValidationError from dapr_agents.llm.utils.structure import StructureHandler from dapr_agents.prompt.prompty import Prompty, PromptyHelper from dapr_agents.tool.base import AgentTool from dapr_agents.tool.utils.tool import ToolHelper from dapr_agents.types.message import BaseMessage logger = logging.getLogger(__name__) def _make_json_serializable(obj: Any) -> Any: """Recursively convert datetime/date to ISO strings for JSON serialization.""" if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, date): return obj.isoformat() if isinstance(obj, dict): return {k: _make_json_serializable(v) for k, v in obj.items()} if isinstance(obj, list): return [_make_json_serializable(v) for v in obj] return obj class RequestHandler: """ Handles the preparation of requests for language models. """ @staticmethod def process_prompty_messages( prompty: Prompty, inputs: Dict[str, Any] = {} ) -> List[Dict[str, Any]]: """ Process and format messages based on Prompty template and provided inputs. Args: prompty (Prompty): The Prompty instance containing the template and settings. inputs (Dict[str, Any]): Input variables for the Prompty template (default is an empty dictionary). Returns: List[Dict[str, Any]]: Processed and prepared messages. """ # Prepare inputs and generate messages from Prompty content api_type = prompty.model.api prepared_inputs = PromptyHelper.prepare_inputs( inputs, prompty.inputs, prompty.sample ) messages = PromptyHelper.to_prompt( prompty.content, prepared_inputs, api_type=api_type ) return messages @staticmethod def normalize_chat_messages( messages: Union[ str, Dict[str, Any], BaseMessage, Iterable[Union[Dict[str, Any], BaseMessage]], ], ) -> List[Dict[str, Any]]: """ Normalize and validate the input messages into a list of dictionaries. Args: messages (Union[str, Dict[str, Any], BaseMessage, Iterable[Union[Dict[str, Any], BaseMessage]]]): Input messages in various formats (string, dict, BaseMessage, or an iterable). Returns: List[Dict[str, Any]]: A list of normalized message dictionaries with keys 'role' and 'content'. Raises: ValueError: If the input format is unsupported or if required fields are missing in a dictionary. """ # Initialize an empty list to store the normalized messages normalized_messages = [] # Use a queue to process messages iteratively and handle nested structures queue = [messages] while queue: msg = queue.pop(0) if isinstance(msg, str): normalized_messages.append({"role": "user", "content": msg}) elif isinstance(msg, BaseMessage): normalized_messages.append(msg.model_dump()) elif isinstance(msg, dict): role = msg.get("role") if role not in {"user", "assistant", "tool", "system"}: raise ValueError( f"Unrecognized role '{role}'. Supported roles are 'user', 'assistant', 'tool', or 'system'." ) normalized_messages.append(msg) elif isinstance(msg, Iterable) and not isinstance(msg, (str, dict)): queue.extend(msg) else: raise ValueError(f"Unsupported message format: {type(msg)}") return normalized_messages @staticmethod def process_params( params: Dict[str, Any], llm_provider: str, tools: Optional[List[Union[AgentTool, Dict[str, Any]]]] = None, response_format: Optional[Union[Type[BaseModel], Dict[str, Any]]] = None, structured_mode: Literal["json", "function_call"] = "json", ) -> Dict[str, Any]: """ Prepare request parameters for the language model. Args: params: Raw request params (messages/inputs, model, etc.). llm_provider: Provider key, e.g. "openai", "dapr". tools: Tools to expose to the model (AgentTool or already-shaped dicts). response_format: - If structured_mode == "json": a JSON Schema dict or a Pydantic model (we'll convert) to request raw JSON output. - If structured_mode == "function_call": a Pydantic model describing the function/tool signature for model-side function calling. structured_mode: "json" for raw JSON structured output, "function_call" for tool/function calling. Returns: A params dict ready for the target provider. """ # Tools if tools: logger.info("Tools are available in the request.") params["tools"] = [ ToolHelper.format_tool(t, tool_format=llm_provider) for t in tools ] # Structured output if response_format: logger.info(f"Structured Mode Activated! mode={structured_mode}") # If we're on Dapr, we cannot rely on OpenAI-style `response_format`. # Add a small system nudge to enforce JSON-only output so we can parse reliably. if llm_provider == "dapr": params = StructureHandler.ensure_json_only_system_prompt(params) # Generate provider-specific request params params = StructureHandler.generate_request( response_format=response_format, llm_provider=llm_provider, structured_mode=structured_mode, **params, ) return params @staticmethod def make_params_json_serializable(params: Dict[str, Any]) -> Dict[str, Any]: """ Return a copy of params with datetime/date values converted to ISO strings so the dict is safe for JSON serialization (e.g. OpenAI API request body). """ return _make_json_serializable(params) @staticmethod def validate_request( request: Union[BaseModel, Dict[str, Any]], request_class: Type[BaseModel] ) -> BaseModel: """ Validate and transform a dictionary into a Pydantic object. Args: request (Union[BaseModel, Dict[str, Any]]): The request data as a dictionary or a Pydantic object. request_class (Type[BaseModel]): The Pydantic model class for validation. Returns: BaseModel: A validated Pydantic object. Raises: ValueError: If validation fails. """ if isinstance(request, dict): try: request = request_class(**request) except ValidationError as e: raise ValueError(f"Validation error: {e}") try: validated_request = request_class.model_validate(request) except ValidationError as e: raise ValueError(f"Validation error: {e}") return validated_request