19
19
from opentelemetry import trace as trace_api
20
20
from pydantic import BaseModel
21
21
22
+ from strands .tools .decorator import tool
23
+
22
24
from ..event_loop .event_loop import event_loop_cycle , run_tool
23
25
from ..handlers .callback_handler import PrintingCallbackHandler , null_callback_handler
24
26
from ..hooks import (
@@ -425,6 +427,18 @@ def execute() -> T:
425
427
future = executor .submit (execute )
426
428
return future .result ()
427
429
430
+ def _register_structured_output_tool (self , output_model : type [BaseModel ]):
431
+ @tool
432
+ def _structured_output (input : output_model ) -> output_model :
433
+ """If this tool is present it MUST be used to return structured data for the user."""
434
+ return input
435
+
436
+ return _structured_output
437
+
438
+ def _get_structured_output_tool (self , output_model : Type [T ]):
439
+ """Get or create the structured output tool for the given model."""
440
+ return self ._register_structured_output_tool (output_model )
441
+
428
442
async def structured_output_async (
429
443
self , output_model : Type [T ], prompt : Optional [Union [str , list [ContentBlock ]]] = None
430
444
) -> T :
@@ -445,48 +459,129 @@ async def structured_output_async(
445
459
ValueError: If no conversation history or prompt is provided.
446
460
"""
447
461
self .hooks .invoke_callbacks (BeforeInvocationEvent (agent = self ))
448
- with self .tracer .tracer .start_as_current_span (
449
- "execute_structured_output" , kind = trace_api .SpanKind .CLIENT
450
- ) as structured_output_span :
451
- try :
452
- if not self .messages and not prompt :
453
- raise ValueError ("No conversation history or prompt provided" )
454
- # Create temporary messages array if prompt is provided
455
- if prompt :
456
- content : list [ContentBlock ] = [{"text" : prompt }] if isinstance (prompt , str ) else prompt
457
- temp_messages = self .messages + [{"role" : "user" , "content" : content }]
458
- else :
459
- temp_messages = self .messages
460
-
461
- structured_output_span .set_attributes (
462
- {
463
- "gen_ai.system" : "strands-agents" ,
464
- "gen_ai.agent.name" : self .name ,
465
- "gen_ai.agent.id" : self .agent_id ,
466
- "gen_ai.operation.name" : "execute_structured_output" ,
467
- }
468
- )
469
- for message in temp_messages :
470
- structured_output_span .add_event (
471
- f"gen_ai.{ message ['role' ]} .message" ,
472
- attributes = {"role" : message ["role" ], "content" : serialize (message ["content" ])},
473
- )
474
- if self .system_prompt :
475
- structured_output_span .add_event (
476
- "gen_ai.system.message" ,
477
- attributes = {"role" : "system" , "content" : serialize ([{"text" : self .system_prompt }])},
462
+
463
+ # Save original state for restoration BEFORE making any changes
464
+ import copy
465
+
466
+ original_hooks_callbacks = copy .deepcopy (self .hooks ._registered_callbacks )
467
+ original_tool_registry = copy .deepcopy (self .tool_registry .registry )
468
+ original_dynamic_tools = copy .deepcopy (self .tool_registry .dynamic_tools )
469
+
470
+ # Create and add the structured output tool
471
+ structured_output_tool = self ._register_structured_output_tool (output_model )
472
+ self .tool_registry .register_tool (structured_output_tool )
473
+
474
+ # Variable to capture the structured result
475
+ captured_result = None
476
+
477
+ # Import here to avoid circular imports
478
+ from ..experimental .hooks import AfterToolInvocationEvent
479
+
480
+ # Hook to capture structured output tool invocation
481
+ def capture_structured_output_hook (event : AfterToolInvocationEvent ) -> AfterToolInvocationEvent :
482
+ nonlocal captured_result
483
+
484
+ if (
485
+ event .selected_tool
486
+ and hasattr (event .selected_tool , "tool_name" )
487
+ and event .selected_tool .tool_name == "_structured_output"
488
+ and event .result
489
+ and event .result .get ("status" ) == "success"
490
+ ):
491
+ # Parse the validated Pydantic model from the tool result
492
+ try :
493
+ content = event .result .get ("content" , [])
494
+ if content and isinstance (content [0 ], dict ) and "text" in content [0 ]:
495
+ # The tool returns the model instance as string, but we need the actual instance
496
+ # Since our tool returns the input directly, we can reconstruct it
497
+ tool_input = event .tool_use .get ("input" , {}).get ("input" )
498
+ if tool_input :
499
+ captured_result = output_model (** tool_input )
500
+ except Exception :
501
+ # Fallback: the tool should have returned the validated model
502
+ pass
503
+
504
+ return event
505
+
506
+ # Add the callback temporarily (use add_callback, not add_hook)
507
+ self .hooks .add_callback (AfterToolInvocationEvent , capture_structured_output_hook )
508
+
509
+ try :
510
+ with self .tracer .tracer .start_as_current_span (
511
+ "execute_structured_output" , kind = trace_api .SpanKind .CLIENT
512
+ ) as structured_output_span :
513
+ try :
514
+ if not self .messages and not prompt :
515
+ raise ValueError ("No conversation history or prompt provided" )
516
+
517
+ # Create temporary messages array if prompt is provided
518
+ if prompt :
519
+ content : list [ContentBlock ] = [{"text" : prompt }] if isinstance (prompt , str ) else prompt
520
+ message = {"role" : "user" , "content" : content }
521
+ else :
522
+ # Use existing conversation history
523
+ message = {
524
+ "role" : "user" ,
525
+ "content" : [
526
+ {
527
+ "text" : "Please provide the information from our conversation in the requested structured format."
528
+ }
529
+ ],
530
+ }
531
+
532
+ structured_output_span .set_attributes (
533
+ {
534
+ "gen_ai.system" : "strands-agents" ,
535
+ "gen_ai.agent.name" : self .name ,
536
+ "gen_ai.agent.id" : self .agent_id ,
537
+ "gen_ai.operation.name" : "execute_structured_output" ,
538
+ }
478
539
)
479
- events = self .model .structured_output (output_model , temp_messages , system_prompt = self .system_prompt )
480
- async for event in events :
481
- if "callback" in event :
482
- self .callback_handler (** cast (dict , event ["callback" ]))
483
- structured_output_span .add_event (
484
- "gen_ai.choice" , attributes = {"message" : serialize (event ["output" ].model_dump ())}
485
- )
486
- return event ["output" ]
487
-
488
- finally :
489
- self .hooks .invoke_callbacks (AfterInvocationEvent (agent = self ))
540
+
541
+ # Add tracing for messages
542
+ messages_to_trace = self .messages if not prompt else self .messages + [message ]
543
+ for msg in messages_to_trace :
544
+ structured_output_span .add_event (
545
+ f"gen_ai.{ msg ['role' ]} .message" ,
546
+ attributes = {"role" : msg ["role" ], "content" : serialize (msg ["content" ])},
547
+ )
548
+
549
+ if self .system_prompt :
550
+ structured_output_span .add_event (
551
+ "gen_ai.system.message" ,
552
+ attributes = {"role" : "system" , "content" : serialize ([{"text" : self .system_prompt }])},
553
+ )
554
+
555
+ invocation_state = {
556
+ "structured_output_mode" : True ,
557
+ "structured_output_model" : output_model ,
558
+ }
559
+
560
+ # Run the event loop
561
+ async for event in self ._run_loop (message = message , invocation_state = invocation_state ):
562
+ if "stop" in event :
563
+ break
564
+
565
+ # Return the captured structured result
566
+ if captured_result :
567
+ structured_output_span .add_event (
568
+ "gen_ai.choice" , attributes = {"message" : serialize (captured_result .model_dump ())}
569
+ )
570
+ return captured_result
571
+ else :
572
+ raise ValueError ("Failed to capture structured output from agent" )
573
+
574
+ except Exception as e :
575
+ structured_output_span .record_exception (e )
576
+ raise
577
+
578
+ finally :
579
+ # Restore original state
580
+ self .hooks ._registered_callbacks = original_hooks_callbacks
581
+ self .tool_registry .registry = original_tool_registry
582
+ self .tool_registry .dynamic_tools = original_dynamic_tools
583
+
584
+ self .hooks .invoke_callbacks (AfterInvocationEvent (agent = self ))
490
585
491
586
async def stream_async (self , prompt : Union [str , list [ContentBlock ]], ** kwargs : Any ) -> AsyncIterator [Any ]:
492
587
"""Process a natural language prompt and yield events as an async iterator.
0 commit comments