From 3a31d80eb02abea8b16e527ad4031d2ad9704305 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 3 Feb 2026 12:49:50 -0800 Subject: [PATCH] add multi part conversation support --- litellm/llms/a2a/chat/transformation.py | 29 +++++++++---------- litellm/llms/a2a/common_utils.py | 38 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/litellm/llms/a2a/chat/transformation.py b/litellm/llms/a2a/chat/transformation.py index 31938c8c74c..243fba63719 100644 --- a/litellm/llms/a2a/chat/transformation.py +++ b/litellm/llms/a2a/chat/transformation.py @@ -12,7 +12,11 @@ from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMExcepti from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import Choices, Message, ModelResponse -from ..common_utils import A2AError, extract_text_from_a2a_response +from ..common_utils import ( + A2AError, + convert_messages_to_prompt, + extract_text_from_a2a_response, +) from .streaming_iterator import A2AModelResponseIterator @@ -140,24 +144,19 @@ class A2AConfig(BaseConfig): # Generate request ID request_id = str(uuid.uuid4()) - # Convert last message to A2A format (A2A protocol typically sends single message) - # For multi-turn conversations, we'd need to handle context differently if not messages: raise ValueError("At least one message is required for A2A completion") - last_message = messages[-1] + # Convert all messages to maintain conversation history + # Use helper to format conversation with role prefixes + full_context = convert_messages_to_prompt(messages) - # Convert to dict if needed - msg_dict: Dict[str, Any] - if isinstance(last_message, BaseModel): - msg_dict = last_message.model_dump() - elif isinstance(last_message, dict): - msg_dict = cast(Dict[str, Any], last_message) - else: - msg_dict = dict(last_message) # type: ignore - - # Transform to A2A message format - a2a_message = self._openai_message_to_a2a_message(msg_dict) + # Create single A2A message with full conversation context + a2a_message = { + "role": "user", + "parts": [{"kind": "text", "text": full_context}], + "messageId": str(uuid.uuid4()), + } # Build JSON-RPC 2.0 request # For A2A protocol, the method is "message/send" for non-streaming diff --git a/litellm/llms/a2a/common_utils.py b/litellm/llms/a2a/common_utils.py index dbf126b1052..4c7da78b42f 100644 --- a/litellm/llms/a2a/common_utils.py +++ b/litellm/llms/a2a/common_utils.py @@ -3,7 +3,13 @@ Common utilities for A2A (Agent-to-Agent) Protocol """ from typing import Any, Dict, List +from pydantic import BaseModel + +from litellm.litellm_core_utils.prompt_templates.common_utils import ( + convert_content_list_to_str, +) from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.types.llms.openai import AllMessageValues class A2AError(BaseLLMException): @@ -22,6 +28,38 @@ class A2AError(BaseLLMException): ) +def convert_messages_to_prompt(messages: List[AllMessageValues]) -> str: + """ + Convert OpenAI messages to a single prompt string for A2A agent. + + Formats each message as "{role}: {content}" and joins with newlines + to preserve conversation history. Handles both string and list content. + + Args: + messages: List of OpenAI-format messages + + Returns: + Formatted prompt string with full conversation context + """ + conversation_parts = [] + for msg in messages: + # Use LiteLLM's helper to extract text from content (handles both str and list) + content_text = convert_content_list_to_str(message=msg) + + # Get role + if isinstance(msg, BaseModel): + role = msg.model_dump().get("role", "user") + elif isinstance(msg, dict): + role = msg.get("role", "user") + else: + role = dict(msg).get("role", "user") # type: ignore + + if content_text: + conversation_parts.append(f"{role}: {content_text}") + + return "\n".join(conversation_parts) + + def extract_text_from_a2a_message( message: Dict[str, Any], depth: int = 0, max_depth: int = 10 ) -> str: