utils.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import json
  2. import sentry_sdk
  3. from sentry_sdk.ai.utils import (
  4. GEN_AI_ALLOWED_MESSAGE_ROLES,
  5. normalize_message_roles,
  6. set_data_normalized,
  7. normalize_message_role,
  8. truncate_and_annotate_messages,
  9. )
  10. from sentry_sdk.consts import SPANDATA, SPANSTATUS, OP
  11. from sentry_sdk.integrations import DidNotEnable
  12. from sentry_sdk.scope import should_send_default_pii
  13. from sentry_sdk.tracing_utils import set_span_errored
  14. from sentry_sdk.utils import event_from_exception, safe_serialize
  15. from sentry_sdk.ai._openai_completions_api import _transform_system_instructions
  16. from sentry_sdk.ai._openai_responses_api import (
  17. _is_system_instruction,
  18. _get_system_instructions,
  19. )
  20. from typing import TYPE_CHECKING
  21. if TYPE_CHECKING:
  22. from typing import Any
  23. from agents import Usage, TResponseInputItem
  24. from sentry_sdk._types import TextPart
  25. try:
  26. import agents
  27. except ImportError:
  28. raise DidNotEnable("OpenAI Agents not installed")
  29. def _capture_exception(exc: "Any") -> None:
  30. set_span_errored()
  31. event, hint = event_from_exception(
  32. exc,
  33. client_options=sentry_sdk.get_client().options,
  34. mechanism={"type": "openai_agents", "handled": False},
  35. )
  36. sentry_sdk.capture_event(event, hint=hint)
  37. def _set_agent_data(span: "sentry_sdk.tracing.Span", agent: "agents.Agent") -> None:
  38. span.set_data(
  39. SPANDATA.GEN_AI_SYSTEM, "openai"
  40. ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why.
  41. span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent.name)
  42. if agent.model_settings.max_tokens:
  43. span.set_data(
  44. SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens
  45. )
  46. # Get model name from agent.model or fall back to request model (for when agent.model is None/default)
  47. model_name = None
  48. if agent.model:
  49. model_name = agent.model.model if hasattr(agent.model, "model") else agent.model
  50. elif hasattr(agent, "_sentry_request_model"):
  51. model_name = agent._sentry_request_model
  52. if model_name:
  53. span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name)
  54. if agent.model_settings.presence_penalty:
  55. span.set_data(
  56. SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY,
  57. agent.model_settings.presence_penalty,
  58. )
  59. if agent.model_settings.temperature:
  60. span.set_data(
  61. SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature
  62. )
  63. if agent.model_settings.top_p:
  64. span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p)
  65. if agent.model_settings.frequency_penalty:
  66. span.set_data(
  67. SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY,
  68. agent.model_settings.frequency_penalty,
  69. )
  70. if len(agent.tools) > 0:
  71. span.set_data(
  72. SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS,
  73. safe_serialize([vars(tool) for tool in agent.tools]),
  74. )
  75. def _set_usage_data(span: "sentry_sdk.tracing.Span", usage: "Usage") -> None:
  76. span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens)
  77. span.set_data(
  78. SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED,
  79. usage.input_tokens_details.cached_tokens,
  80. )
  81. span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens)
  82. span.set_data(
  83. SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING,
  84. usage.output_tokens_details.reasoning_tokens,
  85. )
  86. span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens)
  87. def _set_input_data(
  88. span: "sentry_sdk.tracing.Span", get_response_kwargs: "dict[str, Any]"
  89. ) -> None:
  90. if not should_send_default_pii():
  91. return
  92. request_messages = []
  93. messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", [])
  94. instructions_text_parts: "list[TextPart]" = []
  95. explicit_instructions = get_response_kwargs.get("system_instructions")
  96. if explicit_instructions is not None:
  97. instructions_text_parts.append(
  98. {
  99. "type": "text",
  100. "content": explicit_instructions,
  101. }
  102. )
  103. system_instructions = _get_system_instructions(messages)
  104. # Deliberate use of function accepting completions API type because
  105. # of shared structure FOR THIS PURPOSE ONLY.
  106. instructions_text_parts += _transform_system_instructions(system_instructions)
  107. if len(instructions_text_parts) > 0:
  108. span.set_data(
  109. SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
  110. json.dumps(instructions_text_parts),
  111. )
  112. non_system_messages = [
  113. message for message in messages if not _is_system_instruction(message)
  114. ]
  115. for message in non_system_messages:
  116. if "role" in message:
  117. normalized_role = normalize_message_role(message.get("role")) # type: ignore
  118. content = message.get("content") # type: ignore
  119. request_messages.append(
  120. {
  121. "role": normalized_role,
  122. "content": (
  123. [{"type": "text", "text": content}]
  124. if isinstance(content, str)
  125. else content
  126. ),
  127. }
  128. )
  129. else:
  130. if message.get("type") == "function_call": # type: ignore
  131. request_messages.append(
  132. {
  133. "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT,
  134. "content": [message],
  135. }
  136. )
  137. elif message.get("type") == "function_call_output": # type: ignore
  138. request_messages.append(
  139. {
  140. "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL,
  141. "content": [message],
  142. }
  143. )
  144. normalized_messages = normalize_message_roles(request_messages)
  145. scope = sentry_sdk.get_current_scope()
  146. messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
  147. if messages_data is not None:
  148. set_data_normalized(
  149. span,
  150. SPANDATA.GEN_AI_REQUEST_MESSAGES,
  151. messages_data,
  152. unpack=False,
  153. )
  154. def _set_output_data(span: "sentry_sdk.tracing.Span", result: "Any") -> None:
  155. if not should_send_default_pii():
  156. return
  157. output_messages: "dict[str, list[Any]]" = {
  158. "response": [],
  159. "tool": [],
  160. }
  161. for output in result.output:
  162. if output.type == "function_call":
  163. output_messages["tool"].append(output.dict())
  164. elif output.type == "message":
  165. for output_message in output.content:
  166. try:
  167. output_messages["response"].append(output_message.text)
  168. except AttributeError:
  169. # Unknown output message type, just return the json
  170. output_messages["response"].append(output_message.dict())
  171. if len(output_messages["tool"]) > 0:
  172. span.set_data(
  173. SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(output_messages["tool"])
  174. )
  175. if len(output_messages["response"]) > 0:
  176. set_data_normalized(
  177. span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"]
  178. )
  179. def _create_mcp_execute_tool_spans(
  180. span: "sentry_sdk.tracing.Span", result: "agents.Result"
  181. ) -> None:
  182. for output in result.output:
  183. if output.__class__.__name__ == "McpCall":
  184. with sentry_sdk.start_span(
  185. op=OP.GEN_AI_EXECUTE_TOOL,
  186. description=f"execute_tool {output.name}",
  187. start_timestamp=span.start_timestamp,
  188. ) as execute_tool_span:
  189. execute_tool_span.set_data(SPANDATA.GEN_AI_TOOL_NAME, output.name)
  190. if should_send_default_pii():
  191. execute_tool_span.set_data(
  192. SPANDATA.GEN_AI_TOOL_INPUT, output.arguments
  193. )
  194. execute_tool_span.set_data(
  195. SPANDATA.GEN_AI_TOOL_OUTPUT, output.output
  196. )
  197. if output.error:
  198. execute_tool_span.set_status(SPANSTATUS.INTERNAL_ERROR)