mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
UI: - Auto-trigger discovery when connection details are filled; remove the "Use these selections" button (selection syncs live to parent, user just clicks Next). - Edit Settings: auto-discover upstream card on open; cross-check with DB-stored card so only already-saved skills/capabilities are pre-ticked. - Extract shared buildDiscoveryRequest + selectionsFromSavedAgentCard helpers into agent_discovery_utils.ts so both add and edit flows share the same logic. Backend: - agent_card.py: rename the proxy security requirements field from the non-standard ``securityRequirements`` to the spec-correct ``security`` key (matches AgentCard TypedDict and A2A/OpenAPI convention). - agent_card.py: remove ``securityRequirements`` from _ALLOWED_TOP_LEVEL_KEYS. - endpoints.py: _build_merged_agent_card now forwards agent_name and description from the request so the stored card reflects the admin- supplied name, not just whatever the upstream card advertised. - utils.py: remove overly-broad ``or "parts" in result`` fallback; use ``kind == "message"`` check only to avoid false matches on future result types that happen to include a ``parts`` field. - test_agent_card.py: update assertions to expect ``security`` key. Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
4.5 KiB
Python
144 lines
4.5 KiB
Python
"""
|
|
Utility functions for A2A protocol.
|
|
"""
|
|
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
|
|
|
|
import litellm
|
|
from litellm._logging import verbose_logger
|
|
|
|
if TYPE_CHECKING:
|
|
from a2a.types import SendMessageRequest, SendStreamingMessageRequest
|
|
|
|
|
|
class A2ARequestUtils:
|
|
"""Utility class for A2A request/response processing."""
|
|
|
|
@staticmethod
|
|
def extract_text_from_message(message: Any) -> str:
|
|
"""
|
|
Extract text content from A2A message parts.
|
|
|
|
Args:
|
|
message: A2A message dict or object with 'parts' containing text parts
|
|
|
|
Returns:
|
|
Concatenated text from all text parts
|
|
"""
|
|
if message is None:
|
|
return ""
|
|
|
|
# Handle both dict and object access
|
|
if isinstance(message, dict):
|
|
parts = message.get("parts", [])
|
|
else:
|
|
parts = getattr(message, "parts", []) or []
|
|
|
|
text_parts: List[str] = []
|
|
for part in parts:
|
|
if isinstance(part, dict):
|
|
if part.get("kind") == "text":
|
|
text_parts.append(part.get("text", ""))
|
|
else:
|
|
if getattr(part, "kind", None) == "text":
|
|
text_parts.append(getattr(part, "text", ""))
|
|
|
|
return " ".join(text_parts)
|
|
|
|
@staticmethod
|
|
def extract_text_from_response(response_dict: Dict[str, Any]) -> str:
|
|
"""
|
|
Extract text content from A2A response result.
|
|
|
|
Args:
|
|
response_dict: A2A response dict with 'result' containing message
|
|
|
|
Returns:
|
|
Text from response message parts
|
|
"""
|
|
result = response_dict.get("result", {})
|
|
if not isinstance(result, dict):
|
|
return ""
|
|
|
|
# Direct message format (A2A spec): detect by explicit kind tag only.
|
|
# The "parts" heuristic is too broad and would match any future result
|
|
# type that happens to include a "parts" field.
|
|
if result.get("kind") == "message":
|
|
return A2ARequestUtils.extract_text_from_message(result)
|
|
|
|
message = result.get("message", {})
|
|
return A2ARequestUtils.extract_text_from_message(message)
|
|
|
|
@staticmethod
|
|
def get_input_message_from_request(
|
|
request: "Union[SendMessageRequest, SendStreamingMessageRequest]",
|
|
) -> Any:
|
|
"""
|
|
Extract the input message from an A2A request.
|
|
|
|
Args:
|
|
request: The A2A SendMessageRequest or SendStreamingMessageRequest
|
|
|
|
Returns:
|
|
The message object/dict or None
|
|
"""
|
|
params = getattr(request, "params", None)
|
|
if params is None:
|
|
return None
|
|
return getattr(params, "message", None)
|
|
|
|
@staticmethod
|
|
def count_tokens(text: str) -> int:
|
|
"""
|
|
Count tokens in text using litellm.token_counter.
|
|
|
|
Args:
|
|
text: Text to count tokens for
|
|
|
|
Returns:
|
|
Token count, or 0 if counting fails
|
|
"""
|
|
if not text:
|
|
return 0
|
|
try:
|
|
return litellm.token_counter(text=text)
|
|
except Exception:
|
|
verbose_logger.debug("Failed to count tokens")
|
|
return 0
|
|
|
|
@staticmethod
|
|
def calculate_usage_from_request_response(
|
|
request: "Union[SendMessageRequest, SendStreamingMessageRequest]",
|
|
response_dict: Dict[str, Any],
|
|
) -> Tuple[int, int, int]:
|
|
"""
|
|
Calculate token usage from A2A request and response.
|
|
|
|
Args:
|
|
request: The A2A SendMessageRequest or SendStreamingMessageRequest
|
|
response_dict: The A2A response as a dict
|
|
|
|
Returns:
|
|
Tuple of (prompt_tokens, completion_tokens, total_tokens)
|
|
"""
|
|
# Count input tokens
|
|
input_message = A2ARequestUtils.get_input_message_from_request(request)
|
|
input_text = A2ARequestUtils.extract_text_from_message(input_message)
|
|
prompt_tokens = A2ARequestUtils.count_tokens(input_text)
|
|
|
|
# Count output tokens
|
|
output_text = A2ARequestUtils.extract_text_from_response(response_dict)
|
|
completion_tokens = A2ARequestUtils.count_tokens(output_text)
|
|
|
|
total_tokens = prompt_tokens + completion_tokens
|
|
|
|
return prompt_tokens, completion_tokens, total_tokens
|
|
|
|
|
|
# Backwards compatibility aliases
|
|
def extract_text_from_a2a_message(message: Any) -> str:
|
|
return A2ARequestUtils.extract_text_from_message(message)
|
|
|
|
|
|
def extract_text_from_a2a_response(response_dict: Dict[str, Any]) -> str:
|
|
return A2ARequestUtils.extract_text_from_response(response_dict)
|