openai/chatkit-python

Public

mirrored from https://github.com/openai/chatkit-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dbf253878fc04bd1f3a005306868c4c47efee65b

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/test_agents.py

1721lines · modecode

1import asyncio
2import json
3from collections.abc import AsyncIterator
4from datetime import datetime
5from typing import cast
6from unittest.mock import AsyncMock, Mock
7
8import pytest
9from agents import (
10 Agent,
11 GuardrailFunctionOutput,
12 InputGuardrail,
13 InputGuardrailResult,
14 InputGuardrailTripwireTriggered,
15 OutputGuardrail,
16 OutputGuardrailResult,
17 OutputGuardrailTripwireTriggered,
18 RawResponsesStreamEvent,
19 RunContextWrapper,
20 RunItemStreamEvent,
21 Runner,
22 RunResultStreaming,
23 StreamEvent,
24 ToolCallItem,
25)
26from agents._run_impl import QueueCompleteSentinel
27from openai.types.responses import (
28 EasyInputMessageParam,
29 ResponseFileSearchToolCall,
30 ResponseInputContentParam,
31 ResponseInputTextParam,
32 ResponseOutputItemAddedEvent,
33 ResponseOutputItemDoneEvent,
34 ResponseOutputMessage,
35 ResponseReasoningItem,
36)
37from openai.types.responses.response_content_part_added_event import (
38 ResponseContentPartAddedEvent,
39)
40from openai.types.responses.response_file_search_tool_call import Result
41from openai.types.responses.response_input_item_param import Message
42from openai.types.responses.response_output_text import (
43 AnnotationContainerFileCitation as ResponsesAnnotationContainerFileCitation,
44)
45from openai.types.responses.response_output_text import (
46 AnnotationFileCitation as ResponsesAnnotationFileCitation,
47)
48from openai.types.responses.response_output_text import (
49 AnnotationFilePath as ResponsesAnnotationFilePath,
50)
51from openai.types.responses.response_output_text import (
52 AnnotationURLCitation as ResponsesAnnotationURLCitation,
53)
54from openai.types.responses.response_output_text import (
55 ResponseOutputText,
56)
57from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent
58from openai.types.responses.response_text_done_event import ResponseTextDoneEvent
59
60from chatkit.agents import (
61 AgentContext,
62 ResponseStreamConverter,
63 ThreadItemConverter,
64 accumulate_text,
65 simple_to_agent_input,
66 stream_agent_response,
67)
68from chatkit.types import (
69 Annotation,
70 AssistantMessageContent,
71 AssistantMessageContentPartAdded,
72 AssistantMessageContentPartAnnotationAdded,
73 AssistantMessageContentPartDone,
74 AssistantMessageContentPartTextDelta,
75 AssistantMessageItem,
76 Attachment,
77 ClientToolCallItem,
78 CustomSummary,
79 CustomTask,
80 DurationSummary,
81 FileSource,
82 GeneratedImage,
83 GeneratedImageItem,
84 GeneratedImageUpdated,
85 HiddenContextItem,
86 InferenceOptions,
87 Page,
88 TaskItem,
89 ThoughtTask,
90 Thread,
91 ThreadItemAddedEvent,
92 ThreadItemDoneEvent,
93 ThreadItemUpdatedEvent,
94 ThreadStreamEvent,
95 URLSource,
96 UserMessageItem,
97 UserMessageTagContent,
98 UserMessageTextContent,
99 WidgetItem,
100 Workflow,
101 WorkflowItem,
102 WorkflowTaskAdded,
103 WorkflowTaskUpdated,
104)
105from chatkit.widgets import Card, Text
106
107thread = Thread(id="123", title="Test", created_at=datetime.now(), items=Page())
108
109mock_store = Mock()
110mock_store.generate_item_id = lambda item_type, thread, context: f"{item_type}_id"
111mock_store.load_thread_items = AsyncMock(return_value=Page())
112mock_store.add_thread_item = AsyncMock()
113
114
115class RunResult(RunResultStreaming):
116 def add_event(self, event: StreamEvent):
117 self._event_queue.put_nowait(event)
118
119 def done(self):
120 self.is_complete = True
121 self._event_queue.put_nowait(QueueCompleteSentinel())
122
123 def throw_input_guardrails(self):
124 self._stored_exception = InputGuardrailTripwireTriggered(
125 InputGuardrailResult(
126 guardrail=Mock(spec=InputGuardrail),
127 output=GuardrailFunctionOutput(
128 output_info=None,
129 tripwire_triggered=True,
130 ),
131 )
132 )
133 self.is_complete = True
134 self._event_queue.put_nowait(QueueCompleteSentinel())
135
136 def throw_output_guardrails(self):
137 self._stored_exception = OutputGuardrailTripwireTriggered(
138 OutputGuardrailResult(
139 guardrail=Mock(spec=OutputGuardrail),
140 output=GuardrailFunctionOutput(
141 output_info=None,
142 tripwire_triggered=True,
143 ),
144 agent=Mock(spec=Agent),
145 agent_output=None,
146 )
147 )
148 self.is_complete = True
149 self._event_queue.put_nowait(QueueCompleteSentinel())
150
151
152def make_result() -> RunResult:
153 return RunResult(
154 context_wrapper=Mock(spec=RunContextWrapper),
155 input=[],
156 tool_input_guardrail_results=[],
157 tool_output_guardrail_results=[],
158 new_items=[],
159 raw_responses=[],
160 final_output=None,
161 current_agent=Agent(name="test"),
162 current_turn=0,
163 max_turns=10,
164 _current_agent_output_schema=None,
165 trace=None,
166 is_complete=False,
167 _event_queue=asyncio.Queue(),
168 _input_guardrail_queue=asyncio.Queue(),
169 _output_guardrails_task=None,
170 _run_impl_task=None,
171 _stored_exception=None,
172 output_guardrail_results=[],
173 input_guardrail_results=[],
174 )
175
176
177async def all_events(
178 events: AsyncIterator[ThreadStreamEvent],
179) -> list[ThreadStreamEvent]:
180 return [event async for event in events]
181
182
183async def test_returns_widget_item():
184 context = AgentContext(
185 previous_response_id=None, thread=thread, store=mock_store, request_context=None
186 )
187 result = make_result()
188 result.add_event(
189 RunItemStreamEvent(name="tool_called", item=Mock(spec=ToolCallItem))
190 )
191 await context.stream_widget(Card(children=[Text(value="Hello, world!")]))
192 result.done()
193
194 events = await all_events(
195 stream_agent_response(
196 context=context,
197 result=result,
198 )
199 )
200
201 assert len(events) == 1
202 assert isinstance(events[0], ThreadItemDoneEvent)
203 assert isinstance(events[0].item, WidgetItem)
204 assert events[0].item.widget == Card(children=[Text(value="Hello, world!")])
205
206
207async def test_returns_widget_item_generator():
208 context = AgentContext(
209 previous_response_id=None, thread=thread, store=mock_store, request_context=None
210 )
211 result = make_result()
212 result.add_event(
213 RunItemStreamEvent(name="tool_called", item=Mock(spec=ToolCallItem))
214 )
215
216 def render_widget(i: int) -> Card:
217 return Card(children=[Text(id="text", value="Hello, world"[:i])])
218
219 async def widget_generator():
220 yield render_widget(0)
221 yield render_widget(12)
222
223 await context.stream_widget(widget_generator())
224 result.done()
225
226 events = await all_events(
227 stream_agent_response(
228 context=context,
229 result=result,
230 )
231 )
232
233 assert len(events) == 3
234 assert isinstance(events[0], ThreadItemAddedEvent)
235 assert isinstance(events[0].item, WidgetItem)
236 assert events[0].item.widget == Card(children=[Text(id="text", value="")])
237
238 assert isinstance(events[1], ThreadItemUpdatedEvent)
239 assert events[1].update.type == "widget.streaming_text.value_delta"
240 assert events[1].update.component_id == "text"
241 assert events[1].update.delta == "Hello, world"
242
243 assert isinstance(events[2], ThreadItemDoneEvent)
244 assert isinstance(events[2].item, WidgetItem)
245 assert events[2].item.widget == Card(
246 children=[Text(id="text", value="Hello, world")]
247 )
248
249
250async def test_returns_widget_full_replace_generator():
251 context = AgentContext(
252 previous_response_id=None, thread=thread, store=mock_store, request_context=None
253 )
254 result = make_result()
255 result.add_event(
256 RunItemStreamEvent(name="tool_called", item=Mock(spec=ToolCallItem))
257 )
258
259 async def widget_generator():
260 yield Card(children=[Text(id="text", value="Hello!")])
261 yield Card(children=[Text(key="other text", value="World!", streaming=False)])
262
263 await context.stream_widget(widget_generator())
264 result.done()
265
266 events = await all_events(
267 stream_agent_response(
268 context=context,
269 result=result,
270 )
271 )
272
273 assert len(events) == 3
274 assert isinstance(events[0], ThreadItemAddedEvent)
275 assert isinstance(events[0].item, WidgetItem)
276 assert events[0].item.widget == Card(children=[Text(id="text", value="Hello!")])
277
278 assert isinstance(events[1], ThreadItemUpdatedEvent)
279 assert events[1].update.type == "widget.root.updated"
280 assert events[1].update.widget == Card(
281 children=[Text(key="other text", value="World!", streaming=False)]
282 )
283
284 assert isinstance(events[2], ThreadItemDoneEvent)
285 assert isinstance(events[2].item, WidgetItem)
286 assert events[2].item.widget == Card(
287 children=[Text(key="other text", value="World!", streaming=False)]
288 )
289
290
291async def test_accumulate_text():
292 def delta(text: str) -> RawResponsesStreamEvent:
293 return RawResponsesStreamEvent(
294 type="raw_response_event",
295 data=ResponseTextDeltaEvent(
296 type="response.output_text.delta",
297 delta=text,
298 content_index=0,
299 item_id="123",
300 logprobs=[],
301 output_index=0,
302 sequence_number=0,
303 ),
304 )
305
306 result = Runner.run_streamed(
307 Agent("Assistant", instructions="You are a helpful assistant."), "Say hello!"
308 )
309 result = make_result()
310 result.add_event(delta("Hello, "))
311 result.add_event(delta("world!"))
312
313 result.done()
314
315 events = [
316 event
317 async for event in accumulate_text(
318 result.stream_events(), Text(key="text", value="", streaming=True)
319 )
320 ]
321 assert events == [
322 Text(key="text", value="", streaming=True),
323 Text(key="text", value="Hello, ", streaming=True),
324 Text(key="text", value="Hello, world!", streaming=True),
325 Text(key="text", value="Hello, world!", streaming=False),
326 ]
327
328
329async def test_input_item_converter_quotes_last_user_message():
330 items = [
331 UserMessageItem(
332 id="123",
333 content=[UserMessageTextContent(text="Hello!")],
334 attachments=[],
335 inference_options=InferenceOptions(),
336 thread_id=thread.id,
337 quoted_text="Hi!",
338 created_at=datetime.now(),
339 ),
340 UserMessageItem(
341 id="123",
342 content=[UserMessageTextContent(text="I'm well, thank you!")],
343 attachments=[],
344 inference_options=InferenceOptions(),
345 thread_id=thread.id,
346 quoted_text="How are you doing?",
347 created_at=datetime.now(),
348 ),
349 ]
350
351 async def throw_exception(
352 _: Attachment,
353 ) -> ResponseInputContentParam:
354 raise Exception("Not implemented")
355
356 input_items = await simple_to_agent_input(items)
357 assert len(input_items) == 3
358 assert input_items[0] == {
359 "content": [
360 {
361 "text": "Hello!",
362 "type": "input_text",
363 },
364 ],
365 "role": "user",
366 "type": "message",
367 }
368 assert input_items[1] == {
369 "content": [
370 {
371 "text": "I'm well, thank you!",
372 "type": "input_text",
373 },
374 ],
375 "role": "user",
376 "type": "message",
377 }
378 assert input_items[2] == {
379 "content": [
380 {
381 "text": "The user is referring to this in particular: \nHow are you doing?",
382 "type": "input_text",
383 },
384 ],
385 "role": "user",
386 "type": "message",
387 }
388
389
390async def test_input_item_converter_to_input_items_mixed():
391 items = [
392 UserMessageItem(
393 id="123",
394 content=[UserMessageTextContent(text="Hello!")],
395 attachments=[],
396 inference_options=InferenceOptions(),
397 thread_id=thread.id,
398 quoted_text="Hi!",
399 created_at=datetime.now(),
400 ),
401 UserMessageItem(
402 id="123",
403 content=[UserMessageTextContent(text="I'm well, thank you!")],
404 attachments=[],
405 inference_options=InferenceOptions(),
406 thread_id=thread.id,
407 quoted_text="How are you doing?",
408 created_at=datetime.now(),
409 ),
410 AssistantMessageItem(
411 id="123",
412 content=[
413 AssistantMessageContent(text="How are you doing?"),
414 AssistantMessageContent(text="Can't do that"),
415 ],
416 thread_id=thread.id,
417 created_at=datetime.now(),
418 ),
419 WidgetItem(
420 id="wd_123",
421 widget=Card(children=[Text(value="Hello, world!")]),
422 thread_id=thread.id,
423 created_at=datetime.now(),
424 ),
425 ]
426
427 input_items = await simple_to_agent_input(items)
428 assert len(input_items) == 4
429 assert input_items[0] == {
430 "content": [
431 {
432 "text": "Hello!",
433 "type": "input_text",
434 },
435 ],
436 "role": "user",
437 "type": "message",
438 }
439 assert input_items[1] == {
440 "content": [
441 {
442 "text": "I'm well, thank you!",
443 "type": "input_text",
444 },
445 ],
446 "role": "user",
447 "type": "message",
448 }
449 assert input_items[2] == {
450 "content": [
451 {
452 "annotations": [],
453 "text": "How are you doing?",
454 "logprobs": None,
455 "type": "output_text",
456 },
457 {
458 "annotations": [],
459 "text": "Can't do that",
460 "logprobs": None,
461 "type": "output_text",
462 },
463 ],
464 "type": "message",
465 "role": "assistant",
466 }
467 assert "type" in input_items[3]
468 widget_item = cast(EasyInputMessageParam, input_items[3])
469 assert widget_item.get("type") == "message"
470 assert widget_item.get("role") == "user"
471 text = widget_item.get("content")[0]["text"] # type: ignore
472 assert (
473 "The following graphical UI widget (id: wd_123) was displayed to the user"
474 in text
475 )
476 assert "Hello, world!" in text
477 assert "created_at" not in text
478
479
480async def test_input_item_converter_user_input_with_tags():
481 class MyThreadItemConverter(ThreadItemConverter):
482 async def tag_to_message_content(self, tag):
483 return ResponseInputTextParam(
484 type="input_text", text=tag.text + " " + tag.data["key"]
485 )
486
487 items = [
488 UserMessageItem(
489 id="123",
490 content=[
491 UserMessageTagContent(
492 text="Hello!", type="input_tag", id="hello", data={"key": "value"}
493 )
494 ],
495 attachments=[],
496 inference_options=InferenceOptions(),
497 thread_id=thread.id,
498 created_at=datetime.now(),
499 )
500 ]
501 items = await MyThreadItemConverter().to_agent_input(items)
502
503 assert len(items) == 2
504 assert items[0] == {
505 "content": [
506 {
507 "text": "@Hello!",
508 "type": "input_text",
509 },
510 ],
511 "role": "user",
512 "type": "message",
513 }
514 assert items[1] == {
515 "content": [
516 {
517 "text": "# User-provided context for @-mentions\n- When referencing resolved entities, use their canonical names **without** '@'.\n"
518 + "- The '@' form appears only in user text and should not be echoed.",
519 "type": "input_text",
520 },
521 {
522 "text": "Hello! value",
523 "type": "input_text",
524 },
525 ],
526 "role": "user",
527 "type": "message",
528 }
529
530
531async def test_input_item_converter_user_input_with_tags_throws_by_default():
532 items = [
533 UserMessageItem(
534 id="123",
535 content=[
536 UserMessageTagContent(
537 text="Hello!", type="input_tag", id="hello", data={}
538 )
539 ],
540 attachments=[],
541 inference_options=InferenceOptions(),
542 thread_id=thread.id,
543 created_at=datetime.now(),
544 )
545 ]
546
547 with pytest.raises(NotImplementedError):
548 await simple_to_agent_input(items)
549
550
551async def test_input_item_converter_generated_image_item():
552 items = [
553 GeneratedImageItem(
554 id="img_item_1",
555 thread_id=thread.id,
556 created_at=datetime.now(),
557 image=GeneratedImage(id="img_1", url="https://example.com/img.png"),
558 )
559 ]
560
561 input_items = await simple_to_agent_input(items)
562 assert len(input_items) == 1
563
564 message = cast(dict, input_items[0])
565 assert message.get("type") == "message"
566 assert message.get("role") == "user"
567
568 content = cast(list, message.get("content"))
569 assert content[0].get("type") == "input_text"
570 assert content[0].get("text") == "The following image was generated by the agent."
571 assert content[1].get("type") == "input_image"
572 assert content[1].get("file_id") == "img_1"
573 assert content[1].get("image_url") == "https://example.com/img.png"
574 assert content[1].get("detail") == "auto"
575
576
577async def test_input_item_converter_generated_image_item_without_image():
578 items = [
579 GeneratedImageItem(
580 id="img_item_1",
581 thread_id=thread.id,
582 created_at=datetime.now(),
583 )
584 ]
585
586 input_items = await simple_to_agent_input(items)
587 assert input_items == []
588
589
590async def test_input_item_converter_for_hidden_context_with_string_content():
591 items = [
592 HiddenContextItem(
593 id="123",
594 content="User pressed the red button",
595 thread_id=thread.id,
596 created_at=datetime.now(),
597 )
598 ]
599
600 # The default converter works for string content
601 items = await simple_to_agent_input(items)
602 assert len(items) == 1
603 assert items[0] == {
604 "content": [
605 {
606 "text": "Hidden context for the agent (not shown to the user):\n<HiddenContext>\nUser pressed the red button\n</HiddenContext>",
607 "type": "input_text",
608 },
609 ],
610 "role": "user",
611 "type": "message",
612 }
613
614
615async def test_input_item_converter_for_hidden_context_with_non_string_content():
616 items = [
617 HiddenContextItem(
618 id="123",
619 content={"harry": "potter", "hermione": "granger"},
620 thread_id=thread.id,
621 created_at=datetime.now(),
622 )
623 ]
624
625 # Default converter should throw
626 with pytest.raises(NotImplementedError):
627 await simple_to_agent_input(items)
628
629 class MyThreadItemConverter(ThreadItemConverter):
630 async def hidden_context_to_input(self, item: HiddenContextItem):
631 content = ",".join(item.content.keys())
632 return Message(
633 type="message",
634 content=[
635 ResponseInputTextParam(
636 type="input_text", text=f"<HIDDEN>{content}</HIDDEN>"
637 )
638 ],
639 role="user",
640 )
641
642 items = await MyThreadItemConverter().to_agent_input(items)
643 assert len(items) == 1
644 assert items[0] == {
645 "content": [
646 {
647 "text": "<HIDDEN>harry,hermione</HIDDEN>",
648 "type": "input_text",
649 },
650 ],
651 "role": "user",
652 "type": "message",
653 }
654
655
656async def test_input_item_converter_with_client_tool_call():
657 items = [
658 UserMessageItem(
659 id="123",
660 content=[UserMessageTextContent(text="Call a client tool call xyz")],
661 attachments=[],
662 inference_options=InferenceOptions(),
663 thread_id=thread.id,
664 quoted_text="Hi!",
665 created_at=datetime.now(),
666 ),
667 TaskItem(
668 id="tsk_123",
669 created_at=datetime.now(),
670 task=CustomTask(title="Called xyx"),
671 thread_id=thread.id,
672 ),
673 ClientToolCallItem(
674 id="ctc_123",
675 thread_id=thread.id,
676 created_at=datetime.now(),
677 name="xyz",
678 arguments={"foo": "bar"},
679 call_id="ctc_123",
680 ),
681 ClientToolCallItem(
682 id="ctc_123_done",
683 thread_id=thread.id,
684 created_at=datetime.now(),
685 name="xyz",
686 arguments={"foo": "bar"},
687 call_id="ctc_123",
688 status="completed",
689 output={"success": True},
690 ),
691 ]
692
693 input_items = await simple_to_agent_input(items)
694 assert len(input_items) == 4
695 assert input_items[0] == {
696 "content": [
697 {
698 "text": "Call a client tool call xyz",
699 "type": "input_text",
700 },
701 ],
702 "role": "user",
703 "type": "message",
704 }
705 assert input_items[1] == {
706 "content": [
707 {
708 "text": "A message was displayed to the user that the following task was performed:\n<Task>\nCalled xyx\n</Task>",
709 "type": "input_text",
710 },
711 ],
712 "type": "message",
713 "role": "user",
714 }
715 assert input_items[2] == {
716 "type": "function_call",
717 "name": "xyz",
718 "arguments": json.dumps({"foo": "bar"}),
719 "call_id": "ctc_123",
720 }
721 assert input_items[3] == {
722 "type": "function_call_output",
723 "call_id": "ctc_123",
724 "output": json.dumps({"success": True}),
725 }
726
727
728async def test_stream_agent_response_yields_context_events_without_streaming_events():
729 context = AgentContext(
730 previous_response_id=None, thread=thread, store=mock_store, request_context=None
731 )
732 result = make_result()
733
734 event = ThreadItemAddedEvent(
735 item=WidgetItem(
736 id="123",
737 created_at=datetime.now(),
738 thread_id=thread.id,
739 widget=Card(children=[Text(id="text", value="Hello, world!")]),
740 ),
741 )
742
743 await context.stream(event)
744
745 response_streamer = stream_agent_response(context, result)
746 event = await response_streamer.__anext__()
747
748 assert event.type == "thread.item.added"
749
750 future = asyncio.ensure_future(response_streamer.__anext__())
751 assert future.done() is False
752
753 result.done()
754
755 try:
756 await future
757 assert False, "expected StopAsyncIteration"
758 except StopAsyncIteration:
759 pass
760
761 assert future.done() is True
762
763
764async def test_stream_agent_response_maps_events():
765 context = AgentContext(
766 previous_response_id=None, thread=thread, store=mock_store, request_context=None
767 )
768 result = make_result()
769
770 event = ThreadItemAddedEvent(
771 item=WidgetItem(
772 id="123",
773 created_at=datetime.now(),
774 thread_id=thread.id,
775 widget=Card(children=[Text(id="text", value="Hello, world!")]),
776 ),
777 )
778
779 await context.stream(event)
780 result.add_event(
781 RawResponsesStreamEvent(
782 type="raw_response_event",
783 data=ResponseTextDeltaEvent(
784 type="response.output_text.delta",
785 delta="Hello, world!",
786 content_index=0,
787 item_id="123",
788 logprobs=[],
789 output_index=0,
790 sequence_number=0,
791 ),
792 )
793 )
794
795 response_streamer = stream_agent_response(context, result)
796 event1 = await response_streamer.__anext__()
797 event2 = await response_streamer.__anext__()
798
799 assert {event1.type, event2.type} == {
800 "thread.item.added",
801 "thread.item.updated",
802 }
803
804 future = asyncio.ensure_future(response_streamer.__anext__())
805 assert future.done() is False
806
807 result.done()
808
809 try:
810 await future
811 assert False, "expected StopAsyncIteration"
812 except StopAsyncIteration:
813 pass
814
815 assert future.done() is True
816
817
818@pytest.mark.parametrize(
819 "raw_event,expected_event",
820 [
821 (
822 RawResponsesStreamEvent(
823 type="raw_response_event",
824 data=ResponseTextDeltaEvent(
825 type="response.output_text.delta",
826 delta="Hello, world!",
827 content_index=0,
828 item_id="123",
829 logprobs=[],
830 output_index=0,
831 sequence_number=0,
832 ),
833 ),
834 ThreadItemUpdatedEvent(
835 item_id="123",
836 update=AssistantMessageContentPartTextDelta(
837 content_index=0,
838 delta="Hello, world!",
839 ),
840 ),
841 ),
842 (
843 RawResponsesStreamEvent(
844 type="raw_response_event",
845 data=ResponseContentPartAddedEvent(
846 type="response.content_part.added",
847 part=ResponseOutputText(
848 type="output_text",
849 text="New content",
850 annotations=[],
851 ),
852 content_index=1,
853 item_id="123",
854 output_index=0,
855 sequence_number=1,
856 ),
857 ),
858 ThreadItemUpdatedEvent(
859 item_id="123",
860 update=AssistantMessageContentPartAdded(
861 content_index=1,
862 content=AssistantMessageContent(text="New content", annotations=[]),
863 ),
864 ),
865 ),
866 (
867 RawResponsesStreamEvent(
868 type="raw_response_event",
869 data=ResponseTextDoneEvent(
870 type="response.output_text.done",
871 text="Final text",
872 content_index=0,
873 item_id="123",
874 logprobs=[],
875 output_index=0,
876 sequence_number=2,
877 ),
878 ),
879 ThreadItemUpdatedEvent(
880 item_id="123",
881 update=AssistantMessageContentPartDone(
882 content_index=0,
883 content=AssistantMessageContent(
884 text="Final text",
885 annotations=[],
886 ),
887 ),
888 ),
889 ),
890 (
891 RawResponsesStreamEvent(
892 type="raw_response_event",
893 data=Mock(
894 type="response.output_text.annotation.added",
895 annotation=ResponsesAnnotationFileCitation(
896 type="file_citation",
897 file_id="file_123",
898 filename="file.txt",
899 index=5,
900 ),
901 content_index=0,
902 item_id="123",
903 annotation_index=0,
904 output_index=0,
905 sequence_number=3,
906 ),
907 ),
908 ThreadItemUpdatedEvent(
909 item_id="123",
910 update=AssistantMessageContentPartAnnotationAdded(
911 content_index=0,
912 annotation_index=0,
913 annotation=Annotation(
914 source=FileSource(filename="file.txt", title="file.txt"),
915 index=5,
916 ),
917 ),
918 ),
919 ),
920 ],
921)
922async def test_event_mapping(raw_event, expected_event):
923 context = AgentContext(
924 previous_response_id=None, thread=thread, store=mock_store, request_context=None
925 )
926 result = make_result()
927
928 result.add_event(raw_event)
929 result.done()
930
931 events = await all_events(stream_agent_response(context, result))
932 if expected_event:
933 assert events == [expected_event]
934 else:
935 assert events == []
936
937
938async def test_stream_agent_response_emits_annotation_added_events():
939 context = AgentContext(
940 previous_response_id=None, thread=thread, store=mock_store, request_context=None
941 )
942 result = make_result()
943 item_id = "item_123"
944
945 def add_annotation_event(annotation, sequence_number):
946 result.add_event(
947 RawResponsesStreamEvent(
948 type="raw_response_event",
949 data=Mock(
950 type="response.output_text.annotation.added",
951 annotation=annotation,
952 content_index=0,
953 item_id=item_id,
954 annotation_index=sequence_number,
955 output_index=0,
956 sequence_number=sequence_number,
957 ),
958 )
959 )
960
961 add_annotation_event(
962 ResponsesAnnotationFileCitation(
963 type="file_citation",
964 file_id="file_invalid",
965 filename="",
966 index=0,
967 ),
968 sequence_number=0,
969 )
970 add_annotation_event(
971 ResponsesAnnotationContainerFileCitation(
972 type="container_file_citation",
973 container_id="container_1",
974 file_id="file_123",
975 filename="container.txt",
976 start_index=0,
977 end_index=3,
978 ),
979 sequence_number=1,
980 )
981 add_annotation_event(
982 ResponsesAnnotationURLCitation(
983 type="url_citation",
984 url="https://example.com",
985 title="Example",
986 start_index=1,
987 end_index=5,
988 ),
989 sequence_number=2,
990 )
991 result.done()
992
993 events = await all_events(stream_agent_response(context, result))
994 assert events == [
995 ThreadItemUpdatedEvent(
996 item_id=item_id,
997 update=AssistantMessageContentPartAnnotationAdded(
998 content_index=0,
999 annotation_index=0,
1000 annotation=Annotation(
1001 source=FileSource(filename="container.txt", title="container.txt"),
1002 index=3,
1003 ),
1004 ),
1005 ),
1006 ThreadItemUpdatedEvent(
1007 item_id=item_id,
1008 update=AssistantMessageContentPartAnnotationAdded(
1009 content_index=0,
1010 annotation_index=1,
1011 annotation=Annotation(
1012 source=URLSource(
1013 url="https://example.com",
1014 title="Example",
1015 ),
1016 index=5,
1017 ),
1018 ),
1019 ),
1020 ]
1021
1022
1023@pytest.mark.parametrize("throw_guardrail", ["input", "output"])
1024async def test_stream_agent_response_yields_item_removed_event(throw_guardrail):
1025 context = AgentContext(
1026 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1027 )
1028 result = make_result()
1029 result.add_event(
1030 RawResponsesStreamEvent(
1031 type="raw_response_event",
1032 data=ResponseOutputItemAddedEvent(
1033 type="response.output_item.added",
1034 item=ResponseOutputMessage(
1035 id="1",
1036 content=[
1037 ResponseOutputText(
1038 annotations=[], type="output_text", text="Hello, world!"
1039 )
1040 ],
1041 role="assistant",
1042 status="completed",
1043 type="message",
1044 ),
1045 output_index=0,
1046 sequence_number=0,
1047 ),
1048 )
1049 )
1050 await context.stream(
1051 ThreadItemAddedEvent(
1052 item=AssistantMessageItem(
1053 id="2",
1054 content=[AssistantMessageContent(text="Hello, world!")],
1055 thread_id=thread.id,
1056 created_at=datetime.now(),
1057 ),
1058 )
1059 )
1060
1061 await context.stream(
1062 ThreadItemDoneEvent(
1063 item=WidgetItem(
1064 id="3",
1065 created_at=datetime.now(),
1066 thread_id=thread.id,
1067 widget=Card(children=[Text(id="text", value="Hello, world!")]),
1068 ),
1069 )
1070 )
1071
1072 iterator = stream_agent_response(context, result)
1073
1074 n = 3
1075 events = []
1076 # Grab first 3 events to
1077 async for event in iterator:
1078 n -= 1
1079 events.append(event)
1080 if n == 0:
1081 break
1082
1083 if throw_guardrail == "input":
1084 result.throw_input_guardrails()
1085 else:
1086 result.throw_output_guardrails()
1087
1088 try:
1089 async for event in iterator:
1090 events.append(event)
1091 assert False, "Guardrail should have been thrown from stream_agent_response"
1092 except (InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered):
1093 pass
1094 except Exception as e:
1095 assert False, f"Unexpected exception: {e}"
1096
1097 deleted_item_ids = {
1098 event.item_id for event in events if event.type == "thread.item.removed"
1099 }
1100 assert deleted_item_ids == {"1", "2", "3"}
1101
1102
1103async def test_stream_agent_response_assistant_message_content_types():
1104 AgentContext(
1105 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1106 )
1107 result = make_result()
1108
1109 result.add_event(
1110 RawResponsesStreamEvent(
1111 type="raw_response_event",
1112 data=ResponseOutputItemDoneEvent(
1113 type="response.output_item.done",
1114 item=ResponseFileSearchToolCall(
1115 id="fs_0",
1116 queries=["Hello, world!"],
1117 status="completed",
1118 type="file_search_call",
1119 results=[
1120 Result(
1121 file_id="f_123",
1122 filename="test.txt",
1123 text="Hello, world!",
1124 score=1.0,
1125 ),
1126 Result(
1127 file_id="f_123",
1128 filename="test.txt",
1129 text="Hello, friends!",
1130 score=0.5,
1131 ),
1132 ],
1133 ),
1134 output_index=0,
1135 sequence_number=0,
1136 ),
1137 )
1138 )
1139 result.add_event(
1140 RawResponsesStreamEvent(
1141 type="raw_response_event",
1142 data=ResponseOutputItemDoneEvent(
1143 type="response.output_item.done",
1144 item=ResponseOutputMessage(
1145 id="1",
1146 content=[
1147 ResponseOutputText(
1148 annotations=[
1149 ResponsesAnnotationFileCitation(
1150 type="file_citation",
1151 file_id="f_123",
1152 index=0,
1153 filename="test.txt",
1154 ),
1155 ResponsesAnnotationContainerFileCitation(
1156 type="container_file_citation",
1157 container_id="container_1",
1158 file_id="f_456",
1159 filename="container.txt",
1160 start_index=0,
1161 end_index=3,
1162 ),
1163 ResponsesAnnotationURLCitation(
1164 type="url_citation",
1165 url="https://www.google.com",
1166 title="Google",
1167 start_index=0,
1168 end_index=10,
1169 ),
1170 ResponsesAnnotationFilePath(
1171 type="file_path",
1172 file_id="123",
1173 index=0,
1174 ),
1175 ],
1176 text="Hello, world!",
1177 type="output_text",
1178 ),
1179 ResponseOutputText(
1180 annotations=[],
1181 text="Can't do that",
1182 type="output_text",
1183 ),
1184 ],
1185 role="assistant",
1186 status="completed",
1187 type="message",
1188 ),
1189 output_index=0,
1190 sequence_number=0,
1191 ),
1192 )
1193 )
1194
1195 result.done()
1196
1197 context = AgentContext(
1198 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1199 )
1200 events = await all_events(stream_agent_response(context, result))
1201 assert len(events) == 1
1202 assert isinstance(events[0], ThreadItemDoneEvent)
1203 message = events[0].item
1204 assert isinstance(message, AssistantMessageItem)
1205 assert message.content == [
1206 AssistantMessageContent(
1207 annotations=[
1208 Annotation(
1209 source=FileSource(
1210 filename="test.txt",
1211 title="test.txt",
1212 ),
1213 index=0,
1214 ),
1215 Annotation(
1216 source=FileSource(
1217 filename="container.txt",
1218 title="container.txt",
1219 ),
1220 index=3,
1221 ),
1222 Annotation(
1223 source=URLSource(
1224 url="https://www.google.com",
1225 title="Google",
1226 ),
1227 index=10,
1228 ),
1229 ],
1230 text="Hello, world!",
1231 ),
1232 AssistantMessageContent(text="Can't do that", annotations=[]),
1233 ]
1234 assert message.id == "1"
1235
1236
1237async def test_stream_agent_response_image_generation_events():
1238 context = AgentContext(
1239 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1240 )
1241 result = make_result()
1242
1243 result.add_event(
1244 RawResponsesStreamEvent(
1245 type="raw_response_event",
1246 data=Mock(
1247 type="response.output_item.added",
1248 item=Mock(type="image_generation_call", id="img_call_1"),
1249 output_index=0,
1250 sequence_number=0,
1251 ),
1252 )
1253 )
1254 result.add_event(
1255 RawResponsesStreamEvent(
1256 type="raw_response_event",
1257 data=Mock(
1258 type="response.output_item.done",
1259 item=Mock(
1260 type="image_generation_call", id="img_call_1", result="dGVzdA=="
1261 ),
1262 output_index=0,
1263 sequence_number=1,
1264 ),
1265 )
1266 )
1267 result.done()
1268
1269 stream = stream_agent_response(context, result)
1270 event1 = await stream.__anext__()
1271 assert isinstance(event1, ThreadItemAddedEvent)
1272 assert isinstance(event1.item, GeneratedImageItem)
1273 assert event1.item.type == "generated_image"
1274 assert event1.item.id == "message_id"
1275 assert event1.item.image is None
1276
1277 event2 = await stream.__anext__()
1278 assert isinstance(event2, ThreadItemDoneEvent)
1279 assert isinstance(event2.item, GeneratedImageItem)
1280 assert event2.item.id == event1.item.id
1281 assert event2.item.image == GeneratedImage(
1282 id="img_call_1", url="data:image/png;base64,dGVzdA=="
1283 )
1284
1285 with pytest.raises(StopAsyncIteration):
1286 await stream.__anext__()
1287
1288
1289async def test_stream_agent_response_image_generation_events_with_custom_converter():
1290 context = AgentContext(
1291 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1292 )
1293 result = make_result()
1294
1295 result.add_event(
1296 RawResponsesStreamEvent(
1297 type="raw_response_event",
1298 data=Mock(
1299 type="response.output_item.added",
1300 item=Mock(type="image_generation_call", id="img_call_1"),
1301 output_index=0,
1302 sequence_number=0,
1303 ),
1304 )
1305 )
1306 result.add_event(
1307 RawResponsesStreamEvent(
1308 type="raw_response_event",
1309 data=Mock(
1310 type="response.output_item.done",
1311 item=Mock(
1312 type="image_generation_call", id="img_call_1", result="dGVzdA=="
1313 ),
1314 output_index=0,
1315 sequence_number=1,
1316 ),
1317 )
1318 )
1319 result.done()
1320
1321 class CustomResponseStreamConverter(ResponseStreamConverter):
1322 def __init__(self):
1323 super().__init__()
1324 self.calls: list[tuple[str, str, int | None]] = []
1325
1326 async def base64_image_to_url(
1327 self,
1328 image_id: str,
1329 base64_image: str,
1330 partial_image_index: int | None = None,
1331 ) -> str:
1332 self.calls.append((image_id, base64_image, partial_image_index))
1333 return f"https://example.com/{image_id}"
1334
1335 converter = CustomResponseStreamConverter()
1336 stream = stream_agent_response(context, result, converter=converter)
1337 event1 = await stream.__anext__()
1338 assert isinstance(event1, ThreadItemAddedEvent)
1339 assert isinstance(event1.item, GeneratedImageItem)
1340 assert event1.item.image is None
1341
1342 event2 = await stream.__anext__()
1343 assert isinstance(event2, ThreadItemDoneEvent)
1344 assert isinstance(event2.item, GeneratedImageItem)
1345 assert converter.calls == [("img_call_1", "dGVzdA==", None)]
1346 assert event2.item.image == GeneratedImage(
1347 id="img_call_1", url="https://example.com/img_call_1"
1348 )
1349 with pytest.raises(StopAsyncIteration):
1350 await stream.__anext__()
1351
1352
1353async def test_stream_agent_response_image_generation_partial_progress():
1354 context = AgentContext(
1355 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1356 )
1357 result = make_result()
1358
1359 result.add_event(
1360 RawResponsesStreamEvent(
1361 type="raw_response_event",
1362 data=Mock(
1363 type="response.output_item.added",
1364 item=Mock(type="image_generation_call", id="img_call_1"),
1365 output_index=0,
1366 sequence_number=0,
1367 ),
1368 )
1369 )
1370 result.add_event(
1371 RawResponsesStreamEvent(
1372 type="raw_response_event",
1373 data=Mock(
1374 type="response.image_generation_call.partial_image",
1375 partial_image_b64="dGVzdA==",
1376 partial_image_index=1,
1377 item_id="img_call_1",
1378 output_index=0,
1379 sequence_number=1,
1380 ),
1381 )
1382 )
1383 result.add_event(
1384 RawResponsesStreamEvent(
1385 type="raw_response_event",
1386 data=Mock(
1387 type="response.output_item.done",
1388 item=Mock(
1389 type="image_generation_call", id="img_call_1", result="dGVzdA=="
1390 ),
1391 output_index=0,
1392 sequence_number=2,
1393 ),
1394 )
1395 )
1396 result.done()
1397
1398 converter = ResponseStreamConverter(partial_images=3)
1399 events = await all_events(
1400 stream_agent_response(context, result, converter=converter)
1401 )
1402
1403 assert len(events) == 3
1404 added_event, partial_event, done_event = events
1405
1406 assert isinstance(added_event, ThreadItemAddedEvent)
1407 assert isinstance(added_event.item, GeneratedImageItem)
1408
1409 assert isinstance(partial_event, ThreadItemUpdatedEvent)
1410 assert isinstance(partial_event.update, GeneratedImageUpdated)
1411 assert partial_event.update.progress == pytest.approx(1 / 3)
1412 assert partial_event.update.image == GeneratedImage(
1413 id="img_call_1", url="data:image/png;base64,dGVzdA=="
1414 )
1415
1416 assert isinstance(done_event, ThreadItemDoneEvent)
1417 assert isinstance(done_event.item, GeneratedImageItem)
1418 assert done_event.item.image == GeneratedImage(
1419 id="img_call_1", url="data:image/png;base64,dGVzdA=="
1420 )
1421
1422
1423async def test_workflow_streams_first_thought():
1424 context = AgentContext(
1425 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1426 )
1427 result = make_result()
1428
1429 # first thought
1430 result.add_event(
1431 RawResponsesStreamEvent(
1432 type="raw_response_event",
1433 data=ResponseOutputItemAddedEvent(
1434 type="response.output_item.added",
1435 item=ResponseReasoningItem(
1436 id="resp_1",
1437 summary=[],
1438 type="reasoning",
1439 ),
1440 output_index=0,
1441 sequence_number=0,
1442 ),
1443 )
1444 )
1445 result.add_event(
1446 RawResponsesStreamEvent(
1447 type="raw_response_event",
1448 data=Mock(
1449 type="response.reasoning_summary_text.delta",
1450 item_id="resp_1",
1451 summary_index=0,
1452 delta="Think",
1453 ),
1454 )
1455 )
1456 result.add_event(
1457 RawResponsesStreamEvent(
1458 type="raw_response_event",
1459 data=Mock(
1460 type="response.reasoning_summary_text.delta",
1461 item_id="resp_1",
1462 summary_index=0,
1463 delta="ing 1",
1464 ),
1465 )
1466 )
1467 result.add_event(
1468 RawResponsesStreamEvent(
1469 type="raw_response_event",
1470 data=Mock(
1471 type="response.reasoning_summary_text.done",
1472 item_id="resp_1",
1473 summary_index=0,
1474 text="Thinking 1",
1475 ),
1476 )
1477 )
1478
1479 # second thought
1480 result.add_event(
1481 RawResponsesStreamEvent(
1482 type="raw_response_event",
1483 data=Mock(
1484 type="response.reasoning_summary_text.delta",
1485 item_id="resp_1",
1486 summary_index=1,
1487 delta="Think",
1488 ),
1489 )
1490 )
1491 result.add_event(
1492 RawResponsesStreamEvent(
1493 type="raw_response_event",
1494 data=Mock(
1495 type="response.reasoning_summary_text.delta",
1496 item_id="resp_1",
1497 summary_index=1,
1498 delta="ing 2",
1499 ),
1500 )
1501 )
1502 result.add_event(
1503 RawResponsesStreamEvent(
1504 type="raw_response_event",
1505 data=Mock(
1506 type="response.reasoning_summary_text.done",
1507 item_id="resp_1",
1508 summary_index=1,
1509 text="Thinking 2",
1510 ),
1511 )
1512 )
1513
1514 result.done()
1515 stream = stream_agent_response(context, result)
1516
1517 # Workflow added
1518 event = await anext(stream)
1519 assert isinstance(event, ThreadItemAddedEvent)
1520 assert context.workflow_item is not None
1521 assert context.workflow_item.workflow.type == "reasoning"
1522 assert len(context.workflow_item.workflow.tasks) == 0
1523 assert event == ThreadItemAddedEvent(item=context.workflow_item)
1524
1525 # First thought added
1526 event = await anext(stream)
1527 assert context.workflow_item is not None
1528 assert len(context.workflow_item.workflow.tasks) == 1
1529 assert isinstance(event, ThreadItemUpdatedEvent)
1530 assert event == ThreadItemUpdatedEvent(
1531 item_id=context.workflow_item.id,
1532 update=WorkflowTaskAdded(
1533 task=ThoughtTask(content="Think"),
1534 task_index=0,
1535 ),
1536 )
1537
1538 # First thought delta
1539 event = await anext(stream)
1540 assert context.workflow_item is not None
1541 assert len(context.workflow_item.workflow.tasks) == 1
1542 assert isinstance(event, ThreadItemUpdatedEvent)
1543 assert event == ThreadItemUpdatedEvent(
1544 item_id=context.workflow_item.id,
1545 update=WorkflowTaskUpdated(
1546 task=ThoughtTask(content="Thinking 1"),
1547 task_index=0,
1548 ),
1549 )
1550
1551 # First thought done
1552 event = await anext(stream)
1553 assert context.workflow_item is not None
1554 assert len(context.workflow_item.workflow.tasks) == 1
1555 assert isinstance(event, ThreadItemUpdatedEvent)
1556 assert event == ThreadItemUpdatedEvent(
1557 item_id=context.workflow_item.id,
1558 update=WorkflowTaskUpdated(
1559 task=ThoughtTask(content="Thinking 1"),
1560 task_index=0,
1561 ),
1562 )
1563
1564 # Second thought added (not streamed)
1565 event = await anext(stream)
1566 assert context.workflow_item is not None
1567 assert len(context.workflow_item.workflow.tasks) == 2
1568 assert isinstance(event, ThreadItemUpdatedEvent)
1569 assert event == ThreadItemUpdatedEvent(
1570 item_id=context.workflow_item.id,
1571 update=WorkflowTaskAdded(
1572 task=ThoughtTask(content="Thinking 2"),
1573 task_index=1,
1574 ),
1575 )
1576
1577 try:
1578 while True:
1579 await anext(stream)
1580 except StopAsyncIteration:
1581 pass
1582
1583
1584async def test_workflow_ends_on_message():
1585 context = AgentContext(
1586 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1587 )
1588 result = make_result()
1589
1590 # first thought
1591 result.add_event(
1592 RawResponsesStreamEvent(
1593 type="raw_response_event",
1594 data=ResponseOutputItemAddedEvent(
1595 type="response.output_item.added",
1596 item=ResponseReasoningItem(
1597 id="resp_1",
1598 summary=[],
1599 type="reasoning",
1600 ),
1601 output_index=0,
1602 sequence_number=0,
1603 ),
1604 )
1605 )
1606 result.add_event(
1607 RawResponsesStreamEvent(
1608 type="raw_response_event",
1609 data=Mock(
1610 type="response.reasoning_summary_text.done",
1611 item_id="resp_1",
1612 summary_index=0,
1613 text="Thinking 1",
1614 ),
1615 )
1616 )
1617
1618 # not reasoning
1619 result.add_event(
1620 RawResponsesStreamEvent(
1621 type="raw_response_event",
1622 data=ResponseOutputItemAddedEvent(
1623 type="response.output_item.added",
1624 item=ResponseOutputMessage(
1625 id="m_1",
1626 content=[],
1627 role="assistant",
1628 status="in_progress",
1629 type="message",
1630 ),
1631 output_index=0,
1632 sequence_number=0,
1633 ),
1634 )
1635 )
1636
1637 result.done()
1638 stream = stream_agent_response(context, result)
1639
1640 # Workflow added
1641 event = await anext(stream)
1642 assert isinstance(event, ThreadItemAddedEvent)
1643 assert context.workflow_item is not None
1644 assert context.workflow_item.workflow.type == "reasoning"
1645 assert len(context.workflow_item.workflow.tasks) == 0
1646 assert event == ThreadItemAddedEvent(item=context.workflow_item)
1647
1648 # First thought done
1649 event = await anext(stream)
1650 assert context.workflow_item is not None
1651 assert len(context.workflow_item.workflow.tasks) == 1
1652 assert isinstance(event, ThreadItemUpdatedEvent)
1653 assert event == ThreadItemUpdatedEvent(
1654 item_id=context.workflow_item.id,
1655 update=WorkflowTaskAdded(
1656 task=ThoughtTask(content="Thinking 1"),
1657 task_index=0,
1658 ),
1659 )
1660
1661 # Workflow ended
1662 event = await anext(stream)
1663 assert isinstance(event, ThreadItemDoneEvent)
1664 assert event.item.type == "workflow"
1665 assert context.workflow_item is None
1666 # Summary and expanded are handled by the end_workflow method
1667 assert isinstance(event.item.workflow.summary, DurationSummary)
1668 assert event.item.workflow.expanded is False
1669
1670 try:
1671 while True:
1672 await anext(stream)
1673 except StopAsyncIteration:
1674 pass
1675
1676
1677async def test_existing_workflow_summary_not_overwritten_on_automatic_end():
1678 context = AgentContext(
1679 previous_response_id=None, thread=thread, store=mock_store, request_context=None
1680 )
1681 result = make_result()
1682 context.workflow_item = WorkflowItem(
1683 id="wf_1",
1684 created_at=datetime.now(),
1685 workflow=Workflow(type="custom", tasks=[], summary=CustomSummary(title="Test")),
1686 thread_id=thread.id,
1687 )
1688
1689 result.add_event(
1690 RawResponsesStreamEvent(
1691 type="raw_response_event",
1692 data=ResponseOutputItemAddedEvent(
1693 type="response.output_item.added",
1694 item=ResponseOutputMessage(
1695 id="m_1",
1696 content=[],
1697 role="assistant",
1698 status="in_progress",
1699 type="message",
1700 ),
1701 output_index=0,
1702 sequence_number=0,
1703 ),
1704 )
1705 )
1706
1707 result.done()
1708 stream = stream_agent_response(context, result)
1709
1710 event = await anext(stream)
1711
1712 assert isinstance(event, ThreadItemDoneEvent)
1713 assert context.workflow_item is None
1714 assert event.item.type == "workflow"
1715 assert event.item.workflow.summary == CustomSummary(title="Test")
1716
1717 try:
1718 while True:
1719 await anext(stream)
1720 except StopAsyncIteration:
1721 pass
1722