Akshay66777's picture
AgentScope Gradio chat app — ZeroGPU-ready
9792ea7 verified
Raw
History Blame Contribute Delete
20.1 kB
# -*- coding: utf-8 -*-
"""Extract attributes from AgentScope components for OpenTelemetry tracing."""
import inspect
from typing import Any, Dict, TYPE_CHECKING
from ...message import Msg, ToolCallBlock
from ._attributes import (
SpanAttributes,
OperationNameValues,
ProviderNameValues,
)
from ._converter import _convert_block_to_part
from ._utils import _serialize_to_str
from ...model import ChatResponse, ChatModelBase
from ...event import (
ExternalExecutionResultEvent,
UserConfirmResultEvent,
)
if TYPE_CHECKING:
from ...agent import Agent
from ...tool import Toolkit, ToolChoice
_CLASS_NAME_MAP = {
"dashscope": ProviderNameValues.DASHSCOPE,
"openai": ProviderNameValues.OPENAI,
"anthropic": ProviderNameValues.ANTHROPIC,
"gemini": ProviderNameValues.GCP_GEMINI,
"ollama": ProviderNameValues.OLLAMA,
"deepseek": ProviderNameValues.DEEPSEEK,
"xai": ProviderNameValues.XAI,
"moonshot": ProviderNameValues.MOONSHOT,
}
# Map base URL fragments to provider names for OpenAI-compatible APIs
_BASE_URL_PROVIDER_MAP = [
("api.openai.com", ProviderNameValues.OPENAI),
("dashscope", ProviderNameValues.DASHSCOPE),
("deepseek", ProviderNameValues.DEEPSEEK),
("moonshot", ProviderNameValues.MOONSHOT),
("generativelanguage.googleapis.com", ProviderNameValues.GCP_GEMINI),
("openai.azure.com", ProviderNameValues.AZURE_AI_OPENAI),
("amazonaws.com", ProviderNameValues.AWS_BEDROCK),
("api.x.ai", ProviderNameValues.XAI),
]
def _get_common_attributes(session_id: str = "") -> Dict[str, str]:
"""Get common attributes for all spans.
Args:
session_id (`str`):
The session ID to set as conversation ID.
Returns:
`Dict[str, str]`:
Common span attributes including conversation ID.
"""
return {
SpanAttributes.GEN_AI_CONVERSATION_ID: (
session_id if session_id else "[no_session_id]"
),
}
def _get_provider_name(instance: "ChatModelBase") -> str:
"""Get provider name from ChatModelBase instance.
Maps ChatModelBase class names to provider names, with special handling
for OpenAI-compatible APIs that may use different base URLs.
This follows the implementation pattern from agentscope-java PR #73.
Args:
instance (`ChatModelBase`):
The chat model instance to get the provider name for.
Returns:
`str`:
Provider name (e.g., "openai", "dashscope", "anthropic")
"""
classname = instance.__class__.__name__
# For other model types, use direct mapping
prefix_key = (
classname.removesuffix("ChatModel")
.removesuffix("MultiAgentModel")
.removesuffix("ResponseModel")
.lower()
)
# Special handling for OpenAI-compatible models — inspect base_url
# from credential to distinguish the actual provider.
if prefix_key == "openai":
base_url = getattr(instance.credential, "base_url", None)
if base_url:
base_url = str(base_url)
for url_fragment, provider_name in _BASE_URL_PROVIDER_MAP:
if url_fragment in base_url:
return provider_name
return ProviderNameValues.OPENAI
return _CLASS_NAME_MAP.get(prefix_key, "unknown")
def _get_tool_definitions(
tools: list[dict[str, Any]] | None,
tool_choice: "ToolChoice | None",
) -> str | None:
"""Extract and serialize tool definitions for tracing.
Converts AgentScope/OpenAI nested tool format to OpenTelemetry GenAI
flat format for tracing.
Args:
tools (`list[dict[str, Any]] | None`, optional):
List of tool definitions in OpenAI format with nested
structure: ``[{"type": "function", "function": {...}}]``
tool_choice (`ToolChoice | None`, optional):
Tool choice configuration with ``mode`` and optional ``tools``
fields. If mode is ``"none"``, returns None to indicate tools
should not be traced.
Returns:
`str | None`:
Serialized tool definitions in flat format:
``[{"type": "function", "name": ..., "parameters": ...}]``
or None if tools should not be traced (e.g., tools is None/empty
or tool_choice is "none").
"""
# No tools provided
if tools is None or not isinstance(tools, list) or len(tools) == 0:
return None
# Tool choice is explicitly "none" (model should not use tools)
if tool_choice is not None and tool_choice.mode == "none":
return None
try:
# Convert nested format to flat format for OpenTelemetry GenAI
# TODO: Currently only supports "function" type tools. If other tool
# types are added in the future (e.g., "retrieval", "code_interpreter",
# "browser"), this conversion logic needs to be updated to handle them.
flat_tools = []
for tool in tools:
if not isinstance(tool, dict) or "function" not in tool:
continue
func_def = tool["function"]
flat_tool = {
"type": tool.get("type", "function"),
"name": func_def.get("name"),
"description": func_def.get("description"),
"parameters": func_def.get("parameters"),
}
# Remove None values
flat_tool = {k: v for k, v in flat_tool.items() if v is not None}
flat_tools.append(flat_tool)
if flat_tools:
return _serialize_to_str(flat_tools)
return None
except Exception:
return None
def _get_llm_request_attributes(
instance: "ChatModelBase",
kwargs: Dict[str, Any],
) -> Dict[str, Any]:
"""Get LLM request attributes for OpenTelemetry tracing.
Extracts request parameters from LLM model calls into GenAI attributes.
Args:
instance (`ChatModelBase`):
The chat model instance making the request.
kwargs (`Dict[str, Any]`):
Keyword arguments including generation parameters such as
temperature, top_p, top_k, max_tokens, presence_penalty,
frequency_penalty, stop_sequences, seed, tools, and tool_choice.
Returns:
`Dict[str, Any]`:
OpenTelemetry GenAI attributes with mixed-type values (``str``,
``int``, ``float``, or ``list``), including operation name,
provider name, model name, generation parameters (e.g.
temperature, max_tokens, stop_sequences), and tool definitions.
"""
attributes = {
# required attributes
SpanAttributes.GEN_AI_OPERATION_NAME: OperationNameValues.CHAT,
SpanAttributes.GEN_AI_PROVIDER_NAME: _get_provider_name(instance),
# conditionally required attributes
SpanAttributes.GEN_AI_REQUEST_MODEL: instance.model,
# recommended attributes
SpanAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature"),
SpanAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("p")
or kwargs.get("top_p"),
SpanAttributes.GEN_AI_REQUEST_TOP_K: kwargs.get("top_k"),
SpanAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
SpanAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get(
"presence_penalty",
),
SpanAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get(
"frequency_penalty",
),
SpanAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: kwargs.get(
"stop_sequences",
),
SpanAttributes.GEN_AI_REQUEST_SEED: kwargs.get("seed"),
}
# Extract tool definitions if provided
tool_definitions = _get_tool_definitions(
tools=kwargs.get("tools"),
tool_choice=kwargs.get("tool_choice"),
)
if tool_definitions:
attributes[SpanAttributes.GEN_AI_TOOL_DEFINITIONS] = tool_definitions
return {k: v for k, v in attributes.items() if v is not None}
def _get_llm_span_name(attributes: Dict[str, str]) -> str:
"""Generate span name for LLM operations.
Args:
attributes (`Dict[str, str]`):
LLM request attributes dictionary containing operation name and
model name.
Returns:
`str`:
Formatted span name in the format "{operation} {model}",
e.g., "chat gpt-4" or "chat qwen-plus".
"""
return (
f"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} "
f"{attributes[SpanAttributes.GEN_AI_REQUEST_MODEL]}"
)
def _get_llm_output_messages(
chat_response: ChatResponse | None,
) -> list[dict[str, Any]]:
"""Extract and format LLM output messages for tracing.
Converts ChatResponse objects to standardized message format compatible
with OpenTelemetry GenAI specification.
Args:
chat_response (` ChatResponse | None`):
Chat response object with content blocks. Should be a ChatResponse
instance containing content blocks (text, tool_use, etc.).
Returns:
`list[dict[str, Any]]`:
List containing a single formatted message dictionary with role,
parts, and finish_reason. Returns the original response if it's
not a ChatResponse instance, or an error message format if
conversion fails.
"""
try:
if not isinstance(chat_response, ChatResponse):
return [
{
"role": "assistant",
"parts": [
{
"type": "text",
"content": str(chat_response),
},
],
"finish_reason": "unknown",
},
]
parts = []
finish_reason = "stop" # Default finish reason
for block in chat_response.content:
part = _convert_block_to_part(block)
if part:
parts.append(part)
output_message = {
"role": "assistant",
"parts": parts,
"finish_reason": finish_reason,
}
return [output_message]
except Exception:
return [
{
"role": "assistant",
"parts": [
{
"type": "text",
"content": "<error processing response>",
},
],
"finish_reason": "error",
},
]
def _get_llm_response_attributes(
chat_response: ChatResponse | None,
) -> Dict[str, Any]:
"""Get LLM response attributes for OpenTelemetry tracing.
Extracts response metadata and formats into GenAI attributes.
Args:
chat_response (`ChatResponse | None`):
Chat response object with data and usage info. Should have
attributes like id, usage (with input_tokens and output_tokens),
and content blocks.
Returns:
`Dict[str, Any]`:
OpenTelemetry GenAI response attributes including response ID,
finish reasons, token usage (input/output tokens), and output
messages.
"""
attributes = {
SpanAttributes.GEN_AI_RESPONSE_ID: getattr(
chat_response,
"id",
"unknown_id",
),
# FIXME: finish reason should be capture in chat response
SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS: '["stop"]',
}
if hasattr(chat_response, "usage") and chat_response.usage:
usage = chat_response.usage
attributes[
SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS
] = usage.input_tokens
attributes[
SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS
] = usage.output_tokens
cache_input = usage.cache_input_tokens
if cache_input:
attributes[
SpanAttributes.AGENTSCOPE_CACHE_INPUT_TOKENS
] = cache_input
cache_creation = usage.cache_creation_input_tokens
if cache_creation:
attributes[
SpanAttributes.AGENTSCOPE_CACHE_CREATION_INPUT_TOKENS
] = cache_creation
output_messages = _get_llm_output_messages(chat_response)
if output_messages:
attributes[SpanAttributes.GEN_AI_OUTPUT_MESSAGES] = _serialize_to_str(
output_messages,
)
return attributes
def _get_agent_messages(
msg: Msg | list[Msg],
) -> list[dict[str, Any]]:
"""Convert AgentScope message(s) to standardized parts format.
Transforms Msg objects into OpenTelemetry GenAI format.
Args:
msg (`Msg | list[Msg]`):
AgentScope message object or list of message objects with
content blocks.
Returns:
`list[dict[str, Any]]`:
List of formatted message dictionaries with role, parts, name,
and finish_reason.
"""
try:
if isinstance(msg, Msg):
msg = [msg]
formatted_msgs = []
for m in msg:
parts = []
for block in m.get_content_blocks():
part = _convert_block_to_part(block)
if part:
parts.append(part)
formatted_msg = {
"role": m.role,
"parts": parts,
"name": m.name,
"finish_reason": "stop",
}
formatted_msgs.append(formatted_msg)
return formatted_msgs
except Exception:
# Fallback: try simple attribute access on the original object.
# If msg was already converted to a list or lacks role/name, return
# an empty list rather than raising a secondary exception.
try:
single = msg[0] if isinstance(msg, list) else msg
return [
{
"role": single.role,
"parts": [
{
"type": "text",
"content": (
str(single.content) if single.content else ""
),
},
],
"name": single.name,
"finish_reason": "stop",
},
]
except Exception:
return []
def _get_agent_request_attributes(
instance: "Agent",
kwargs: Dict[str, Any],
) -> Dict[str, str]:
"""Get agent request attributes for OpenTelemetry tracing.
Extracts agent metadata and input data into GenAI attributes.
Args:
instance (`Agent`):
The agent instance making the request.
kwargs (`Dict[str, Any]`):
Keyword arguments passed to the agent's reply method.
Returns:
`Dict[str, str]`:
OpenTelemetry GenAI attributes including operation name, agent ID,
agent name, agent description, and input messages (if provided).
"""
attributes = {
SpanAttributes.GEN_AI_OPERATION_NAME: (
OperationNameValues.INVOKE_AGENT
),
SpanAttributes.GEN_AI_AGENT_NAME: instance.name,
SpanAttributes.GEN_AI_AGENT_DESCRIPTION: inspect.getdoc(
instance.__class__,
)
or "No description available",
}
inputs = kwargs.get("inputs")
if inputs is not None:
if isinstance(inputs, (Msg, list)):
input_messages = _get_agent_messages(inputs)
attributes[
SpanAttributes.GEN_AI_INPUT_MESSAGES
] = _serialize_to_str(input_messages)
elif isinstance(inputs, UserConfirmResultEvent):
attributes[
SpanAttributes.AGENTSCOPE_INCOMING_EVENT_TYPE
] = "user_confirm_result"
elif isinstance(inputs, ExternalExecutionResultEvent):
attributes[
SpanAttributes.AGENTSCOPE_INCOMING_EVENT_TYPE
] = "external_execution_result"
return attributes
def _get_agent_span_name(attributes: Dict[str, str]) -> str:
"""Generate span name for agent operations.
Args:
attributes (`Dict[str, str]`):
Agent request attributes dictionary containing operation name and
agent name.
Returns:
`str`:
Formatted span name in the format "{operation} {agent_name}",
e.g., "invoke_agent MyAgent".
"""
return (
f"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} "
f"{attributes[SpanAttributes.GEN_AI_AGENT_NAME]}"
)
def _get_agent_response_attributes(
agent_response: Msg,
) -> Dict[str, str]:
"""Get agent response attributes for OpenTelemetry tracing.
Args:
agent_response (`Msg`):
Response message returned by agent, containing content blocks.
Returns:
`Dict[str, str]`:
OpenTelemetry GenAI response attributes including output messages.
"""
attributes = {
SpanAttributes.GEN_AI_OUTPUT_MESSAGES: _serialize_to_str(
_get_agent_messages(agent_response),
),
}
return attributes
def _get_tool_request_attributes(
instance: "Toolkit",
tool_call: ToolCallBlock,
) -> Dict[str, str]:
"""Get tool request attributes for OpenTelemetry tracing.
Extracts tool execution metadata into GenAI attributes.
Args:
instance (`Toolkit`):
Toolkit instance with tool definitions. Used to extract tool
description from the tool's JSON schema.
tool_call (`ToolCallBlock`):
Tool use block with call information including id, name, and input
arguments.
Returns:
`Dict[str, str]`:
OpenTelemetry GenAI tool attributes including operation name, tool
call ID, tool name, tool description (if available), and tool call
arguments.
"""
attributes = {
SpanAttributes.GEN_AI_OPERATION_NAME: (
OperationNameValues.EXECUTE_TOOL
),
}
if tool_call:
tool_name = tool_call.name
attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID] = tool_call.id
attributes[SpanAttributes.GEN_AI_TOOL_NAME] = tool_name
# tool_call.input is already a JSON string; pass it directly to avoid
# double-encoding (e.g. '{"city": "Beijing"}' → '"{\\"city\\"...}"')
attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS] = tool_call.input
if tool_name:
registered = getattr(instance, "tools", {}).get(tool_name)
if registered is not None:
tool_obj = getattr(registered, "tool", None)
description = getattr(tool_obj, "description", None)
if description:
attributes[
SpanAttributes.GEN_AI_TOOL_DESCRIPTION
] = description
return attributes
def _get_tool_span_name(attributes: Dict[str, str]) -> str:
"""Generate span name for tool operations.
Args:
attributes (`Dict[str, str]`):
Tool request attributes dictionary containing operation name and
tool name.
Returns:
`str`:
Formatted span name in the format "{operation} {tool_name}",
e.g., "execute_tool search".
"""
return (
f"{attributes[SpanAttributes.GEN_AI_OPERATION_NAME]} "
f"{attributes[SpanAttributes.GEN_AI_TOOL_NAME]}"
)
def _get_tool_response_attributes(
tool_response: Any,
) -> Dict[str, str]:
"""Get tool response attributes for OpenTelemetry tracing.
Args:
tool_response (`Any`):
Response object from tool execution. Can be any serializable object
returned by the tool function.
Returns:
`Dict[str, str]`:
OpenTelemetry GenAI response attributes including tool call result.
"""
attributes = {
SpanAttributes.GEN_AI_TOOL_CALL_RESULT: _serialize_to_str(
tool_response,
),
}
return attributes