mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-10-12 02:12:31 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user