utils.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import sentry_sdk
  2. from contextvars import ContextVar
  3. from sentry_sdk.consts import SPANDATA
  4. from sentry_sdk.scope import should_send_default_pii
  5. from sentry_sdk.tracing_utils import set_span_errored
  6. from sentry_sdk.utils import event_from_exception, safe_serialize
  7. from typing import TYPE_CHECKING
  8. if TYPE_CHECKING:
  9. from typing import Any, Optional
  10. # Store the current agent context in a contextvar for re-entrant safety
  11. # Using a list as a stack to support nested agent calls
  12. _agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar(
  13. "pydantic_ai_agent_context_stack", default=[]
  14. )
  15. def push_agent(agent: "Any", is_streaming: bool = False) -> None:
  16. """Push an agent context onto the stack along with its streaming flag."""
  17. stack = _agent_context_stack.get().copy()
  18. stack.append({"agent": agent, "is_streaming": is_streaming})
  19. _agent_context_stack.set(stack)
  20. def pop_agent() -> None:
  21. """Pop an agent context from the stack."""
  22. stack = _agent_context_stack.get().copy()
  23. if stack:
  24. stack.pop()
  25. _agent_context_stack.set(stack)
  26. def get_current_agent() -> "Any":
  27. """Get the current agent from the contextvar stack."""
  28. stack = _agent_context_stack.get()
  29. if stack:
  30. return stack[-1]["agent"]
  31. return None
  32. def get_is_streaming() -> bool:
  33. """Get the streaming flag from the contextvar stack."""
  34. stack = _agent_context_stack.get()
  35. if stack:
  36. return stack[-1].get("is_streaming", False)
  37. return False
  38. def _should_send_prompts() -> bool:
  39. """
  40. Check if prompts should be sent to Sentry.
  41. This checks both send_default_pii and the include_prompts integration setting.
  42. """
  43. if not should_send_default_pii():
  44. return False
  45. from . import PydanticAIIntegration
  46. # Get the integration instance from the client
  47. integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration)
  48. if integration is None:
  49. return False
  50. return getattr(integration, "include_prompts", False)
  51. def _set_agent_data(span: "sentry_sdk.tracing.Span", agent: "Any") -> None:
  52. """Set agent-related data on a span.
  53. Args:
  54. span: The span to set data on
  55. agent: Agent object (can be None, will try to get from contextvar if not provided)
  56. """
  57. # Extract agent name from agent object or contextvar
  58. agent_obj = agent
  59. if not agent_obj:
  60. # Try to get from contextvar
  61. agent_obj = get_current_agent()
  62. if agent_obj and hasattr(agent_obj, "name") and agent_obj.name:
  63. span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name)
  64. def _get_model_name(model_obj: "Any") -> "Optional[str]":
  65. """Extract model name from a model object.
  66. Args:
  67. model_obj: Model object to extract name from
  68. Returns:
  69. Model name string or None if not found
  70. """
  71. if not model_obj:
  72. return None
  73. if hasattr(model_obj, "model_name"):
  74. return model_obj.model_name
  75. elif hasattr(model_obj, "name"):
  76. try:
  77. return model_obj.name()
  78. except Exception:
  79. return str(model_obj)
  80. elif isinstance(model_obj, str):
  81. return model_obj
  82. else:
  83. return str(model_obj)
  84. def _set_model_data(
  85. span: "sentry_sdk.tracing.Span", model: "Any", model_settings: "Any"
  86. ) -> None:
  87. """Set model-related data on a span.
  88. Args:
  89. span: The span to set data on
  90. model: Model object (can be None, will try to get from agent if not provided)
  91. model_settings: Model settings (can be None, will try to get from agent if not provided)
  92. """
  93. # Try to get agent from contextvar if we need it
  94. agent_obj = get_current_agent()
  95. # Extract model information
  96. model_obj = model
  97. if not model_obj and agent_obj and hasattr(agent_obj, "model"):
  98. model_obj = agent_obj.model
  99. if model_obj:
  100. # Set system from model
  101. if hasattr(model_obj, "system"):
  102. span.set_data(SPANDATA.GEN_AI_SYSTEM, model_obj.system)
  103. # Set model name
  104. model_name = _get_model_name(model_obj)
  105. if model_name:
  106. span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name)
  107. # Extract model settings
  108. settings = model_settings
  109. if not settings and agent_obj and hasattr(agent_obj, "model_settings"):
  110. settings = agent_obj.model_settings
  111. if settings:
  112. settings_map = {
  113. "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS,
  114. "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE,
  115. "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P,
  116. "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY,
  117. "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY,
  118. }
  119. # ModelSettings is a TypedDict (dict at runtime), so use dict access
  120. if isinstance(settings, dict):
  121. for setting_name, spandata_key in settings_map.items():
  122. value = settings.get(setting_name)
  123. if value is not None:
  124. span.set_data(spandata_key, value)
  125. else:
  126. # Fallback for object-style settings
  127. for setting_name, spandata_key in settings_map.items():
  128. if hasattr(settings, setting_name):
  129. value = getattr(settings, setting_name)
  130. if value is not None:
  131. span.set_data(spandata_key, value)
  132. def _set_available_tools(span: "sentry_sdk.tracing.Span", agent: "Any") -> None:
  133. """Set available tools data on a span from an agent's function toolset.
  134. Args:
  135. span: The span to set data on
  136. agent: Agent object with _function_toolset attribute
  137. """
  138. if not agent or not hasattr(agent, "_function_toolset"):
  139. return
  140. try:
  141. tools = []
  142. # Get tools from the function toolset
  143. if hasattr(agent._function_toolset, "tools"):
  144. for tool_name, tool in agent._function_toolset.tools.items():
  145. tool_info = {"name": tool_name}
  146. # Add description from function_schema if available
  147. if hasattr(tool, "function_schema"):
  148. schema = tool.function_schema
  149. if getattr(schema, "description", None):
  150. tool_info["description"] = schema.description
  151. # Add parameters from json_schema
  152. if getattr(schema, "json_schema", None):
  153. tool_info["parameters"] = schema.json_schema
  154. tools.append(tool_info)
  155. if tools:
  156. span.set_data(
  157. SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
  158. )
  159. except Exception:
  160. # If we can't extract tools, just skip it
  161. pass
  162. def _capture_exception(exc: "Any", handled: bool = False) -> None:
  163. set_span_errored()
  164. event, hint = event_from_exception(
  165. exc,
  166. client_options=sentry_sdk.get_client().options,
  167. mechanism={"type": "pydantic_ai", "handled": handled},
  168. )
  169. sentry_sdk.capture_event(event, hint=hint)