feat(flow): promote conversational flows to stable (#7107)

Move the canonical API into crewai.flow while preserving experimental imports and declarative references through compatibility aliases.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Lorenze Jay
2026-08-27 23:06:57 +05:30
committed by GitHub
co-authored by Cursor
parent 5139078c6a
commit f90d37b4ac
21 changed files with 2002 additions and 1969 deletions
@@ -5,14 +5,6 @@ icon: comments
mode: "wide"
---
<Warning>
**هذه ميزة تجريبية.** يقع سطح `Flow` المحادثاتي
(`conversational = True` و`handle_turn` و`stream_turn` و`ConversationConfig`
و`RouterConfig` و`ConversationState` والرسم المدمج) ضمن
`crewai.experimental`، وقد يتغير قبل أن يصبح مستقراً. ثبّت إصدار CrewAI
إذا كنت تعتمد على سلوك محدد.
</Warning>
## نظرة عامة
تعامل التطبيقات المحادثية مع كل سطر من المستخدم كـ **تشغيل flow جديد** بنفس **معرّف الجلسة**. توفر CrewAI مساعدات لسجل الرسائل، وتوجيه النية الاختياري، وتأجيل التتبع، والبث المنظّم للجولات، إضافة إلى REPL محلي عبر `flow.chat()`.
@@ -48,7 +40,7 @@ from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -129,7 +121,7 @@ result = stream.result
## مساعدات `ChatState` منخفضة المستوى
تظل `ChatState` و`ConversationalConfig` القديمة ومساعدات `crewai.flow.conversation` قابلة للاستيراد للتنسيق المتقدم أو الاختبارات أو الأغلفة المخصصة. وهي منفصلة عن واجهتي `ConversationState` / `ConversationConfig` التجريبيتين، ولا تضيف وسيطي `user_message=` أو `session_id=` إلى `Flow.kickoff()`.
تظل `ChatState` و`ConversationalConfig` القديمة ومساعدات `crewai.flow.conversation` قابلة للاستيراد للتنسيق المتقدم أو الاختبارات أو الأغلفة المخصصة. وهي منفصلة عن واجهتي `ConversationState` / `ConversationConfig`، ولا تضيف وسيطي `user_message=` أو `session_id=` إلى `Flow.kickoff()`.
```python
from crewai.flow import ChatState
@@ -151,7 +143,7 @@ class MyChatState(ChatState):
`ConversationalInputs` هو `TypedDict` لمفاتيح `kickoff(inputs={...})` الاصطلاحية: `id` و`user_message` و`last_intent`.
تخزن `ConversationState` التجريبية `messages` ككائنات `ConversationMessage`، وتوفر أيضاً `current_user_message` و`ended` و`events` و`agent_threads`. استخدم `conversation_messages` عند تمرير سجلها القانوني إلى LLM.
تخزن `ConversationState` رسائل `messages` ككائنات `ConversationMessage`، وتوفر أيضاً `current_user_message` و`ended` و`events` و`agent_threads`. استخدم `conversation_messages` عند تمرير سجلها القانوني إلى LLM.
## API المحادثة على `Flow`
@@ -193,7 +185,7 @@ class MyChatState(ChatState):
### مساعدات الوحدة (`crewai.flow.conversation`)
يمكن استيرادها من `crewai.flow.conversation` للاختبارات أو التنسيق المخصص. تستخدم هذه المساعدات بنية `ConversationalConfig` القديمة؛ كما تمسح `prepare_conversational_turn()` قيمة `last_intent`، بخلاف `handle_turn()` التجريبية التي تحتفظ بها كسياق للموجّه.
يمكن استيرادها من `crewai.flow.conversation` للاختبارات أو التنسيق المخصص. تستخدم هذه المساعدات بنية `ConversationalConfig` القديمة؛ كما تمسح `prepare_conversational_turn()` قيمة `last_intent`، بخلاف `handle_turn()` التي تحتفظ بها كسياق للموجّه.
| الدالة | الوصف |
|--------|--------|
@@ -237,7 +229,7 @@ def route_turn(self, context):
لا تستخدم `@human_feedback` لأسطر المتابعة في الدردشة إلا عند الحاجة لموافقة بشرية على مخرجات خطوة محددة.
## `Flow` المحادثاتي (تجريبي)
## `Flow` المحادثاتي
اشترك في رسم الدردشة المحادثاتي بتعيين `conversational = True` على صنف فرعي من `Flow` أو بتطبيق `@ConversationConfig(...)`. يوفر `Flow` الأساسي عندئذٍ `route_conversation` كنقطة البدء/الموجّه المدمجة، إضافة إلى مستمعي `converse_turn` و`end_conversation`. يظل المستمع المهمل `answer_from_history_turn` متاحاً للتوافق. يدير الإطار `state.messages`، ويمكنه تشغيل LLM للموجّه، ويبقي دفعة trace مفتوحة عبر الجولات. أنت تكتب **المسارات المخصصة**؛ والإطار يتولى الباقي.
@@ -248,7 +240,7 @@ def route_turn(self, context):
```python
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -333,7 +325,7 @@ from typing import Literal
from pydantic import BaseModel
from crewai import LLM
from crewai.experimental.conversational import RouterConfig
from crewai.flow import RouterConfig
class MyRoute(BaseModel):
@@ -472,7 +464,7 @@ flow.chat(
from typing import Any
from crewai import Flow
from crewai.experimental.conversational import ConversationState
from crewai.flow import ConversationState
class SupportFlow(Flow[ConversationState]):
@@ -580,7 +572,7 @@ flow.chat(session_id=session_id)
### دورة حياة trace لـ `Flow` المحادثاتي
يستخدم [`Flow` المحادثاتي](#flow-المحادثاتي-تجريبي) التجريبي دورة حياة التتبع نفسها: القيمة الافتراضية لـ `defer_trace_finalization` هي `True`، ولذلك يبقي كل `handle_turn()` trace الجلسة مفتوحاً. تمنع الجولات المؤجلة أيضاً إصدار `flow_failed` لكل جولة؛ وعند حدوث خطأ في جولة أو إلغاء الجلسة، أنهِ الجلسة صراحةً. يغلق ذلك الدفعة بحدث `FlowFinished` على مستوى الجلسة بدلاً من حدث `FlowFailed` لكل جولة. لُف REPL/الحلقة دائماً بـ `try/finally` واستدعِ `flow.finalize_session_traces()` عند الخروج. بدون ذلك، تبقى دفعة trace مفتوحة وقد لا تُصدَّر المحادثة النهائية أبداً.
يستخدم [`Flow` المحادثاتي](#flow-المحادثاتي) دورة حياة التتبع نفسها: القيمة الافتراضية لـ `defer_trace_finalization` هي `True`، ولذلك يبقي كل `handle_turn()` trace الجلسة مفتوحاً. تمنع الجولات المؤجلة أيضاً إصدار `flow_failed` لكل جولة؛ وعند حدوث خطأ في جولة أو إلغاء الجلسة، أنهِ الجلسة صراحةً. يغلق ذلك الدفعة بحدث `FlowFinished` على مستوى الجلسة بدلاً من حدث `FlowFailed` لكل جولة. لُف REPL/الحلقة دائماً بـ `try/finally` واستدعِ `flow.finalize_session_traces()` عند الخروج. بدون ذلك، تبقى دفعة trace مفتوحة وقد لا تُصدَّر المحادثة النهائية أبداً.
## البث
@@ -613,7 +605,7 @@ from crewai.flow import (
start,
)
from crewai.flow.conversation import prepare_conversational_turn
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
RouterConfig,
@@ -151,7 +151,7 @@ result = stream.result
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
from crewai.flow import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
@@ -5,14 +5,6 @@ icon: comments
mode: "wide"
---
<Warning>
**This is an experimental feature.** The conversational `Flow` surface
(`conversational = True`, `handle_turn`, `stream_turn`, `ConversationConfig`,
`RouterConfig`, `ConversationState`, and the built-in graph) lives under
`crewai.experimental` and may change before it graduates. Pin your CrewAI
version if you depend on specific behavior.
</Warning>
## Overview
Conversational apps treat each user line as a **new flow run** with the **same session id**. CrewAI adds helpers for message history, optional intent routing, deferred tracing, structured turn streaming, and a local `flow.chat()` REPL.
@@ -48,7 +40,7 @@ from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -129,7 +121,7 @@ Decorating a `Flow` subclass with `ConversationConfig` both attaches the chat de
## Lower-level `ChatState` helpers
`ChatState`, the legacy `ConversationalConfig`, and `crewai.flow.conversation` helpers are still importable for advanced orchestration, tests, or custom wrappers. They are separate from the experimental `ConversationState` / `ConversationConfig` API and do not add `user_message=` or `session_id=` keyword arguments to `Flow.kickoff()`.
`ChatState`, the legacy `ConversationalConfig`, and `crewai.flow.conversation` helpers are still importable for advanced orchestration, tests, or custom wrappers. They are separate from the `ConversationState` / `ConversationConfig` API and do not add `user_message=` or `session_id=` keyword arguments to `Flow.kickoff()`.
```python
from crewai.flow import ChatState
@@ -151,7 +143,7 @@ class MyChatState(ChatState):
`ConversationalInputs` is a `TypedDict` for conventional `kickoff(inputs={...})` keys: `id`, `user_message`, `last_intent`.
The experimental `ConversationState` stores `messages` as `ConversationMessage` objects and additionally provides `current_user_message`, `ended`, `events`, and `agent_threads`. Use `conversation_messages` when passing its canonical history to an LLM.
`ConversationState` stores `messages` as `ConversationMessage` objects and additionally provides `current_user_message`, `ended`, `events`, and `agent_threads`. Use `conversation_messages` when passing its canonical history to an LLM.
## `Flow` conversational API
@@ -193,7 +185,7 @@ The experimental `ConversationState` stores `messages` as `ConversationMessage`
### Module helpers (`crewai.flow.conversation`)
Importable from `crewai.flow.conversation` for tests or custom orchestration. These helpers use the legacy `ConversationalConfig` shape; `prepare_conversational_turn()` also clears `last_intent`, unlike experimental `handle_turn()`, which preserves it as router context.
Importable from `crewai.flow.conversation` for tests or custom orchestration. These helpers use the legacy `ConversationalConfig` shape; `prepare_conversational_turn()` also clears `last_intent`, unlike `handle_turn()`, which preserves it as router context.
| Function | Description |
|----------|-------------|
@@ -237,7 +229,7 @@ Each `handle_turn()` completes one graph run, and the conversation continues wit
Do **not** use `@human_feedback` for follow-up chat lines unless a human must approve a specific step output before it is shown.
## Conversational `Flow` (experimental)
## Conversational `Flow`
Opt into the conversational chat graph by setting `conversational = True` on a `Flow` subclass or applying `@ConversationConfig(...)`. The base `Flow` then supplies `route_conversation` as the built-in start/router plus the `converse_turn` and `end_conversation` listeners. The deprecated `answer_from_history_turn` listener remains available for compatibility. The framework manages `state.messages`, can drive a router LLM, and keeps the trace batch open across turns. You write the **custom routes**; the framework owns the rest.
@@ -248,7 +240,7 @@ Use this when you want a multi-turn chat with a router and per-route handlers wi
```python
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -333,7 +325,7 @@ from typing import Literal
from pydantic import BaseModel
from crewai import LLM
from crewai.experimental.conversational import RouterConfig
from crewai.flow import RouterConfig
class MyRoute(BaseModel):
@@ -471,7 +463,7 @@ To run side effects (event bus setup, telemetry) on every routing decision, over
from typing import Any
from crewai import Flow
from crewai.experimental.conversational import ConversationState
from crewai.flow import ConversationState
class SupportFlow(Flow[ConversationState]):
@@ -581,7 +573,7 @@ the session ends.
### Conversational `Flow` trace lifecycle
The experimental [conversational `Flow`](#conversational-flow-experimental) uses the same tracing lifecycle: `defer_trace_finalization` defaults to `True`, so each `handle_turn()` keeps the session trace open. Deferred turns also suppress per-turn `flow_failed`; on a turn error or session abort, finalize the session explicitly. This closes the batch with the session-level `FlowFinished` event rather than a per-turn `FlowFailed` event. Always wrap your REPL/loop in `try/finally` and call `flow.finalize_session_traces()` on exit. Without it, the trace batch stays open and the final conversation may never export.
The [conversational `Flow`](#conversational-flow) uses the same tracing lifecycle: `defer_trace_finalization` defaults to `True`, so each `handle_turn()` keeps the session trace open. Deferred turns also suppress per-turn `flow_failed`; on a turn error or session abort, finalize the session explicitly. This closes the batch with the session-level `FlowFinished` event rather than a per-turn `FlowFailed` event. Always wrap your REPL/loop in `try/finally` and call `flow.finalize_session_traces()` on exit. Without it, the trace batch stays open and the final conversation may never export.
## Streaming
@@ -614,7 +606,7 @@ from crewai.flow import (
start,
)
from crewai.flow.conversation import prepare_conversational_turn
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
RouterConfig,
@@ -151,7 +151,7 @@ Conversational Flows can stream one user turn with `stream_turn()`:
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
from crewai.flow import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
@@ -5,14 +5,6 @@ icon: comments
mode: "wide"
---
<Warning>
**실험적 기능입니다.** 대화형 `Flow` API 표면
(`conversational = True`, `handle_turn`, `stream_turn`, `ConversationConfig`,
`RouterConfig`, `ConversationState`, 내장 그래프)은 `crewai.experimental`
하위에 있으며 정식 기능이 되기 전에 변경될 수 있습니다. 특정 동작에
의존한다면 CrewAI 버전을 고정하세요.
</Warning>
## 개요
대화형 앱은 각 사용자 입력을 **동일한 세션 id**로 **새 flow 실행**으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 라우팅, 지연 트레이싱, 구조화된 턴 스트리밍, 로컬 `flow.chat()` REPL을 위한 헬퍼를 제공합니다.
@@ -48,7 +40,7 @@ from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -129,7 +121,7 @@ result = stream.result
## 하위 수준 `ChatState` 헬퍼
`ChatState`, 레거시 `ConversationalConfig`, `crewai.flow.conversation` 헬퍼는 고급 오케스트레이션, 테스트, 커스텀 래퍼에서 계속 import할 수 있습니다. 이들은 실험적 `ConversationState` / `ConversationConfig` API와 별개이며 `Flow.kickoff()`에 `user_message=` 또는 `session_id=` 키워드 인자를 추가하지 않습니다.
`ChatState`, 레거시 `ConversationalConfig`, `crewai.flow.conversation` 헬퍼는 고급 오케스트레이션, 테스트, 커스텀 래퍼에서 계속 import할 수 있습니다. 이들은 `ConversationState` / `ConversationConfig` API와 별개이며 `Flow.kickoff()`에 `user_message=` 또는 `session_id=` 키워드 인자를 추가하지 않습니다.
```python
from crewai.flow import ChatState
@@ -151,7 +143,7 @@ class MyChatState(ChatState):
`ConversationalInputs`는 `kickoff(inputs={...})`용 `TypedDict`: `id`, `user_message`, `last_intent`.
실험적 `ConversationState`는 `messages`를 `ConversationMessage` 객체로 저장하며 `current_user_message`, `ended`, `events`, `agent_threads`도 제공합니다. 정식 기록을 LLM에 전달할 때는 `conversation_messages`를 사용하세요.
`ConversationState`는 `messages`를 `ConversationMessage` 객체로 저장하며 `current_user_message`, `ended`, `events`, `agent_threads`도 제공합니다. 정식 기록을 LLM에 전달할 때는 `conversation_messages`를 사용하세요.
## `Flow` 대화 API
@@ -193,7 +185,7 @@ class MyChatState(ChatState):
### 모듈 헬퍼 (`crewai.flow.conversation`)
테스트 또는 커스텀 오케스트레이션을 위해 `crewai.flow.conversation`에서 import할 수 있습니다. 이 헬퍼들은 레거시 `ConversationalConfig` 형태를 사용합니다. 또한 `prepare_conversational_turn()`은 `last_intent`를 지우지만, 실험적 `handle_turn()`은 router 컨텍스트로 보존합니다.
테스트 또는 커스텀 오케스트레이션을 위해 `crewai.flow.conversation`에서 import할 수 있습니다. 이 헬퍼들은 레거시 `ConversationalConfig` 형태를 사용합니다. 또한 `prepare_conversational_turn()`은 `last_intent`를 지우지만, `handle_turn()`은 router 컨텍스트로 보존합니다.
| 함수 | 설명 |
|------|------|
@@ -237,7 +229,7 @@ def route_turn(self, context):
후속 채팅 줄에 `@human_feedback`를 쓰지 마세요. 특정 스텝 출력을 사람이 승인해야 할 때만 사용하세요.
## 대화형 `Flow` (실험적)
## 대화형 `Flow`
`Flow` 서브클래스에 `conversational = True`를 지정하거나 `@ConversationConfig(...)`를 적용하면 대화형 채팅 그래프가 활성화됩니다. 베이스 `Flow`는 내장 start/router인 `route_conversation`과 `converse_turn`, `end_conversation` 리스너를 제공합니다. 사용 중단된 `answer_from_history_turn` 리스너는 호환성을 위해 계속 제공됩니다. 또한 `state.messages`를 관리하고 router LLM을 구동할 수 있으며 턴 간 trace batch를 열린 상태로 유지합니다. 여러분은 **커스텀 라우트**를 작성하고 나머지는 프레임워크에 맡기면 됩니다.
@@ -248,7 +240,7 @@ LLM 기반 라우터와 라우트별 핸들러로 멀티턴 챗을 만들고 싶
```python
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -333,7 +325,7 @@ from typing import Literal
from pydantic import BaseModel
from crewai import LLM
from crewai.experimental.conversational import RouterConfig
from crewai.flow import RouterConfig
class MyRoute(BaseModel):
@@ -468,7 +460,7 @@ flow.chat(
from typing import Any
from crewai import Flow
from crewai.experimental.conversational import ConversationState
from crewai.flow import ConversationState
class SupportFlow(Flow[ConversationState]):
@@ -576,7 +568,7 @@ flow.chat(session_id=session_id)
### 대화형 `Flow` trace 수명 주기
실험적 [대화형 `Flow`](#대화형-flow-실험적)는 동일한 tracing 수명 주기를 따릅니다. `defer_trace_finalization` 기본값이 `True`이므로 각 `handle_turn()`은 세션 trace를 열린 상태로 유지합니다. 지연된 턴은 턴별 `flow_failed`도 억제합니다. 턴 오류나 세션 중단이 발생하면 세션을 명시적으로 finalize하세요. 그러면 턴별 `FlowFailed` 이벤트 대신 세션 수준 `FlowFinished` 이벤트로 batch가 닫힙니다. REPL/루프는 항상 `try/finally`로 감싸고 종료 시 `flow.finalize_session_traces()`를 호출하세요. 호출하지 않으면 trace batch가 열린 채 남아 최종 대화가 export되지 않을 수 있습니다.
[대화형 `Flow`](#대화형-flow)는 동일한 tracing 수명 주기를 따릅니다. `defer_trace_finalization` 기본값이 `True`이므로 각 `handle_turn()`은 세션 trace를 열린 상태로 유지합니다. 지연된 턴은 턴별 `flow_failed`도 억제합니다. 턴 오류나 세션 중단이 발생하면 세션을 명시적으로 finalize하세요. 그러면 턴별 `FlowFailed` 이벤트 대신 세션 수준 `FlowFinished` 이벤트로 batch가 닫힙니다. REPL/루프는 항상 `try/finally`로 감싸고 종료 시 `flow.finalize_session_traces()`를 호출하세요. 호출하지 않으면 trace batch가 열린 채 남아 최종 대화가 export되지 않을 수 있습니다.
## 스트리밍
@@ -609,7 +601,7 @@ from crewai.flow import (
start,
)
from crewai.flow.conversation import prepare_conversational_turn
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
RouterConfig,
@@ -151,7 +151,7 @@ result = stream.result
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
from crewai.flow import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
@@ -5,15 +5,6 @@ icon: comments
mode: "wide"
---
<Warning>
**Esta é uma funcionalidade experimental.** A superfície conversacional de
`Flow` (`conversational = True`, `handle_turn`, `stream_turn`,
`ConversationConfig`, `RouterConfig`, `ConversationState` e o grafo
embutido) vive em `crewai.experimental` e pode mudar antes de se tornar
estável. Fixe sua versão do CrewAI se depender de um comportamento
específico.
</Warning>
## Visão geral
Apps conversacionais tratam cada linha do usuário como uma **nova execução do flow** com o **mesmo id de sessão**. A CrewAI oferece helpers para histórico de mensagens, roteamento opcional de intenção, tracing adiado, streaming estruturado de turnos e um REPL local `flow.chat()`.
@@ -49,7 +40,7 @@ from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -130,7 +121,7 @@ Decorar uma subclasse de `Flow` com `ConversationConfig` anexa os padrões de ch
## Helpers `ChatState` de mais baixo nível
`ChatState`, o `ConversationalConfig` legado e os helpers de `crewai.flow.conversation` continuam disponíveis para importação em orquestração avançada, testes ou wrappers customizados. Eles são separados da API experimental `ConversationState` / `ConversationConfig` e não adicionam os argumentos nomeados `user_message=` ou `session_id=` a `Flow.kickoff()`.
`ChatState`, o `ConversationalConfig` legado e os helpers de `crewai.flow.conversation` continuam disponíveis para importação em orquestração avançada, testes ou wrappers customizados. Eles são separados da API `ConversationState` / `ConversationConfig` e não adicionam os argumentos nomeados `user_message=` ou `session_id=` a `Flow.kickoff()`.
```python
from crewai.flow import ChatState
@@ -152,7 +143,7 @@ class MyChatState(ChatState):
`ConversationalInputs` é um `TypedDict` para as chaves convencionais de `kickoff(inputs={...})`: `id`, `user_message`, `last_intent`.
O `ConversationState` experimental armazena `messages` como objetos `ConversationMessage` e também fornece `current_user_message`, `ended`, `events` e `agent_threads`. Use `conversation_messages` ao passar seu histórico canônico para um LLM.
O `ConversationState` armazena `messages` como objetos `ConversationMessage` e também fornece `current_user_message`, `ended`, `events` e `agent_threads`. Use `conversation_messages` ao passar seu histórico canônico para um LLM.
## API conversacional em `Flow`
@@ -194,7 +185,7 @@ O `ConversationState` experimental armazena `messages` como objetos `Conversatio
### Helpers do módulo (`crewai.flow.conversation`)
Importáveis de `crewai.flow.conversation` para testes ou orquestração customizada. Esses helpers usam o formato legado de `ConversationalConfig`; `prepare_conversational_turn()` também limpa `last_intent`, ao contrário do `handle_turn()` experimental, que o preserva como contexto do router.
Importáveis de `crewai.flow.conversation` para testes ou orquestração customizada. Esses helpers usam o formato legado de `ConversationalConfig`; `prepare_conversational_turn()` também limpa `last_intent`, ao contrário do `handle_turn()`, que o preserva como contexto do router.
| Função | Descrição |
|----------|-------------|
@@ -238,7 +229,7 @@ Cada `handle_turn()` conclui uma execução do grafo, e a conversa continua com
Não use `@human_feedback` para linhas de chat de follow-up, a menos que um humano precise aprovar uma saída específica antes de exibi-la.
## `Flow` conversacional (experimental)
## `Flow` conversacional
Habilite o grafo de chat conversacional definindo `conversational = True` em uma subclasse de `Flow` ou aplicando `@ConversationConfig(...)`. O `Flow` base passa a fornecer `route_conversation` como start/router embutido, além dos listeners `converse_turn` e `end_conversation`. O listener descontinuado `answer_from_history_turn` permanece disponível para compatibilidade. O framework gerencia `state.messages`, pode acionar um LLM de roteamento e mantém o batch de trace aberto entre turnos. Você escreve as **rotas customizadas**; o framework cuida do resto.
@@ -249,7 +240,7 @@ Use isto quando quiser um chat multi-turno com router e handlers por rota sem ca
```python
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
)
@@ -335,7 +326,7 @@ from typing import Literal
from pydantic import BaseModel
from crewai import LLM
from crewai.experimental.conversational import RouterConfig
from crewai.flow import RouterConfig
class MyRoute(BaseModel):
@@ -474,7 +465,7 @@ Para rodar efeitos colaterais (setup de event bus, telemetria) em toda decisão
from typing import Any
from crewai import Flow
from crewai.experimental.conversational import ConversationState
from crewai.flow import ConversationState
class SupportFlow(Flow[ConversationState]):
@@ -582,7 +573,7 @@ flow.chat(session_id=session_id)
### Ciclo de vida de trace do `Flow` conversacional
O [`Flow` conversacional](#flow-conversacional-experimental) experimental usa o mesmo ciclo de vida de tracing: `defer_trace_finalization` é `True` por padrão, então cada `handle_turn()` mantém o trace da sessão aberto. Turnos adiados também suprimem `flow_failed` por turno; em caso de erro em um turno ou encerramento antecipado da sessão, finalize a sessão explicitamente. Isso fecha o batch com o evento `FlowFinished` no nível da sessão, em vez de um evento `FlowFailed` por turno. Sempre envolva seu REPL/loop em `try/finally` e chame `flow.finalize_session_traces()` na saída. Sem isso, o batch fica aberto e a conversa final pode nunca ser exportada.
O [`Flow` conversacional](#flow-conversacional) usa o mesmo ciclo de vida de tracing: `defer_trace_finalization` é `True` por padrão, então cada `handle_turn()` mantém o trace da sessão aberto. Turnos adiados também suprimem `flow_failed` por turno; em caso de erro em um turno ou encerramento antecipado da sessão, finalize a sessão explicitamente. Isso fecha o batch com o evento `FlowFinished` no nível da sessão, em vez de um evento `FlowFailed` por turno. Sempre envolva seu REPL/loop em `try/finally` e chame `flow.finalize_session_traces()` na saída. Sem isso, o batch fica aberto e a conversa final pode nunca ser exportada.
## Streaming
@@ -615,7 +606,7 @@ from crewai.flow import (
start,
)
from crewai.flow.conversation import prepare_conversational_turn
from crewai.experimental.conversational import (
from crewai.flow import (
ConversationConfig,
ConversationState,
RouterConfig,
@@ -151,7 +151,7 @@ Flows conversacionais podem transmitir um turno de usuário com `stream_turn()`:
```python
from crewai import Flow
from crewai.experimental.conversational import ConversationConfig, ConversationState
from crewai.flow import ConversationConfig, ConversationState
@ConversationConfig(llm="gpt-4o-mini", defer_trace_finalization=True)
+9 -14
View File
@@ -1,17 +1,14 @@
"""Experimental CrewAI surface — APIs here may change without major-version bumps."""
"""Experimental CrewAI surface and compatibility exports."""
from __future__ import annotations
from typing import Any
# ``crewai.experimental.conversational`` is pure data shapes — no Flow or Task
# imports — so it's safe to eager-import. Everything else is resolved lazily
# below; otherwise the chain
# crewai → Flow → experimental.conversational → experimental.__init__
# → experimental.agent_executor / experimental.evaluation
# → Flow / Task (mid-load)
# would deadlock with "partially initialized module" ImportErrors.
from crewai.experimental.conversational import (
# Conversational Flow graduated to ``crewai.flow``. Keep these exports so
# existing ``from crewai.experimental import ...`` imports continue to work.
# Everything still experimental is resolved lazily below to avoid Flow/Task
# import cycles.
from crewai.flow.conversational import (
AgentMessage,
ConversationConfig,
ConversationEvent,
@@ -47,11 +44,9 @@ _LAZY_FROM_EVALUATION = {
def __getattr__(name: str) -> Any:
"""Lazily resolve symbols whose modules import ``Flow`` or ``Task``.
Eager re-exports would deadlock when ``Flow`` itself is the consumer that
triggered ``crewai.experimental.__init__`` (``Flow`` imports types from
:mod:`crewai.experimental.conversational`). Callers like
``from crewai.experimental import AgentExecutor`` still work — the
real import just runs lazily, after the original loader finishes.
Eager re-exports would deadlock when ``Flow`` or ``Task`` is still loading.
Callers like ``from crewai.experimental import AgentExecutor`` still work —
the real import just runs lazily, after the original loader finishes.
"""
if name in _LAZY_FROM_AGENT_EXECUTOR:
from crewai.experimental.agent_executor import (
@@ -1,217 +1,8 @@
"""Conversational types and helpers shared by ``Flow`` (experimental).
"""Deprecated compatibility alias for :mod:`crewai.flow.conversational`."""
The conversational chat surface (``Flow`` with ``conversational = True``) is
EXPERIMENTAL. APIs in this module and the conversational methods on ``Flow``
may change without a major-version bump until the feature graduates.
import sys
This module hosts the **data shapes** — ``ConversationConfig``,
``RouterConfig``, ``ConversationState`` and its message types — plus the
``_conversational_only`` decorator used to gate built-in conversational
methods on the base ``Flow`` class. The methods themselves live on ``Flow``
directly.
"""
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Any, Literal, TypeVar, cast
from uuid import uuid4
import warnings
from pydantic import BaseModel, ConfigDict, Field
from crewai.utilities.types import LLMMessage
from crewai.flow import conversational as _real
ConversationMessageRole = Literal["user", "assistant", "system", "tool"]
ConversationEventVisibility = Literal["private", "public"]
F = TypeVar("F", bound=Callable[..., Any])
ANSWER_FROM_HISTORY_DEPRECATION_MESSAGE = (
"`answer_from_history_prompt` and `answer_from_history_llm` are deprecated "
"and will be removed in a future release. Use `ConversationConfig.llm` "
"with the built-in `converse` route, or override `converse_turn()` for "
"custom history behavior."
)
def _conversational_only(func: F) -> F:
"""Mark a method as part of the conversational built-in graph.
Methods carrying this marker only register on a ``Flow`` subclass when
``conversational = True``. Subclasses that don't opt in see them as
inert attributes — they don't fire and don't pollute the listener graph.
"""
func.__conversational_only__ = True # type: ignore[attr-defined]
return func
@dataclass
class RouterConfig:
"""LLM router configuration for the experimental conversational ``Flow``.
.. warning::
**EXPERIMENTAL.** Part of the conversational ``Flow`` surface. Fields
and defaults may change before the feature graduates from
``crewai.experimental``. Pin your CrewAI version if you depend on
a specific shape.
``route_descriptions`` overrides the per-route descriptions used to build
the router LLM's "available routes" catalog. Routes without an entry fall
back to the handler's docstring first line (or, for built-in routes, the
framework's canned description). ``prompt`` is reserved for domain
policy/voice, not the route catalog — that's auto-built.
"""
prompt: str | None = None
response_format: type[BaseModel] | None = None
llm: Any | None = None
routes: Sequence[str] | None = None
route_descriptions: dict[str, str] | None = None
default_intent: str | None = "converse"
fallback_intent: str | None = "converse"
intent_field: str = "intent"
@dataclass
class ConversationConfig:
"""Class-level configuration for the experimental conversational ``Flow``.
.. warning::
**EXPERIMENTAL.** Part of the conversational ``Flow`` surface. Fields
and defaults may change before the feature graduates from
``crewai.experimental``. Pin your CrewAI version if you depend on
a specific shape.
``system_prompt`` defaults to the ``slices.conversational_system_prompt``
translation when left as ``None``. Pass an empty string to opt out of any
system prompt for ``converse_turn``.
``answer_from_history_prompt`` and ``answer_from_history_llm`` are
deprecated. The built-in ``converse`` route already answers from canonical
message history without tools; override ``converse_turn`` when a separate
history policy is required.
"""
system_prompt: str | None = None
llm: Any | None = None
router: RouterConfig | None = None
answer_from_history_prompt: str | None = None
default_intents: Sequence[str] | None = None
intent_llm: Any | None = None
answer_from_history_llm: Any | None = None
visible_agent_outputs: Sequence[str] | Literal["all"] | None = None
defer_trace_finalization: bool = True
def __post_init__(self) -> None:
"""Warn when deprecated history-routing configuration is used."""
if (
self.answer_from_history_prompt is not None
or self.answer_from_history_llm is not None
):
warnings.warn(
ANSWER_FROM_HISTORY_DEPRECATION_MESSAGE,
DeprecationWarning,
stacklevel=3,
)
def __call__(self, flow_cls: type[Any]) -> type[Any]:
"""Use this config as a class decorator.
Decorating a Flow also opts it into conversational mode. Every field
here is consumed only by the conversational runtime, so a decorated Flow
that was not conversational would silently discard its whole config:
``handle_turn()`` would return ``None`` without appending or raising.
Setting ``conversational = True`` explicitly is still supported and is
the way to opt in without a config.
"""
flow_cls.conversational_config = self
flow_cls.conversational = True
return flow_cls
class ConversationMessage(BaseModel):
"""Canonical user-facing message shared across conversational turns."""
model_config = ConfigDict(arbitrary_types_allowed=True)
role: ConversationMessageRole
content: str | list[dict[str, Any]] | None
name: str | None = None
tool_call_id: str | None = None
tool_calls: list[dict[str, Any]] | None = None
files: dict[str, Any] | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class AgentMessage(BaseModel):
"""Private per-agent message or scratch result."""
role: ConversationMessageRole | str = "assistant"
content: Any
metadata: dict[str, Any] = Field(default_factory=dict)
class ConversationEvent(BaseModel):
"""Structured trace/event that is separate from user-visible messages."""
type: str
payload: dict[str, Any] = Field(default_factory=dict)
agent_name: str | None = None
visibility: ConversationEventVisibility = "private"
class ConversationState(BaseModel):
"""Structured state for the experimental conversational ``Flow``.
.. warning::
**EXPERIMENTAL.** Field shape and defaults may change before the
conversational ``Flow`` graduates from ``crewai.experimental``.
``messages`` is the canonical user-facing history. Agent/tool scratch work
belongs in ``events`` or ``agent_threads`` unless explicitly made public.
"""
id: str = Field(default_factory=lambda: str(uuid4()))
messages: list[ConversationMessage] = Field(default_factory=list)
current_user_message: str | None = None
last_user_message: str | None = None
last_intent: str | None = None
ended: bool = False
events: list[ConversationEvent] = Field(default_factory=list)
agent_threads: dict[str, list[AgentMessage]] = Field(default_factory=dict)
session_ready: bool = False
def message_to_llm_dict(message: Any) -> LLMMessage:
"""Coerce a stored ``ConversationMessage`` (or dict) into an ``LLMMessage``."""
if isinstance(message, BaseModel):
data = message.model_dump(exclude_none=True)
elif isinstance(message, dict):
data = dict(message)
else:
data = {"role": "user", "content": str(message)}
return cast(
LLMMessage,
{key: value for key, value in data.items() if key != "metadata"},
)
__all__ = [
"AgentMessage",
"ConversationConfig",
"ConversationEvent",
"ConversationEventVisibility",
"ConversationMessage",
"ConversationMessageRole",
"ConversationState",
"RouterConfig",
"_conversational_only",
"message_to_llm_dict",
]
sys.modules[__name__] = _real
File diff suppressed because it is too large Load Diff
+14
View File
@@ -9,6 +9,14 @@ from crewai.flow.conversation import (
ConversationalConfig,
ConversationalInputs,
)
from crewai.flow.conversational import (
AgentMessage,
ConversationConfig,
ConversationEvent,
ConversationMessage,
ConversationState,
RouterConfig,
)
from crewai.flow.dsl import HumanFeedbackResult, human_feedback
from crewai.flow.expressions import Expression
from crewai.flow.flow import Flow, and_, listen, or_, router, start
@@ -23,8 +31,13 @@ from crewai.flow.visualization import (
__all__ = [
"AgentMessage",
"ChatState",
"ConsoleProvider",
"ConversationConfig",
"ConversationEvent",
"ConversationMessage",
"ConversationState",
"ConversationalConfig",
"ConversationalInputs",
"Expression",
@@ -36,6 +49,7 @@ __all__ = [
"InputProvider",
"InputResponse",
"PendingFeedbackContext",
"RouterConfig",
"and_",
"build_flow_structure",
"flow_config",
@@ -0,0 +1,194 @@
"""Conversational types and helpers shared by ``Flow``.
This module hosts the **data shapes** — ``ConversationConfig``,
``RouterConfig``, ``ConversationState`` and its message types — plus the
``_conversational_only`` decorator used to gate built-in conversational
methods on the base ``Flow`` class. The methods themselves live on ``Flow``
directly.
"""
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Any, Literal, TypeVar, cast
from uuid import uuid4
import warnings
from pydantic import BaseModel, ConfigDict, Field
from crewai.utilities.types import LLMMessage
ConversationMessageRole = Literal["user", "assistant", "system", "tool"]
ConversationEventVisibility = Literal["private", "public"]
F = TypeVar("F", bound=Callable[..., Any])
ANSWER_FROM_HISTORY_DEPRECATION_MESSAGE = (
"`answer_from_history_prompt` and `answer_from_history_llm` are deprecated "
"and will be removed in a future release. Use `ConversationConfig.llm` "
"with the built-in `converse` route, or override `converse_turn()` for "
"custom history behavior."
)
def _conversational_only(func: F) -> F:
"""Mark a method as part of the conversational built-in graph.
Methods carrying this marker only register on a ``Flow`` subclass when
``conversational = True``. Subclasses that don't opt in see them as
inert attributes — they don't fire and don't pollute the listener graph.
"""
func.__conversational_only__ = True # type: ignore[attr-defined]
return func
@dataclass
class RouterConfig:
"""LLM router configuration for conversational ``Flow``.
``route_descriptions`` overrides the per-route descriptions used to build
the router LLM's "available routes" catalog. Routes without an entry fall
back to the handler's docstring first line (or, for built-in routes, the
framework's canned description). ``prompt`` is reserved for domain
policy/voice, not the route catalog — that's auto-built.
"""
prompt: str | None = None
response_format: type[BaseModel] | None = None
llm: Any | None = None
routes: Sequence[str] | None = None
route_descriptions: dict[str, str] | None = None
default_intent: str | None = "converse"
fallback_intent: str | None = "converse"
intent_field: str = "intent"
@dataclass
class ConversationConfig:
"""Class-level configuration for conversational ``Flow``.
``system_prompt`` defaults to the ``slices.conversational_system_prompt``
translation when left as ``None``. Pass an empty string to opt out of any
system prompt for ``converse_turn``.
``answer_from_history_prompt`` and ``answer_from_history_llm`` are
deprecated. The built-in ``converse`` route already answers from canonical
message history without tools; override ``converse_turn`` when a separate
history policy is required.
"""
system_prompt: str | None = None
llm: Any | None = None
router: RouterConfig | None = None
answer_from_history_prompt: str | None = None
default_intents: Sequence[str] | None = None
intent_llm: Any | None = None
answer_from_history_llm: Any | None = None
visible_agent_outputs: Sequence[str] | Literal["all"] | None = None
defer_trace_finalization: bool = True
def __post_init__(self) -> None:
"""Warn when deprecated history-routing configuration is used."""
if (
self.answer_from_history_prompt is not None
or self.answer_from_history_llm is not None
):
warnings.warn(
ANSWER_FROM_HISTORY_DEPRECATION_MESSAGE,
DeprecationWarning,
stacklevel=3,
)
def __call__(self, flow_cls: type[Any]) -> type[Any]:
"""Use this config as a class decorator.
Decorating a Flow also opts it into conversational mode. Every field
here is consumed only by the conversational runtime, so a decorated Flow
that was not conversational would silently discard its whole config:
``handle_turn()`` would return ``None`` without appending or raising.
Setting ``conversational = True`` explicitly is still supported and is
the way to opt in without a config.
"""
flow_cls.conversational_config = self
flow_cls.conversational = True
return flow_cls
class ConversationMessage(BaseModel):
"""Canonical user-facing message shared across conversational turns."""
model_config = ConfigDict(arbitrary_types_allowed=True)
role: ConversationMessageRole
content: str | list[dict[str, Any]] | None
name: str | None = None
tool_call_id: str | None = None
tool_calls: list[dict[str, Any]] | None = None
files: dict[str, Any] | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class AgentMessage(BaseModel):
"""Private per-agent message or scratch result."""
role: ConversationMessageRole | str = "assistant"
content: Any
metadata: dict[str, Any] = Field(default_factory=dict)
class ConversationEvent(BaseModel):
"""Structured trace/event that is separate from user-visible messages."""
type: str
payload: dict[str, Any] = Field(default_factory=dict)
agent_name: str | None = None
visibility: ConversationEventVisibility = "private"
class ConversationState(BaseModel):
"""Structured state for conversational ``Flow``.
``messages`` is the canonical user-facing history. Agent/tool scratch work
belongs in ``events`` or ``agent_threads`` unless explicitly made public.
"""
id: str = Field(default_factory=lambda: str(uuid4()))
messages: list[ConversationMessage] = Field(default_factory=list)
current_user_message: str | None = None
last_user_message: str | None = None
last_intent: str | None = None
ended: bool = False
events: list[ConversationEvent] = Field(default_factory=list)
agent_threads: dict[str, list[AgentMessage]] = Field(default_factory=dict)
session_ready: bool = False
def message_to_llm_dict(message: Any) -> LLMMessage:
"""Coerce a stored ``ConversationMessage`` (or dict) into an ``LLMMessage``."""
if isinstance(message, BaseModel):
data = message.model_dump(exclude_none=True)
elif isinstance(message, dict):
data = dict(message)
else:
data = {"role": "user", "content": str(message)}
return cast(
LLMMessage,
{key: value for key, value in data.items() if key != "metadata"},
)
__all__ = [
"AgentMessage",
"ConversationConfig",
"ConversationEvent",
"ConversationEventVisibility",
"ConversationMessage",
"ConversationMessageRole",
"ConversationState",
"RouterConfig",
"_conversational_only",
"message_to_llm_dict",
]
@@ -1,8 +1,8 @@
"""Static conversational Flow definition models.
This module is part of the serializable Flow Definition contract. It should
only contain static data shapes. Experimental conversational runtime behavior
continues to live in ``crewai.experimental.conversational_mixin``.
only contain static data shapes. Conversational runtime behavior lives in
``crewai.flow.conversational_mixin``.
"""
from __future__ import annotations
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -6,7 +6,7 @@ The implementation now lives in three modules, split by concern:
``@router``, ``or_`` / ``and_``) and Python Flow class projection
- ``crewai.flow.flow_definition`` -- the serializable Flow Definition contract
- ``crewai.flow.runtime`` -- the Flow execution engine and state
- ``crewai.experimental.conversational_mixin`` -- experimental conversational
- ``crewai.flow.conversational_mixin`` -- conversational
runtime extension composed onto the public ``Flow`` class
Prefer importing from those modules in new code; this module preserves the
@@ -17,7 +17,7 @@ from typing import Any, TypeVar
from pydantic import BaseModel
from crewai.experimental.conversational_mixin import _ConversationalMixin
from crewai.flow.conversational_mixin import _ConversationalMixin
from crewai.flow.dsl import and_, listen, or_, router, start
from crewai.flow.runtime import (
_INITIAL_STATE_CLASS_MARKER,
@@ -31,7 +31,7 @@ T = TypeVar("T", bound=dict[str, Any] | BaseModel)
class Flow(_ConversationalMixin, RuntimeFlow[T]):
"""Public Flow class with experimental conversational extension behavior."""
"""Public Flow class with conversational extension behavior."""
__all__ = [
@@ -211,7 +211,7 @@ def _normalize_agent_input(rendered: Any) -> str | list[LLMMessage]:
if isinstance(rendered, list) and all(
isinstance(item, Mapping) for item in rendered
):
from crewai.experimental.conversational import message_to_llm_dict
from crewai.flow.conversational import message_to_llm_dict
# ``message_to_llm_dict`` only drops ``None`` keys for a model input;
# a CEL render is plain dicts, so a serialized message keeps its
@@ -17,7 +17,7 @@ import time
import pytest
from crewai.experimental.conversational import ConversationConfig
from crewai.flow import ConversationConfig
from crewai.flow.async_feedback import HumanFeedbackPending, PendingFeedbackContext
from crewai.flow.flow import Flow, listen, start
from crewai.flow.human_feedback import human_feedback
@@ -0,0 +1,61 @@
"""The former experimental conversational imports remain compatible."""
from importlib import import_module
from pydoc import locate
from crewai.flow import (
AgentMessage,
ConversationConfig,
ConversationEvent,
ConversationMessage,
ConversationState,
RouterConfig,
)
from crewai.utilities.declarative_refs import resolve_ref
def test_experimental_conversational_module_aliases_stable_module() -> None:
experimental = import_module("crewai.experimental.conversational")
stable = import_module("crewai.flow.conversational")
assert experimental is stable
def test_experimental_namespace_reexports_stable_types() -> None:
from crewai.experimental import (
AgentMessage as ExperimentalAgentMessage,
ConversationConfig as ExperimentalConversationConfig,
ConversationEvent as ExperimentalConversationEvent,
ConversationMessage as ExperimentalConversationMessage,
ConversationState as ExperimentalConversationState,
RouterConfig as ExperimentalRouterConfig,
)
assert ExperimentalAgentMessage is AgentMessage
assert ExperimentalConversationConfig is ConversationConfig
assert ExperimentalConversationEvent is ConversationEvent
assert ExperimentalConversationMessage is ConversationMessage
assert ExperimentalConversationState is ConversationState
assert ExperimentalRouterConfig is RouterConfig
def test_experimental_conversational_mixin_aliases_stable_module() -> None:
experimental = import_module("crewai.experimental.conversational_mixin")
stable = import_module("crewai.flow.conversational_mixin")
assert experimental is stable
assert experimental._ConversationalMixin is stable._ConversationalMixin
def test_legacy_declarative_references_still_resolve() -> None:
assert (
resolve_ref(
"crewai.experimental.conversational:ConversationState",
field="state",
)
is ConversationState
)
assert (
locate("crewai.experimental.conversational.ConversationState")
is ConversationState
)
+11 -10
View File
@@ -27,20 +27,23 @@ from crewai.events.types.flow_events import (
MethodExecutionStartedEvent,
)
from crewai.events.types.llm_events import LLMCallStartedEvent, LLMStreamChunkEvent
from crewai.experimental import (
from crewai.flow import (
ChatState,
ConversationConfig,
ConversationMessage,
ConversationState,
Flow,
RouterConfig,
listen,
start,
)
from crewai.flow import Flow, ChatState, listen, start
from crewai.flow.persistence import SQLiteFlowPersistence, persist
from crewai.flow.async_feedback import HumanFeedbackPending, PendingFeedbackContext
from crewai.flow.flow_context import (
current_flow_defer_trace_finalization,
current_flow_id,
current_flow_name,
)
from crewai.flow.persistence import SQLiteFlowPersistence, persist
from crewai.llms.base_llm import BaseLLM
from crewai.flow.conversation import (
append_message,
@@ -1028,10 +1031,10 @@ class TestConversationalFlow:
"""``Flow`` mixes in ``_ConversationalMixin`` — opt-in subclasses get its methods.
The conversational graph + ``handle_turn`` live on the mixin in
``crewai.experimental.conversational_mixin``; this test confirms
``crewai.flow.conversational_mixin``; this test confirms
MRO resolution wires them onto a ``Flow`` subclass that opts in.
"""
from crewai.experimental.conversational_mixin import _ConversationalMixin
from crewai.flow.conversational_mixin import _ConversationalMixin
@ConversationConfig()
class MyChat(Flow):
@@ -2398,7 +2401,7 @@ class TestHandleTurnGuard:
assert DecoratedFlow().handle_turn("hi") == "ok"
_MIXIN = "crewai.experimental.conversational_mixin:_ConversationalMixin"
_MIXIN = "crewai.flow.conversational_mixin:_ConversationalMixin"
class DeclaredChatState(ConversationState):
@@ -2447,7 +2450,7 @@ def _conversational_declaration(**overrides: Any) -> dict[str, Any]:
"name": "DeclaredChat",
"state": {
"type": "pydantic",
"ref": "crewai.experimental.conversational:ConversationState",
"ref": "crewai.flow.conversational:ConversationState",
},
"conversational": {},
"methods": {
@@ -2764,9 +2767,7 @@ class TestDeclaredRouterResponseFormat:
projected = ClassChat.flow_definition().conversational.router.response_format
assert projected is not None
assert projected.python == (
"crewai.experimental.conversational.ConversationState"
)
assert projected.python == "crewai.flow.conversational.ConversationState"
class DeclaredSchemaChatState(ConversationState):
+12 -2
View File
@@ -14,12 +14,22 @@ from pydantic import BaseModel, ValidationError
import crewai.flow.dsl as flow_dsl
import crewai.flow.flow_definition as flow_definition
import crewai.flow.visualization.builder as visualization_builder
from crewai.experimental import ConversationConfig, RouterConfig
from crewai.flow.expressions import (
FLOW_TEMPLATE_EXPRESSION_EXAMPLES,
FLOW_TEMPLATE_EXPRESSION_RULES,
)
from crewai.flow import Flow, and_, human_feedback, listen, or_, persist, router, start
from crewai.flow import (
ConversationConfig,
Flow,
RouterConfig,
and_,
human_feedback,
listen,
or_,
persist,
router,
start,
)
def test_flow_public_exports_are_explicit():