|
80 | 80 | ) |
81 | 81 | from opentelemetry.util.genai.metrics import InvocationMetricsRecorder |
82 | 82 | from opentelemetry.util.genai.span_utils import ( |
| 83 | + _apply_agent_finish_attributes, |
| 84 | + _apply_creation_finish_attributes, |
83 | 85 | _apply_error_attributes, |
84 | 86 | _apply_llm_finish_attributes, |
85 | 87 | _maybe_emit_llm_event, |
86 | 88 | ) |
87 | | -from opentelemetry.util.genai.types import Error, LLMInvocation |
| 89 | +from opentelemetry.util.genai.types import ( |
| 90 | + AgentCreation, |
| 91 | + AgentInvocation, |
| 92 | + Error, |
| 93 | + LLMInvocation, |
| 94 | +) |
88 | 95 | from opentelemetry.util.genai.version import __version__ |
89 | 96 |
|
90 | 97 |
|
@@ -208,6 +215,143 @@ def llm( |
208 | 215 | raise |
209 | 216 | self.stop_llm(invocation) |
210 | 217 |
|
| 218 | + # ---- Agent invocation lifecycle ---- |
| 219 | + |
| 220 | + def start_agent( |
| 221 | + self, |
| 222 | + invocation: AgentInvocation, |
| 223 | + ) -> AgentInvocation: |
| 224 | + """Start an agent invocation and create a pending span entry.""" |
| 225 | + span_name = f"{invocation.operation_name} {invocation.agent_name}".strip() |
| 226 | + kind = SpanKind.CLIENT if invocation.is_remote else SpanKind.INTERNAL |
| 227 | + span = self._tracer.start_span( |
| 228 | + name=span_name, |
| 229 | + kind=kind, |
| 230 | + ) |
| 231 | + invocation.monotonic_start_s = timeit.default_timer() |
| 232 | + invocation.span = span |
| 233 | + invocation.context_token = otel_context.attach( |
| 234 | + set_span_in_context(span) |
| 235 | + ) |
| 236 | + return invocation |
| 237 | + |
| 238 | + def stop_agent(self, invocation: AgentInvocation) -> AgentInvocation: # pylint: disable=no-self-use |
| 239 | + """Finalize an agent invocation successfully and end its span.""" |
| 240 | + if invocation.context_token is None or invocation.span is None: |
| 241 | + return invocation |
| 242 | + |
| 243 | + span = invocation.span |
| 244 | + _apply_agent_finish_attributes(span, invocation) |
| 245 | + otel_context.detach(invocation.context_token) |
| 246 | + span.end() |
| 247 | + return invocation |
| 248 | + |
| 249 | + def fail_agent( # pylint: disable=no-self-use |
| 250 | + self, invocation: AgentInvocation, error: Error |
| 251 | + ) -> AgentInvocation: |
| 252 | + """Fail an agent invocation and end its span with error status.""" |
| 253 | + if invocation.context_token is None or invocation.span is None: |
| 254 | + return invocation |
| 255 | + |
| 256 | + span = invocation.span |
| 257 | + _apply_agent_finish_attributes(span, invocation) |
| 258 | + _apply_error_attributes(span, error) |
| 259 | + otel_context.detach(invocation.context_token) |
| 260 | + span.end() |
| 261 | + return invocation |
| 262 | + |
| 263 | + @contextmanager |
| 264 | + def agent( |
| 265 | + self, invocation: AgentInvocation | None = None |
| 266 | + ) -> Iterator[AgentInvocation]: |
| 267 | + """Context manager for agent invocations. |
| 268 | +
|
| 269 | + Only set data attributes on the invocation object, do not modify the span or context. |
| 270 | +
|
| 271 | + Starts the span on entry. On normal exit, finalizes the invocation and ends the span. |
| 272 | + If an exception occurs inside the context, marks the span as error, ends it, and |
| 273 | + re-raises the original exception. |
| 274 | + """ |
| 275 | + if invocation is None: |
| 276 | + invocation = AgentInvocation() |
| 277 | + self.start_agent(invocation) |
| 278 | + try: |
| 279 | + yield invocation |
| 280 | + except Exception as exc: |
| 281 | + self.fail_agent( |
| 282 | + invocation, Error(message=str(exc), type=type(exc)) |
| 283 | + ) |
| 284 | + raise |
| 285 | + self.stop_agent(invocation) |
| 286 | + |
| 287 | + # ---- Agent creation lifecycle ---- |
| 288 | + |
| 289 | + def start_create_agent( |
| 290 | + self, |
| 291 | + creation: AgentCreation, |
| 292 | + ) -> AgentCreation: |
| 293 | + """Start an agent creation and create a pending span entry.""" |
| 294 | + span_name = f"{creation.operation_name} {creation.agent_name}".strip() |
| 295 | + span = self._tracer.start_span( |
| 296 | + name=span_name, |
| 297 | + kind=SpanKind.CLIENT, |
| 298 | + ) |
| 299 | + creation.monotonic_start_s = timeit.default_timer() |
| 300 | + creation.span = span |
| 301 | + creation.context_token = otel_context.attach( |
| 302 | + set_span_in_context(span) |
| 303 | + ) |
| 304 | + return creation |
| 305 | + |
| 306 | + def stop_create_agent(self, creation: AgentCreation) -> AgentCreation: # pylint: disable=no-self-use |
| 307 | + """Finalize an agent creation successfully and end its span.""" |
| 308 | + if creation.context_token is None or creation.span is None: |
| 309 | + return creation |
| 310 | + |
| 311 | + span = creation.span |
| 312 | + _apply_creation_finish_attributes(span, creation) |
| 313 | + otel_context.detach(creation.context_token) |
| 314 | + span.end() |
| 315 | + return creation |
| 316 | + |
| 317 | + def fail_create_agent( # pylint: disable=no-self-use |
| 318 | + self, creation: AgentCreation, error: Error |
| 319 | + ) -> AgentCreation: |
| 320 | + """Fail an agent creation and end its span with error status.""" |
| 321 | + if creation.context_token is None or creation.span is None: |
| 322 | + return creation |
| 323 | + |
| 324 | + span = creation.span |
| 325 | + _apply_creation_finish_attributes(span, creation) |
| 326 | + _apply_error_attributes(span, error) |
| 327 | + otel_context.detach(creation.context_token) |
| 328 | + span.end() |
| 329 | + return creation |
| 330 | + |
| 331 | + @contextmanager |
| 332 | + def create_agent( |
| 333 | + self, creation: AgentCreation | None = None |
| 334 | + ) -> Iterator[AgentCreation]: |
| 335 | + """Context manager for agent creation. |
| 336 | +
|
| 337 | + Only set data attributes on the creation object, do not modify the span or context. |
| 338 | +
|
| 339 | + Starts the span on entry. On normal exit, finalizes the creation and ends the span. |
| 340 | + If an exception occurs inside the context, marks the span as error, ends it, and |
| 341 | + re-raises the original exception. |
| 342 | + """ |
| 343 | + if creation is None: |
| 344 | + creation = AgentCreation() |
| 345 | + self.start_create_agent(creation) |
| 346 | + try: |
| 347 | + yield creation |
| 348 | + except Exception as exc: |
| 349 | + self.fail_create_agent( |
| 350 | + creation, Error(message=str(exc), type=type(exc)) |
| 351 | + ) |
| 352 | + raise |
| 353 | + self.stop_create_agent(creation) |
| 354 | + |
211 | 355 |
|
212 | 356 | def get_telemetry_handler( |
213 | 357 | tracer_provider: TracerProvider | None = None, |
|
0 commit comments