Unify flow streaming frame items

This commit is contained in:
lorenzejay
2026-06-29 14:50:17 -07:00
parent a48f45c917
commit 90b06a4523
15 changed files with 739 additions and 220 deletions

View File

@@ -9588,6 +9588,7 @@
"edge/pt-BR/learn/human-feedback-in-flows",
"edge/pt-BR/learn/kickoff-async",
"edge/pt-BR/learn/kickoff-for-each",
"edge/pt-BR/learn/streaming-runtime-contract",
"edge/pt-BR/learn/llm-connections",
"edge/pt-BR/learn/multimodal-agents",
"edge/pt-BR/learn/replay-tasks-from-latest-crew-kickoff",
@@ -18478,6 +18479,7 @@
"edge/ko/learn/human-feedback-in-flows",
"edge/ko/learn/kickoff-async",
"edge/ko/learn/kickoff-for-each",
"edge/ko/learn/streaming-runtime-contract",
"edge/ko/learn/llm-connections",
"edge/ko/learn/multimodal-agents",
"edge/ko/learn/replay-tasks-from-latest-crew-kickoff",
@@ -27584,6 +27586,7 @@
"edge/ar/learn/human-feedback-in-flows",
"edge/ar/learn/kickoff-async",
"edge/ar/learn/kickoff-for-each",
"edge/ar/learn/streaming-runtime-contract",
"edge/ar/learn/llm-connections",
"edge/ar/learn/multimodal-agents",
"edge/ar/learn/replay-tasks-from-latest-crew-kickoff",

View File

@@ -52,7 +52,7 @@ class ResearchFlow(Flow):
## البث المتزامن
عند استدعاء `kickoff()` على تدفق مع تفعيل البث، يُرجع كائن `FlowStreamingOutput` يمكنك التكرار عليه:
عند استدعاء `kickoff()` على تدفق مع تفعيل البث، يُرجع جلسة stream تنتج عناصر `StreamFrame` مرتبة:
```python Code
flow = ResearchFlow()
@@ -60,44 +60,43 @@ flow = ResearchFlow()
# Start streaming execution
streaming = flow.kickoff()
# Iterate over chunks as they arrive
for chunk in streaming:
print(chunk.content, end="", flush=True)
# Iterate over stream items as they arrive
for item in streaming:
print(item.content, end="", flush=True)
# Access the final result after streaming completes
result = streaming.result
print(f"\n\nFinal output: {result}")
```
### معلومات جزء البث
### معلومات عنصر البث
يوفر كل جزء سياقاً حول مصدره في التدفق:
يوفر كل عنصر محتوى قابلاً للطباعة وبيانات حدث مهيكلة:
```python Code
streaming = flow.kickoff()
for chunk in streaming:
print(f"Agent: {chunk.agent_role}")
print(f"Task: {chunk.task_name}")
print(f"Content: {chunk.content}")
print(f"Type: {chunk.chunk_type}") # TEXT or TOOL_CALL
for item in streaming:
print(f"Channel: {item.channel}")
print(f"Type: {item.type}")
print(f"Content: {item.content}")
print(f"Event payload: {item.event}")
```
### الوصول إلى خصائص البث
يوفر كائن `FlowStreamingOutput` خصائص وطرق مفيدة:
توفر جلسة stream خصائص وطرق مفيدة:
```python Code
streaming = flow.kickoff()
# Iterate and collect chunks
for chunk in streaming:
print(chunk.content, end="", flush=True)
# Iterate and collect items
for item in streaming:
print(item.content, end="", flush=True)
# After iteration completes
print(f"\nCompleted: {streaming.is_completed}")
print(f"Full text: {streaming.get_full_text()}")
print(f"Total chunks: {len(streaming.chunks)}")
print(f"Total frames: {len(streaming.frames)}")
print(f"Final result: {streaming.result}")
```
@@ -114,9 +113,9 @@ async def stream_flow():
# Start async streaming
streaming = await flow.kickoff_async()
# Async iteration over chunks
async for chunk in streaming:
print(chunk.content, end="", flush=True)
# Async iteration over stream items
async for item in streaming:
print(item.content, end="", flush=True)
# Access final result
result = streaming.result
@@ -422,7 +421,7 @@ except Exception as e:
## الإلغاء وتنظيف الموارد
يدعم `FlowStreamingOutput` الإلغاء السلس بحيث يتوقف العمل الجاري فوراً عند انقطاع اتصال المستهلك.
تدعم جلسة stream الإلغاء السلس بحيث يتوقف العمل الجاري فوراً عند انقطاع اتصال المستهلك.
### مدير السياق غير المتزامن
@@ -430,8 +429,8 @@ except Exception as e:
streaming = await flow.kickoff_async()
async with streaming:
async for chunk in streaming:
print(chunk.content, end="", flush=True)
async for item in streaming:
print(item.content, end="", flush=True)
```
### الإلغاء الصريح
@@ -439,8 +438,8 @@ async with streaming:
```python Code
streaming = await flow.kickoff_async()
try:
async for chunk in streaming:
print(chunk.content, end="", flush=True)
async for item in streaming:
print(item.content, end="", flush=True)
finally:
await streaming.aclose() # غير متزامن
# streaming.close() # المكافئ المتزامن
@@ -451,7 +450,7 @@ finally:
## ملاحظات مهمة
- يفعّل البث تلقائياً بث LLM لأي أطقم مستخدمة داخل التدفق
- يجب التكرار عبر جميع الأجزاء قبل الوصول إلى خاصية `.result`
- يجب التكرار عبر جميع عناصر stream قبل الوصول إلى خاصية `.result`
- يعمل البث مع كل من حالة التدفق المنظمة وغير المنظمة
- يلتقط بث التدفق المخرجات من جميع الأطقم واستدعاءات LLM في التدفق
- يتضمن كل جزء سياقاً حول الوكيل والمهمة التي ولدته

View File

@@ -0,0 +1,195 @@
---
title: عقد بث وقت التشغيل
description: بث إطارات وقت تشغيل مرتبة من التدفقات واستدعاءات LLM المباشرة ودورات المحادثة.
icon: tower-broadcast
mode: "wide"
---
## نظرة عامة
يوفر CrewAI عقد بث قائمًا على الإطارات للأنظمة التي تحتاج إلى أكثر من أجزاء نصية بسيطة. يصدر العقد كائنات `StreamFrame` مرتبة لأحداث دورة حياة Flow، وتوكنات LLM المباشرة، ونشاط الأدوات، ورسائل المحادثة، والأحداث المخصصة.
استخدم هذه الواجهة عندما تبني واجهة مستخدم، أو جسر خدمة، أو تطبيق طرفية، أو وقت تشغيل نشر يحتاج إلى تدفق ثابت من الأحداث المهيكلة أثناء تشغيل Flow أو دورة محادثة أو استدعاء LLM مباشر.
## StreamFrame
لكل إطار نفس الغلاف:
```python
from crewai.types.streaming import StreamFrame
frame.version # "v1"
frame.id # معرف إطار فريد
frame.seq # ترتيب محلي للتنفيذ، عند توفره
frame.type # نوع الحدث المصدر، مثل "flow_started"
frame.channel # "llm", "flow", "tools", "messages", "lifecycle", or "custom"
frame.namespace # نطاق المصدر/وقت التشغيل
frame.timestamp # طابع وقت الحدث
frame.parent_id # معرف الحدث الأب، عند توفره
frame.previous_id # معرف الحدث السابق، عند توفره
frame.data # حمولة الحدث
frame.event # اسم بديل لـ frame.data
frame.content # نص قابل للطباعة لإطارات التوكن، وإلا ""
```
حقل `channel` هو أسرع طريقة لتوجيه الإطارات في المستهلكين:
| القناة | تحتوي على |
|--------|-----------|
| `llm` | توكنات وأجزاء التفكير من أحداث بث LLM |
| `flow` | دورة حياة Flow، وتنفيذ الدوال، والتوجيه، وأحداث الإيقاف/الاستئناف |
| `tools` | أحداث استخدام الأدوات |
| `messages` | أحداث سجل المحادثة |
| `lifecycle` | أحداث دورة حياة وقت التشغيل التي لا تخص قناة أخرى |
| `custom` | أحداث لا تُطابق قناة مدمجة |
يحافظ `frame.type` على نوع الحدث المصدر، حتى يتمكن المستهلكون من التعامل مع أحداث محددة داخل القناة.
## بث Flow
عيّن `stream=True` على Flow لجعل `kickoff()` يعيد جلسة stream:
```python
from crewai.flow import Flow, start
class ReportFlow(Flow):
@start()
def generate(self):
return "done"
flow = ReportFlow(stream=True)
stream = flow.kickoff()
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
if chunk.type == "tool_usage_started":
print(chunk.event["tool_name"])
result = stream.result
```
يجب استهلاك stream قبل قراءة `stream.result`. يؤدي الوصول إلى النتيجة مبكرًا إلى رفع `RuntimeError` حتى لا يتعامل المستهلكون بالخطأ مع تشغيل جزئي على أنه مكتمل.
يمكنك أيضًا استدعاء `flow.stream_events(...)` مباشرة عندما تريد البث لاستدعاء واحد بدون تعيين `stream=True` على مثيل Flow.
## التصفية حسب القناة
يوفر `StreamSession` إسقاطات حسب القناة تحافظ على ترتيب الإطارات العالمي داخل القناة المحددة:
```python
stream = flow.stream_events()
with stream:
for frame in stream.llm:
print(frame.content, end="", flush=True)
result = stream.result
```
الإسقاطات المتاحة هي:
| الإسقاط | الإطارات |
|---------|----------|
| `stream.events` | كل الإطارات |
| `stream.llm` | إطارات LLM |
| `stream.messages` | إطارات رسائل المحادثة |
| `stream.flow` | إطارات Flow |
| `stream.tools` | إطارات الأدوات |
| `stream.interleave([...])` | مجموعة مختارة من القنوات |
استخدم `stream.interleave(["flow", "llm", "messages"])` عندما يريد المستهلك بعض القنوات فقط لكنه ما زال يحتاج إلى ترتيبها النسبي.
## البث غير المتزامن
استخدم `astream()` للمستهلكين غير المتزامنين:
```python
flow = ReportFlow()
stream = flow.astream()
async with stream:
async for chunk in stream.events:
print(chunk.channel, chunk.type, chunk.content)
result = stream.result
```
تملك الجلسة غير المتزامنة نفس إسقاطات الجلسة المتزامنة.
## بث استدعاء LLM مباشر
ما زال `llm.call(...)` يعيد النتيجة النهائية المجمعة. استخدم `llm.stream_events(...)` عندما تريد التكرار على الأجزاء فور وصولها مع الحفاظ على حمولة الحدث المهيكلة:
```python
from crewai import LLM
llm = LLM(model="gpt-4o-mini")
stream = llm.stream_events(
messages=[
{
"role": "user",
"content": "Explain CrewAI streaming in two short sentences.",
}
]
)
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
result = stream.result
```
يفعل `llm.stream_events(...)` البث مؤقتًا للاستدعاء المغلف ثم يستعيد إعداد `stream` السابق في LLM بعد ذلك. تستمر تكاملات المزودين في إصدار أحداث بث LLM الأساسية؛ يوفر هذا المساعد واجهة مكرر مشتركة فوق تلك الأحداث لكل مزودي LLM.
## دورات المحادثة
يمكن للتدفقات المحادثية بث دورة مستخدم واحدة باستخدام `stream_turn()`:
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
class ChatFlow(Flow[ConversationState]):
conversational = True
flow = ChatFlow()
stream = flow.stream_turn("What can you help me with?", session_id="session-1")
with stream:
for frame in stream.events:
if frame.channel == "llm" and frame.type == "llm_stream_chunk":
print(frame.content, end="", flush=True)
reply = stream.result
```
أثناء `stream_turn()`، يفعّل مسار الإجابة المحادثية المدمج بث توكنات LLM لذلك الدور ثم يستعيد إعداد `stream` السابق في LLM بعد ذلك. يجب على معالجات المسارات المخصصة التي تنشئ Agents أو مثيلات LLM خاصة بها تهيئة تلك النماذج للبث إذا احتاجت إلى إخراج على مستوى التوكن.
## التنظيف
استخدم الجلسة كمدير سياق عندما يكون ذلك ممكنًا. إذا انقطع اتصال العميل قبل استهلاك stream بالكامل، فأغلق الجلسة صراحة:
```python
stream = flow.stream_events()
try:
for frame in stream.events:
print(frame.type)
finally:
if not stream.is_exhausted:
stream.close()
```
للتدفقات غير المتزامنة، استخدم `await stream.aclose()`.
## بث الأجزاء القديم
ما زال بث Crew مع `stream=True` يعيد واجهة `CrewStreamingOutput` المعتمدة على الأجزاء والموضحة في [بث تنفيذ Crew](/ar/learn/streaming-crew-execution). وما زالت استدعاءات `llm.call(...)` المباشرة تعيد نتيجة LLM النهائية. عقد الإطارات مخصص لأوقات التشغيل التي تحتاج إلى غلاف حدث ثابت عبر Flows، واستدعاءات LLM المباشرة، ودورات المحادثة، والأدوات، والرسائل.

View File

@@ -52,7 +52,7 @@ class ResearchFlow(Flow):
## Synchronous Streaming
When you call `kickoff()` on a flow with streaming enabled, it returns a `FlowStreamingOutput` object that you can iterate over:
When you call `kickoff()` on a flow with streaming enabled, it returns a stream session that yields ordered `StreamFrame` items:
```python Code
flow = ResearchFlow()
@@ -60,44 +60,43 @@ flow = ResearchFlow()
# Start streaming execution
streaming = flow.kickoff()
# Iterate over chunks as they arrive
for chunk in streaming:
print(chunk.content, end="", flush=True)
# Iterate over stream items as they arrive
for item in streaming:
print(item.content, end="", flush=True)
# Access the final result after streaming completes
result = streaming.result
print(f"\n\nFinal output: {result}")
```
### Stream Chunk Information
### Stream Item Information
Each chunk provides context about where it originated in the flow:
Each item provides both printable content and structured event data:
```python Code
streaming = flow.kickoff()
for chunk in streaming:
print(f"Agent: {chunk.agent_role}")
print(f"Task: {chunk.task_name}")
print(f"Content: {chunk.content}")
print(f"Type: {chunk.chunk_type}") # TEXT or TOOL_CALL
for item in streaming:
print(f"Channel: {item.channel}")
print(f"Type: {item.type}")
print(f"Content: {item.content}")
print(f"Event payload: {item.event}")
```
### Accessing Streaming Properties
The `FlowStreamingOutput` object provides useful properties and methods:
The stream session provides useful properties and methods:
```python Code
streaming = flow.kickoff()
# Iterate and collect chunks
for chunk in streaming:
print(chunk.content, end="", flush=True)
# Iterate and collect items
for item in streaming:
print(item.content, end="", flush=True)
# After iteration completes
print(f"\nCompleted: {streaming.is_completed}")
print(f"Full text: {streaming.get_full_text()}")
print(f"Total chunks: {len(streaming.chunks)}")
print(f"Total frames: {len(streaming.frames)}")
print(f"Final result: {streaming.result}")
```
@@ -114,9 +113,9 @@ async def stream_flow():
# Start async streaming
streaming = await flow.kickoff_async()
# Async iteration over chunks
async for chunk in streaming:
print(chunk.content, end="", flush=True)
# Async iteration over stream items
async for item in streaming:
print(item.content, end="", flush=True)
# Access final result
result = streaming.result
@@ -422,7 +421,7 @@ except Exception as e:
## Cancellation and Resource Cleanup
`FlowStreamingOutput` supports graceful cancellation so that in-flight work stops promptly when the consumer disconnects.
The stream session supports graceful cancellation so that in-flight work stops promptly when the consumer disconnects.
### Async Context Manager
@@ -430,8 +429,8 @@ except Exception as e:
streaming = await flow.kickoff_async()
async with streaming:
async for chunk in streaming:
print(chunk.content, end="", flush=True)
async for item in streaming:
print(item.content, end="", flush=True)
```
### Explicit Cancellation
@@ -439,8 +438,8 @@ async with streaming:
```python Code
streaming = await flow.kickoff_async()
try:
async for chunk in streaming:
print(chunk.content, end="", flush=True)
async for item in streaming:
print(item.content, end="", flush=True)
finally:
await streaming.aclose() # async
# streaming.close() # sync equivalent
@@ -451,7 +450,7 @@ After cancellation, `streaming.is_cancelled` and `streaming.is_completed` are bo
## Important Notes
- Streaming automatically enables LLM streaming for any crews used within the flow
- You must iterate through all chunks before accessing the `.result` property
- You must iterate through all stream items before accessing the `.result` property
- Streaming works with both structured and unstructured flow state
- Flow streaming captures output from all crews and LLM calls in the flow
- Each chunk includes context about which agent and task generated it
@@ -475,4 +474,4 @@ result = streaming.result
print(f"\nFlow complete! View structure at: research_flow.html")
```
By leveraging flow streaming, you can build sophisticated, responsive applications that provide users with real-time visibility into complex multi-stage workflows, making your AI automations more transparent and engaging.
By leveraging flow streaming, you can build sophisticated, responsive applications that provide users with real-time visibility into complex multi-stage workflows, making your AI automations more transparent and engaging.

View File

@@ -28,6 +28,8 @@ frame.timestamp # event timestamp
frame.parent_id # parent event id, when available
frame.previous_id # previous event id, when available
frame.data # event payload
frame.event # alias for frame.data
frame.content # printable text for token-like frames, otherwise ""
```
The `channel` field is the fastest way to route frames in consumers:
@@ -45,7 +47,7 @@ The `channel` field is the fastest way to route frames in consumers:
## Stream a Flow
Use `stream_events()` to run a Flow and iterate over all frames:
Set `stream=True` on a Flow to make `kickoff()` return a stream session:
```python
from crewai.flow import Flow, start
@@ -57,18 +59,22 @@ class ReportFlow(Flow):
return "done"
flow = ReportFlow()
stream = flow.stream_events()
flow = ReportFlow(stream=True)
stream = flow.kickoff()
with stream:
for frame in stream.events:
print(frame.seq, frame.channel, frame.type, frame.data)
for chunk in stream:
print(chunk.content, end="", flush=True)
if chunk.type == "tool_usage_started":
print(chunk.event["tool_name"])
result = stream.result
```
You must consume the stream before reading `stream.result`. Accessing the result early raises a `RuntimeError` so consumers do not accidentally treat a partial run as complete.
You can also call `flow.stream_events(...)` directly when you want streaming for a single invocation without setting `stream=True` on the Flow instance.
## Filter by Channel
`StreamSession` exposes channel projections that preserve global frame order within the selected channel:
@@ -78,7 +84,7 @@ stream = flow.stream_events()
with stream:
for frame in stream.llm:
print(frame.data.get("chunk", ""), end="", flush=True)
print(frame.content, end="", flush=True)
result = stream.result
```
@@ -105,8 +111,8 @@ flow = ReportFlow()
stream = flow.astream()
async with stream:
async for frame in stream.events:
print(frame.channel, frame.type)
async for chunk in stream.events:
print(chunk.channel, chunk.type, chunk.content)
result = stream.result
```
@@ -115,14 +121,14 @@ The async session has the same projections as the sync session.
## Stream a Direct LLM Call
`llm.call(...)` still returns the final assembled result. Use `llm.stream_call(...)` when you want to iterate over chunks as they arrive:
`llm.call(...)` still returns the final assembled result. Use `llm.stream_events(...)` when you want to iterate over chunks as they arrive while keeping the structured event payload:
```python
from crewai import LLM
llm = LLM(model="gpt-4o-mini")
chunks = llm.stream_call(
stream = llm.stream_events(
messages=[
{
"role": "user",
@@ -131,26 +137,14 @@ chunks = llm.stream_call(
]
)
for chunk in chunks:
print(chunk.content, end="", flush=True)
result = chunks.result
```
Use `llm.stream_events(...)` when a runtime needs the full `StreamFrame` envelope rather than text chunks:
```python
stream = llm.stream_events("Explain CrewAI streaming in two short sentences.")
with stream:
for frame in stream.llm:
if frame.type == "llm_stream_chunk":
print(frame.data.get("chunk", ""), end="", flush=True)
for chunk in stream:
print(chunk.content, end="", flush=True)
result = stream.result
```
Both methods temporarily enable streaming for the wrapped call and restore the LLM's previous `stream` setting afterward. Provider integrations continue to emit the underlying LLM stream events; these helpers provide a common iterator API over those events for every LLM provider.
`llm.stream_events(...)` temporarily enables streaming for the wrapped call and restores the LLM's previous `stream` setting afterward. Provider integrations continue to emit the underlying LLM stream events; this helper provides a common iterator API over those events for every LLM provider.
## Conversational Turns
@@ -172,7 +166,7 @@ stream = flow.stream_turn("What can you help me with?", session_id="session-1")
with stream:
for frame in stream.events:
if frame.channel == "llm" and frame.type == "llm_stream_chunk":
print(frame.data.get("chunk", ""), end="", flush=True)
print(frame.content, end="", flush=True)
reply = stream.result
```

View File

@@ -0,0 +1,195 @@
---
title: 스트리밍 런타임 계약
description: Flow, 직접 LLM 호출, 대화 턴에서 정렬된 런타임 프레임을 스트리밍합니다.
icon: tower-broadcast
mode: "wide"
---
## 개요
CrewAI는 단순한 텍스트 청크보다 더 많은 정보가 필요한 런타임을 위해 프레임 기반 스트리밍 계약을 제공합니다. 이 계약은 Flow 생명주기 이벤트, 직접 LLM 토큰, 도구 활동, 대화 메시지, 사용자 지정 이벤트에 대해 정렬된 `StreamFrame` 객체를 방출합니다.
UI, 서비스 브리지, 터미널 앱, 배포 런타임을 만들 때 Flow, 채팅 턴, 직접 LLM 호출이 실행되는 동안 안정적인 구조화 이벤트 스트림이 필요하다면 이 API를 사용하세요.
## StreamFrame
모든 프레임은 같은 envelope를 가집니다:
```python
from crewai.types.streaming import StreamFrame
frame.version # "v1"
frame.id # 고유 프레임 id
frame.seq # 사용 가능한 경우 실행 로컬 순서
frame.type # "flow_started" 같은 원본 이벤트 타입
frame.channel # "llm", "flow", "tools", "messages", "lifecycle", "custom"
frame.namespace # 소스/런타임 namespace
frame.timestamp # 이벤트 timestamp
frame.parent_id # 사용 가능한 경우 부모 이벤트 id
frame.previous_id # 사용 가능한 경우 이전 이벤트 id
frame.data # 이벤트 payload
frame.event # frame.data의 alias
frame.content # 토큰류 프레임의 출력 가능한 텍스트, 그 외에는 ""
```
`channel` 필드는 소비자에서 프레임을 라우팅하는 가장 빠른 방법입니다:
| 채널 | 포함 내용 |
|------|-----------|
| `llm` | LLM 스트리밍 이벤트의 토큰 및 thinking 청크 |
| `flow` | Flow 생명주기, 메서드 실행, 라우팅, pause/resume 이벤트 |
| `tools` | 도구 사용 이벤트 |
| `messages` | 대화 transcript 이벤트 |
| `lifecycle` | 다른 채널에 속하지 않는 런타임 생명주기 이벤트 |
| `custom` | 내장 채널에 매핑되지 않는 이벤트 |
`frame.type`은 원본 이벤트 타입을 보존하므로, 소비자는 채널 안에서 특정 이벤트를 처리할 수 있습니다.
## Flow 스트리밍
Flow에 `stream=True`를 설정하면 `kickoff()`가 stream session을 반환합니다:
```python
from crewai.flow import Flow, start
class ReportFlow(Flow):
@start()
def generate(self):
return "done"
flow = ReportFlow(stream=True)
stream = flow.kickoff()
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
if chunk.type == "tool_usage_started":
print(chunk.event["tool_name"])
result = stream.result
```
`stream.result`를 읽기 전에 stream을 소비해야 합니다. 결과를 너무 일찍 접근하면 `RuntimeError`가 발생하여, 소비자가 부분 실행을 완료된 실행으로 잘못 처리하지 않도록 합니다.
Flow 인스턴스에 `stream=True`를 설정하지 않고 단일 호출만 스트리밍하려면 `flow.stream_events(...)`를 직접 호출할 수도 있습니다.
## 채널별 필터링
`StreamSession`은 선택한 채널 안에서 전역 프레임 순서를 보존하는 채널 projection을 제공합니다:
```python
stream = flow.stream_events()
with stream:
for frame in stream.llm:
print(frame.content, end="", flush=True)
result = stream.result
```
사용 가능한 projection은 다음과 같습니다:
| Projection | 프레임 |
|------------|--------|
| `stream.events` | 모든 프레임 |
| `stream.llm` | LLM 프레임 |
| `stream.messages` | 대화 메시지 프레임 |
| `stream.flow` | Flow 프레임 |
| `stream.tools` | 도구 프레임 |
| `stream.interleave([...])` | 선택한 채널 집합 |
소비자가 일부 채널만 원하지만 상대 순서도 필요하다면 `stream.interleave(["flow", "llm", "messages"])`를 사용하세요.
## 비동기 스트리밍
비동기 소비자는 `astream()`을 사용하세요:
```python
flow = ReportFlow()
stream = flow.astream()
async with stream:
async for chunk in stream.events:
print(chunk.channel, chunk.type, chunk.content)
result = stream.result
```
비동기 세션은 동기 세션과 같은 projection을 제공합니다.
## 직접 LLM 호출 스트리밍
`llm.call(...)`은 계속 최종 조립 결과를 반환합니다. 구조화된 이벤트 payload를 유지하면서 청크가 도착하는 대로 반복 처리하려면 `llm.stream_events(...)`를 사용하세요:
```python
from crewai import LLM
llm = LLM(model="gpt-4o-mini")
stream = llm.stream_events(
messages=[
{
"role": "user",
"content": "Explain CrewAI streaming in two short sentences.",
}
]
)
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
result = stream.result
```
`llm.stream_events(...)`는 감싼 호출 동안 일시적으로 streaming을 활성화하고, 이후 LLM의 이전 `stream` 설정을 복원합니다. provider 통합은 계속 기본 LLM stream 이벤트를 방출하며, 이 helper는 모든 LLM provider에서 그 이벤트 위에 공통 iterator API를 제공합니다.
## 대화 턴
대화형 Flow는 `stream_turn()`으로 사용자 턴 하나를 스트리밍할 수 있습니다:
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
class ChatFlow(Flow[ConversationState]):
conversational = True
flow = ChatFlow()
stream = flow.stream_turn("What can you help me with?", session_id="session-1")
with stream:
for frame in stream.events:
if frame.channel == "llm" and frame.type == "llm_stream_chunk":
print(frame.content, end="", flush=True)
reply = stream.result
```
`stream_turn()` 중에는 내장 대화 응답 경로가 해당 턴에 대해 LLM 토큰 스트리밍을 활성화하고 이후 LLM의 이전 `stream` 설정을 복원합니다. 자체 agent 또는 LLM 인스턴스를 만드는 사용자 지정 route handler는 토큰 단위 출력이 필요하다면 해당 LLM을 streaming으로 구성해야 합니다.
## 정리
가능하면 세션을 context manager로 사용하세요. stream이 끝나기 전에 클라이언트 연결이 끊기면 세션을 명시적으로 닫으세요:
```python
stream = flow.stream_events()
try:
for frame in stream.events:
print(frame.type)
finally:
if not stream.is_exhausted:
stream.close()
```
비동기 stream에서는 `await stream.aclose()`를 사용하세요.
## 레거시 청크 스트리밍
`stream=True`를 사용하는 Crew 스트리밍은 계속 [스트리밍 Crew 실행](/ko/learn/streaming-crew-execution)에 설명된 청크 중심 `CrewStreamingOutput` API를 반환합니다. 직접 `llm.call(...)` 호출도 계속 최종 LLM 결과를 반환합니다. 프레임 계약은 Flow, 직접 LLM 호출, 대화 턴, 도구, 메시지 전반에서 안정적인 이벤트 envelope가 필요한 런타임을 위한 것입니다.

View File

@@ -0,0 +1,195 @@
---
title: Contrato de Streaming do Runtime
description: Transmita frames ordenados do runtime a partir de Flows, chamadas diretas de LLM e turnos conversacionais.
icon: tower-broadcast
mode: "wide"
---
## Visão geral
O CrewAI expõe um contrato de streaming baseado em frames para runtimes que precisam de mais do que chunks de texto simples. O contrato emite objetos `StreamFrame` ordenados para eventos de ciclo de vida de Flow, tokens de LLM diretos, atividade de ferramentas, mensagens de conversa e eventos personalizados.
Use esta API ao criar uma UI, ponte de serviço, aplicativo de terminal ou runtime de implantação que precise de um fluxo estável de eventos estruturados enquanto um Flow, turno de chat ou chamada direta de LLM está em execução.
## StreamFrame
Todo frame tem o mesmo envelope:
```python
from crewai.types.streaming import StreamFrame
frame.version # "v1"
frame.id # id único do frame
frame.seq # ordem local da execução, quando disponível
frame.type # tipo do evento de origem, como "flow_started"
frame.channel # "llm", "flow", "tools", "messages", "lifecycle" ou "custom"
frame.namespace # namespace de origem/runtime
frame.timestamp # timestamp do evento
frame.parent_id # id do evento pai, quando disponível
frame.previous_id # id do evento anterior, quando disponível
frame.data # payload do evento
frame.event # alias para frame.data
frame.content # texto imprimível para frames de token, caso contrário ""
```
O campo `channel` é a forma mais rápida de rotear frames em consumidores:
| Canal | Contém |
|-------|--------|
| `llm` | Tokens e chunks de raciocínio de eventos de streaming de LLM |
| `flow` | Ciclo de vida do Flow, execução de métodos, roteamento e eventos de pausa/retomada |
| `tools` | Eventos de uso de ferramentas |
| `messages` | Eventos do transcript da conversa |
| `lifecycle` | Eventos de ciclo de vida do runtime que não pertencem a outro canal |
| `custom` | Eventos que não mapeiam para um canal integrado |
`frame.type` preserva o tipo do evento de origem, para que consumidores possam tratar eventos específicos dentro de um canal.
## Transmitir um Flow
Defina `stream=True` em um Flow para fazer `kickoff()` retornar uma sessão de stream:
```python
from crewai.flow import Flow, start
class ReportFlow(Flow):
@start()
def generate(self):
return "done"
flow = ReportFlow(stream=True)
stream = flow.kickoff()
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
if chunk.type == "tool_usage_started":
print(chunk.event["tool_name"])
result = stream.result
```
Você deve consumir o stream antes de ler `stream.result`. Acessar o resultado cedo demais gera um `RuntimeError`, para que consumidores não tratem uma execução parcial como concluída.
Você também pode chamar `flow.stream_events(...)` diretamente quando quiser streaming para uma única invocação sem definir `stream=True` na instância do Flow.
## Filtrar por canal
`StreamSession` expõe projeções por canal que preservam a ordem global dos frames dentro do canal selecionado:
```python
stream = flow.stream_events()
with stream:
for frame in stream.llm:
print(frame.content, end="", flush=True)
result = stream.result
```
As projeções disponíveis são:
| Projeção | Frames |
|----------|--------|
| `stream.events` | Todos os frames |
| `stream.llm` | Frames de LLM |
| `stream.messages` | Frames de mensagens de conversa |
| `stream.flow` | Frames de Flow |
| `stream.tools` | Frames de ferramentas |
| `stream.interleave([...])` | Um conjunto selecionado de canais |
Use `stream.interleave(["flow", "llm", "messages"])` quando um consumidor quiser apenas alguns canais, mas ainda precisar da ordem relativa entre eles.
## Streaming assíncrono
Use `astream()` para consumidores assíncronos:
```python
flow = ReportFlow()
stream = flow.astream()
async with stream:
async for chunk in stream.events:
print(chunk.channel, chunk.type, chunk.content)
result = stream.result
```
A sessão assíncrona tem as mesmas projeções da sessão síncrona.
## Transmitir uma chamada direta de LLM
`llm.call(...)` ainda retorna o resultado final montado. Use `llm.stream_events(...)` quando quiser iterar pelos chunks conforme eles chegam, mantendo o payload estruturado do evento:
```python
from crewai import LLM
llm = LLM(model="gpt-4o-mini")
stream = llm.stream_events(
messages=[
{
"role": "user",
"content": "Explain CrewAI streaming in two short sentences.",
}
]
)
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
result = stream.result
```
`llm.stream_events(...)` ativa temporariamente o streaming para a chamada encapsulada e restaura a configuração anterior de `stream` do LLM depois. As integrações de provedores continuam emitindo os eventos de stream de LLM subjacentes; esse helper fornece uma API de iterador comum sobre esses eventos para todos os provedores de LLM.
## Turnos conversacionais
Flows conversacionais podem transmitir um turno de usuário com `stream_turn()`:
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
class ChatFlow(Flow[ConversationState]):
conversational = True
flow = ChatFlow()
stream = flow.stream_turn("What can you help me with?", session_id="session-1")
with stream:
for frame in stream.events:
if frame.channel == "llm" and frame.type == "llm_stream_chunk":
print(frame.content, end="", flush=True)
reply = stream.result
```
Durante `stream_turn()`, o caminho de resposta conversacional integrado ativa o streaming de tokens de LLM para esse turno e restaura a configuração anterior de `stream` do LLM depois. Handlers de rota personalizados que criam seus próprios agentes ou instâncias de LLM devem configurar esses LLMs para streaming se precisarem de saída em nível de token.
## Limpeza
Use a sessão como gerenciador de contexto quando possível. Se um cliente se desconectar antes de o stream ser esgotado, feche a sessão explicitamente:
```python
stream = flow.stream_events()
try:
for frame in stream.events:
print(frame.type)
finally:
if not stream.is_exhausted:
stream.close()
```
Para streams assíncronos, use `await stream.aclose()`.
## Streaming de chunks legado
O streaming de Crew com `stream=True` ainda retorna a API orientada a chunks `CrewStreamingOutput` descrita em [Streaming da Execução de Crew](/pt-BR/learn/streaming-crew-execution). Chamadas diretas `llm.call(...)` ainda retornam o resultado final do LLM. O contrato de frames é destinado a runtimes que precisam de um envelope de evento estável em Flows, chamadas diretas de LLM, turnos conversacionais, ferramentas e mensagens.

View File

@@ -21,11 +21,12 @@ messages = [
}
]
chunks = llm.stream_call(messages=messages)
stream = llm.stream_events(messages=messages)
print("--- chunks ---")
for chunk in chunks:
print(chunk.content, end="", flush=True)
with stream:
for chunk in stream:
print(chunk.content, end="", flush=True)
# print("\n\n--- result ---")
# print(chunks.result)
print("\n\n--- result ---")
print(stream.result)

View File

@@ -9,12 +9,7 @@ Structure (see ``flow_definition``) and executed here.
from __future__ import annotations
import asyncio
from collections.abc import (
AsyncIterator,
Callable,
Iterator,
Sequence,
)
from collections.abc import Callable, Iterator, Sequence
from concurrent.futures import Future, ThreadPoolExecutor
import contextvars
import copy
@@ -143,7 +138,6 @@ if TYPE_CHECKING:
from crewai.flow.visualization import build_flow_structure, render_interactive
from crewai.types.streaming import (
AsyncStreamSession,
FlowStreamingOutput,
StreamSession,
)
from crewai.types.usage_metrics import UsageMetrics
@@ -152,7 +146,6 @@ from crewai.utilities.streaming import (
create_async_frame_generator,
create_frame_generator,
create_frame_streaming_state,
stream_frame_to_chunk,
)
@@ -1921,7 +1914,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
input_files: dict[str, FileInput] | None = None,
from_checkpoint: CheckpointConfig | None = None,
restore_from_state_id: str | None = None,
) -> Any | FlowStreamingOutput:
) -> Any | StreamSession[Any]:
"""Start the flow execution in a synchronous context.
This method wraps kickoff_async so that all state initialization and event
@@ -1942,7 +1935,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
``from_checkpoint``; passing both raises ``ValueError``.
Returns:
The final output from the flow or FlowStreamingOutput if streaming.
The final output from the flow or StreamSession if streaming.
"""
if from_checkpoint is not None and restore_from_state_id is not None:
raise ValueError(
@@ -1954,28 +1947,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
if restored is not None:
return restored.kickoff(inputs=inputs, input_files=input_files)
if self.stream:
streaming_output: FlowStreamingOutput
def chunk_iterator() -> Iterator[Any]:
stream_session = self.stream_events(
inputs=inputs,
input_files=input_files,
restore_from_state_id=restore_from_state_id,
)
try:
with stream_session:
for frame in stream_session.llm:
chunk = stream_frame_to_chunk(frame)
if chunk is not None:
yield chunk
streaming_output._set_result(stream_session.result)
finally:
if not stream_session.is_exhausted:
stream_session.close()
streaming_output = FlowStreamingOutput(sync_iterator=chunk_iterator())
return streaming_output
return self.stream_events(
inputs=inputs,
input_files=input_files,
restore_from_state_id=restore_from_state_id,
)
async def _run_flow() -> Any:
return await self.kickoff_async(
@@ -2002,7 +1978,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
input_files: dict[str, FileInput] | None = None,
from_checkpoint: CheckpointConfig | None = None,
restore_from_state_id: str | None = None,
) -> Any | FlowStreamingOutput:
) -> Any | AsyncStreamSession[Any]:
"""Start the flow execution asynchronously.
This method performs state restoration (if an 'id' is provided and persistence is available)
@@ -2036,28 +2012,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
if restored is not None:
return await restored.kickoff_async(inputs=inputs, input_files=input_files)
if self.stream:
streaming_output: FlowStreamingOutput
async def chunk_iterator() -> AsyncIterator[Any]:
stream_session = self.astream(
inputs=inputs,
input_files=input_files,
restore_from_state_id=restore_from_state_id,
)
try:
async with stream_session:
async for frame in stream_session.llm:
chunk = stream_frame_to_chunk(frame)
if chunk is not None:
yield chunk
streaming_output._set_result(stream_session.result)
finally:
if not stream_session.is_exhausted:
await stream_session.aclose()
streaming_output = FlowStreamingOutput(async_iterator=chunk_iterator())
return streaming_output
return self.astream(
inputs=inputs,
input_files=input_files,
restore_from_state_id=restore_from_state_id,
)
ctx = baggage.set_baggage("flow_inputs", inputs or {})
ctx = baggage.set_baggage("flow_input_files", input_files or {}, context=ctx)
@@ -2401,7 +2360,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
input_files: dict[str, FileInput] | None = None,
from_checkpoint: CheckpointConfig | None = None,
restore_from_state_id: str | None = None,
) -> Any | FlowStreamingOutput:
) -> Any | AsyncStreamSession[Any]:
"""Native async method to start the flow execution. Alias for kickoff_async.
Args:

View File

@@ -7,7 +7,7 @@ in CrewAI, including common functionality for native SDK implementations.
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Generator, Iterator
from collections.abc import Generator
from contextlib import contextmanager
import contextvars
from datetime import datetime
@@ -42,13 +42,12 @@ from crewai.events.types.tool_usage_events import (
ToolUsageFinishedEvent,
ToolUsageStartedEvent,
)
from crewai.types.streaming import LLMStreamingOutput, StreamSession
from crewai.types.streaming import StreamSession
from crewai.types.usage_metrics import UsageMetrics
from crewai.utilities.pydantic_schema_utils import serialize_model_class
from crewai.utilities.streaming import (
create_frame_generator,
create_frame_streaming_state,
stream_frame_to_chunk,
)
@@ -361,42 +360,6 @@ class BaseLLM(BaseModel, ABC):
output_holder.append(stream_session)
return stream_session
def stream_call(
self,
messages: str | list[LLMMessage],
tools: list[dict[str, BaseTool]] | None = None,
callbacks: list[Any] | None = None,
available_functions: dict[str, Any] | None = None,
from_task: Task | None = None,
from_agent: BaseAgent | None = None,
response_model: type[BaseModel] | None = None,
) -> LLMStreamingOutput:
"""Run the LLM call and stream text chunks as they arrive."""
def chunk_iterator() -> Iterator[Any]:
stream_session = self.stream_events(
messages=messages,
tools=tools,
callbacks=callbacks,
available_functions=available_functions,
from_task=from_task,
from_agent=from_agent,
response_model=response_model,
)
try:
with stream_session:
for frame in stream_session.llm:
chunk = stream_frame_to_chunk(frame)
if chunk is not None:
yield chunk
streaming_output._set_result(stream_session.result)
finally:
if not stream_session.is_exhausted:
stream_session.close()
streaming_output = LLMStreamingOutput(sync_iterator=chunk_iterator())
return streaming_output
async def acall(
self,
messages: str | list[LLMMessage],

View File

@@ -42,6 +42,19 @@ class StreamFrame(BaseModel):
previous_id: str | None = None
data: dict[str, Any] = Field(default_factory=dict)
@property
def content(self) -> str:
"""Printable text content for chunk-like consumers."""
chunk = self.data.get("chunk")
if isinstance(chunk, str):
return chunk
return ""
@property
def event(self) -> dict[str, Any]:
"""Structured source event payload."""
return self.data
class StreamSessionBase(Generic[T]):
"""Base stream session with ordered frame iteration and result access."""
@@ -115,6 +128,10 @@ class StreamSession(StreamSessionBase[T]):
"""Iterate over all ordered frames."""
return self.subscribe()
def __iter__(self) -> Iterator[StreamFrame]:
"""Iterate over all ordered frames."""
return self.events
@property
def llm(self) -> Iterator[StreamFrame]:
"""Iterate over LLM token and thinking frames."""
@@ -186,6 +203,10 @@ class AsyncStreamSession(StreamSessionBase[T]):
"""Iterate over all ordered frames."""
return self.subscribe()
def __aiter__(self) -> AsyncIterator[StreamFrame]:
"""Iterate over all ordered frames."""
return self.events
@property
def llm(self) -> AsyncIterator[StreamFrame]:
"""Iterate over LLM token and thinking frames."""
@@ -576,12 +597,3 @@ class FlowStreamingOutput(StreamingOutputBase[Any]):
"""
self._result = result
self._completed = True
class LLMStreamingOutput(StreamingOutputBase[Any]):
"""Streaming output wrapper for direct LLM calls."""
def _set_result(self, result: Any) -> None:
"""Set the final LLM call result after streaming completes."""
self._result = result
self._completed = True

View File

@@ -29,7 +29,7 @@ from crewai.flow.persistence.base import FlowPersistence
from crewai.flow.runtime._actions import FlowScriptExecutionDisabledError
from crewai.state.checkpoint_config import CheckpointConfig
from crewai.tools import BaseTool
from crewai.types.streaming import FlowStreamingOutput
from crewai.types.streaming import StreamSession
class StaticSearchTool(BaseTool):
@@ -2485,7 +2485,7 @@ def test_config_max_method_calls_from_declaration():
def test_config_stream_from_declaration():
flow = Flow.from_declaration(contents=STREAMING_CHAIN_YAML)
streaming = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, StreamSession)
for _ in streaming:
pass
assert streaming.result == "confirmed:True"

View File

@@ -14,7 +14,7 @@ from crewai.events.types.llm_events import LLMStreamChunkEvent, LLMThinkingChunk
from crewai.events.types.tool_usage_events import ToolUsageStartedEvent
from crewai.flow.flow import Flow, start
from crewai.llms.base_llm import BaseLLM
from crewai.types.streaming import FlowStreamingOutput, StreamFrame
from crewai.types.streaming import StreamFrame
class FrameFlow(Flow):
@@ -129,36 +129,37 @@ def test_stream_errors_surface_after_failed_frame() -> None:
_ = stream.result
def test_legacy_flow_streaming_uses_llm_frame_projection() -> None:
def test_flow_streaming_returns_iterable_frame_session() -> None:
flow = FrameFlow()
flow.stream = True
streaming = flow.kickoff()
stream = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
chunks = list(streaming)
assert [chunk.content for chunk in chunks] == ["hello", "thinking"]
assert streaming.result == "done"
with stream:
frames = list(stream)
assert all(isinstance(frame, StreamFrame) for frame in frames)
assert [frame.content for frame in frames if frame.content] == [
"hello",
"thinking",
]
first_content_frame = next(frame for frame in frames if frame.content)
assert first_content_frame.event["chunk"] == "hello"
assert stream.result == "done"
def test_direct_llm_stream_events_scope_and_restore_stream_flag() -> None:
llm = DirectStreamingLLM(model="gpt-4o-mini", stream=False)
with llm.stream_events("hello") as stream:
frames = list(stream.llm)
frames = list(stream)
assert [frame.data["chunk"] for frame in frames] == ["hel", "lo"]
assert [frame.content for frame in frames] == ["hel", "lo"]
assert frames[0].event["chunk"] == "hel"
assert stream.result == "hello"
assert llm.stream is False
def test_direct_llm_stream_call_projects_chunks() -> None:
chunks = DirectStreamingLLM(model="gpt-4o-mini").stream_call("hello")
assert [chunk.content for chunk in chunks] == ["hel", "lo"]
assert chunks.result == "hello"
@pytest.mark.asyncio
async def test_astream_scopes_concurrent_executions() -> None:
class ConcurrentFlow(Flow):

View File

@@ -12,10 +12,13 @@ from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.llm_events import LLMStreamChunkEvent, ToolCall, FunctionCall
from crewai.flow.flow import Flow, start
from crewai.types.streaming import (
AsyncStreamSession,
CrewStreamingOutput,
FlowStreamingOutput,
StreamChunk,
StreamChunkType,
StreamFrame,
StreamSession,
ToolCallChunk,
)
@@ -417,8 +420,8 @@ class TestCrewKickoffStreamingAsync:
class TestFlowKickoffStreaming:
"""Tests for Flow(stream=True).kickoff() method."""
def test_kickoff_streaming_returns_streaming_output(self) -> None:
"""Test that flow kickoff with stream=True returns FlowStreamingOutput."""
def test_kickoff_streaming_returns_stream_session(self) -> None:
"""Test that flow kickoff with stream=True returns StreamSession."""
class SimpleFlow(Flow[dict[str, Any]]):
@start()
@@ -428,7 +431,7 @@ class TestFlowKickoffStreaming:
flow = SimpleFlow()
flow.stream = True
streaming = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, StreamSession)
def test_flow_kickoff_streaming_captures_chunks(self) -> None:
"""Test that flow streaming captures LLM chunks from crew execution."""
@@ -469,7 +472,7 @@ class TestFlowKickoffStreaming:
with patch.object(Flow, "kickoff", mock_kickoff_fn):
streaming = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, StreamSession)
chunks = list(streaming)
assert len(chunks) >= 2
@@ -500,7 +503,7 @@ class TestFlowKickoffStreaming:
with patch.object(Flow, "kickoff", mock_kickoff_fn):
streaming = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, StreamSession)
_ = list(streaming)
result = streaming.result
@@ -511,8 +514,8 @@ class TestFlowKickoffStreamingAsync:
"""Tests for Flow(stream=True).kickoff_async() method."""
@pytest.mark.asyncio
async def test_kickoff_streaming_async_returns_streaming_output(self) -> None:
"""Test that flow kickoff_async with stream=True returns FlowStreamingOutput."""
async def test_kickoff_streaming_async_returns_stream_session(self) -> None:
"""Test that flow kickoff_async with stream=True returns AsyncStreamSession."""
class SimpleFlow(Flow[dict[str, Any]]):
@start()
@@ -522,7 +525,7 @@ class TestFlowKickoffStreamingAsync:
flow = SimpleFlow()
flow.stream = True
streaming = await flow.kickoff_async()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, AsyncStreamSession)
@pytest.mark.asyncio
async def test_flow_kickoff_streaming_async_captures_chunks(self) -> None:
@@ -567,8 +570,8 @@ class TestFlowKickoffStreamingAsync:
with patch.object(Flow, "kickoff_async", mock_kickoff_fn):
streaming = await flow.kickoff_async()
assert isinstance(streaming, FlowStreamingOutput)
chunks: list[StreamChunk] = []
assert isinstance(streaming, AsyncStreamSession)
chunks: list[StreamFrame] = []
async for chunk in streaming:
chunks.append(chunk)
@@ -601,7 +604,7 @@ class TestFlowKickoffStreamingAsync:
with patch.object(Flow, "kickoff_async", mock_kickoff_fn):
streaming = await flow.kickoff_async()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, AsyncStreamSession)
async for _ in streaming:
pass

View File

@@ -4,7 +4,7 @@ import pytest
from crewai import Agent, Crew, Task
from crewai.flow.flow import Flow, start
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
from crewai.types.streaming import AsyncStreamSession, CrewStreamingOutput, StreamSession
@pytest.fixture
@@ -212,7 +212,7 @@ class TestStreamingFlowIntegration:
streaming = flow.kickoff()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, StreamSession)
chunks = []
for chunk in streaming:
@@ -281,7 +281,7 @@ class TestStreamingFlowIntegration:
streaming = await flow.kickoff_async()
assert isinstance(streaming, FlowStreamingOutput)
assert isinstance(streaming, AsyncStreamSession)
chunks = []
async for chunk in streaming: