diff --git a/docs/edge/ar/guides/flows/conversational-flows.mdx b/docs/edge/ar/guides/flows/conversational-flows.mdx index 371de79be..e96814b82 100644 --- a/docs/edge/ar/guides/flows/conversational-flows.mdx +++ b/docs/edge/ar/guides/flows/conversational-flows.mdx @@ -426,6 +426,70 @@ class SupportFlow(Flow[ConversationState]): يمكن لـ `ConversationConfig.visible_agent_outputs` رفع النتائج الخاصة لـ agents محددين إلى عامة عالمياً (`"all"` أو قائمة بالأسماء). +## تعريف تدفق محادثاتي بصيغة JSON/YAML + +يمكن لـ [التدفق التعريفي](/edge/en/concepts/cli) أن يكون محادثاتيًا أيضًا. أضف كتلة `conversational` في المستوى الأعلى وعرّف مساراتك الخاصة كطرق تستمع (`listen`) إلى تسمية مسار: + +```yaml +schema: crewai.flow/v1 +name: SupportFlow + +conversational: + system_prompt: You are a terse support assistant. + llm: gpt-4o-mini + router: + llm: gpt-4o-mini + +methods: + handle_order: + description: Order status, shipping and delivery questions. + listen: order + do: + call: agent + with: + role: Support specialist + goal: Answer order questions accurately + backstory: Knows the fulfilment pipeline. + input: "${state.current_user_message}" +``` + +تعريف الكتلة هو الاشتراك نفسه — القيمة الافتراضية لـ `enabled` هي `true`. اضبطها على `enabled: false` للاحتفاظ بالإعدادات مع إيقاف المحادثة. + +تُوفَّر لك ثلاثة أشياء: + +| المُوفَّر | التفاصيل | +|----------|--------| +| الرسم البياني المدمج | تُضاف `route_conversation` و`converse_turn` و`end_conversation` و`answer_from_history_turn` تلقائيًا. عرّف طريقة بأحد هذه الأسماء لتجاوزها. | +| حالة المحادثة | تُستخدم `ConversationState` عندما لا تحتوي التعريفة على كتلة `state`. لإضافة حقول، وجّه `state` إلى نموذج Pydantic يرث من `ConversationState`. | +| كتالوج المسارات | يُبنى من الطرق التي تعلن تسمية `listen`. وصف كل طريقة (`description`) هو ما يقرأه نموذج التوجيه عند الاختيار بين المسارات. | + +شغّله من Python بنفس واجهات الجولة المستخدمة مع تدفق محادثاتي معرّف بصنف: + +```python +from crewai.flow import Flow + +flow = Flow.from_declaration(path="flow.yaml") + +try: + flow.handle_turn("Where is my order?", session_id="session-1") +finally: + flow.finalize_session_traces() +``` + +### تسمية المسارات + +تتشارك تسميات المسارات وأسماء الطرق مساحة اسم واحدة للمشغّلات، لذا يجب ألا يحمل المعالج اسم المسار الذي يستمع إليه — يُرفض `create_video` الذي يستمع إلى `create_video` عند بناء التدفق. استخدم بادئة `handle_*`. + +### ما لا يمكن للتعريفة التعبير عنه + +| غير قابل للتعبير | استخدم بدلًا منه | +|-----------------|-------------| +| مثيل `LLM` حي أو `BaseLLM` مخصص | سلسلة معرّف النموذج، مثل `gpt-4o-mini` | +| `router.response_format` كصنف نموذج | احذفه؛ يولّد الإطار واحدًا. يُتجاهل المرجع أو المخطط مع تحذير | +| تجاوزات `route_turn()` / `can_answer_from_history()` | اكتب التدفق بلغة Python، أو وجّه `do` لطريقة إلى مرجع `call: code` | + +لا يملك `crewai run` حلقة محادثة بعد: فهو يبلّغ أن التدفق محادثاتي ويخرج بدلًا من تنفيذ جولة واحدة. شغّل التدفق المحادثاتي التعريفي من Python عبر `handle_turn()` أو `stream_turn()` أو `chat()`. + ## التتبع عبر الجولات مع `defer_trace_finalization=True` (افتراضي في `ConversationalConfig`): diff --git a/docs/edge/en/guides/flows/conversational-flows.mdx b/docs/edge/en/guides/flows/conversational-flows.mdx index b8fd7f43d..e42c4966b 100644 --- a/docs/edge/en/guides/flows/conversational-flows.mdx +++ b/docs/edge/en/guides/flows/conversational-flows.mdx @@ -491,6 +491,70 @@ Inside a `@listen(label)` handler, choose: `ConversationConfig.visible_agent_outputs` can promote specific agents' private results to public globally (`"all"`, or a list of agent names). +## Declaring a conversational flow in JSON/YAML + +A [declarative Flow](/edge/en/concepts/cli) can be conversational too. Add a top-level `conversational` block and declare your own routes as methods that `listen` to a route label: + +```yaml +schema: crewai.flow/v1 +name: SupportFlow + +conversational: + system_prompt: You are a terse support assistant. + llm: gpt-4o-mini + router: + llm: gpt-4o-mini + +methods: + handle_order: + description: Order status, shipping and delivery questions. + listen: order + do: + call: agent + with: + role: Support specialist + goal: Answer order questions accurately + backstory: Knows the fulfilment pipeline. + input: "${state.current_user_message}" +``` + +Declaring the block is the opt-in — `enabled` defaults to `true`. Set `enabled: false` to keep the configuration while turning chat off. + +Three things are supplied for you: + +| Supplied | Detail | +|----------|--------| +| The built-in graph | `route_conversation`, `converse_turn`, `end_conversation` and `answer_from_history_turn` are added automatically. Declare a method under one of those names to override it. | +| Conversation state | `ConversationState` is used when the declaration has no `state` block. To add fields, point `state` at a Pydantic model that extends `ConversationState`. | +| The route catalog | Built from the methods that declare a `listen` label. Each method's `description` is what the routing model reads when choosing between routes. | + +Run it from Python with the same turn APIs as a class-based conversational Flow: + +```python +from crewai.flow import Flow + +flow = Flow.from_declaration(path="flow.yaml") + +try: + flow.handle_turn("Where is my order?", session_id="session-1") +finally: + flow.finalize_session_traces() +``` + +### Naming routes + +Route labels and method names share one trigger namespace, so a handler must not be named after the route it listens to — `create_video` listening to `create_video` is rejected when the flow is built. Use a `handle_*` prefix. + +### What a declaration cannot express + +| Not expressible | Use instead | +|-----------------|-------------| +| A live `LLM` instance or a custom `BaseLLM` | A model id string, such as `gpt-4o-mini` | +| `router.response_format` as a model class | Omit it; the framework synthesizes one. A ref or schema is ignored with a warning | +| `route_turn()` / `can_answer_from_history()` overrides | Author the Flow in Python, or point a method's `do` at a `call: code` ref | + +`crewai run` has no chat loop yet: it reports that the flow is conversational and exits rather than running a single turn. Drive a declarative conversational flow from Python with `handle_turn()`, `stream_turn()` or `chat()`. + ## Tracing across turns With `defer_trace_finalization=True` (default in `ConversationConfig`): diff --git a/docs/edge/ko/guides/flows/conversational-flows.mdx b/docs/edge/ko/guides/flows/conversational-flows.mdx index 3a18cd1da..fd3a7c256 100644 --- a/docs/edge/ko/guides/flows/conversational-flows.mdx +++ b/docs/edge/ko/guides/flows/conversational-flows.mdx @@ -427,6 +427,70 @@ LLM router를 우회해 프로그램적으로 라우트를 선택하려면 `rout `ConversationConfig.visible_agent_outputs`로 특정 에이전트의 private 결과를 전역적으로 public으로 승격할 수 있습니다 (`"all"` 또는 이름 리스트). +## JSON/YAML로 대화형 플로우 선언하기 + +[선언적 플로우](/edge/en/concepts/cli)도 대화형이 될 수 있습니다. 최상위 `conversational` 블록을 추가하고, 라우트 레이블을 `listen`하는 메서드로 직접 라우트를 선언하세요: + +```yaml +schema: crewai.flow/v1 +name: SupportFlow + +conversational: + system_prompt: You are a terse support assistant. + llm: gpt-4o-mini + router: + llm: gpt-4o-mini + +methods: + handle_order: + description: Order status, shipping and delivery questions. + listen: order + do: + call: agent + with: + role: Support specialist + goal: Answer order questions accurately + backstory: Knows the fulfilment pipeline. + input: "${state.current_user_message}" +``` + +블록을 선언하는 것 자체가 옵트인입니다 — `enabled`의 기본값은 `true`입니다. 설정은 유지하면서 채팅만 끄려면 `enabled: false`로 지정하세요. + +세 가지가 자동으로 제공됩니다: + +| 제공 항목 | 설명 | +|----------|--------| +| 내장 그래프 | `route_conversation`, `converse_turn`, `end_conversation`, `answer_from_history_turn`이 자동으로 추가됩니다. 같은 이름의 메서드를 선언하면 재정의됩니다. | +| 대화 상태 | 선언에 `state` 블록이 없으면 `ConversationState`가 사용됩니다. 필드를 추가하려면 `ConversationState`를 상속한 Pydantic 모델을 `state`에 지정하세요. | +| 라우트 카탈로그 | `listen` 레이블을 선언한 메서드들로부터 구성됩니다. 각 메서드의 `description`이 라우팅 모델이 라우트를 선택할 때 읽는 내용입니다. | + +클래스 기반 대화형 플로우와 동일한 턴 API로 Python에서 실행합니다: + +```python +from crewai.flow import Flow + +flow = Flow.from_declaration(path="flow.yaml") + +try: + flow.handle_turn("Where is my order?", session_id="session-1") +finally: + flow.finalize_session_traces() +``` + +### 라우트 이름 짓기 + +라우트 레이블과 메서드 이름은 하나의 트리거 네임스페이스를 공유하므로, 핸들러 이름이 자신이 listen하는 라우트와 같으면 안 됩니다 — `create_video`가 `create_video`를 listen하면 플로우 생성 시 거부됩니다. `handle_*` 접두사를 사용하세요. + +### 선언으로 표현할 수 없는 것 + +| 표현 불가 | 대신 사용 | +|-----------------|-------------| +| 살아 있는 `LLM` 인스턴스나 커스텀 `BaseLLM` | `gpt-4o-mini` 같은 모델 ID 문자열 | +| 모델 클래스로서의 `router.response_format` | 생략하세요; 프레임워크가 생성합니다. ref나 스키마는 경고와 함께 무시됩니다 | +| `route_turn()` / `can_answer_from_history()` 재정의 | 플로우를 Python으로 작성하거나, 메서드의 `do`를 `call: code` ref로 지정하세요 | + +`crewai run`에는 아직 채팅 루프가 없습니다: 단일 턴을 실행하는 대신 플로우가 대화형임을 알리고 종료합니다. 선언적 대화형 플로우는 Python에서 `handle_turn()`, `stream_turn()`, `chat()`으로 실행하세요. + ## 턴 간 트레이싱 `defer_trace_finalization=True` (`ConversationalConfig` 기본값): diff --git a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx index 10ffdcbd6..9012bbe6e 100644 --- a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx +++ b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx @@ -428,6 +428,70 @@ Dentro de um handler `@listen(label)`, escolha: `ConversationConfig.visible_agent_outputs` pode promover globalmente os resultados privados de agentes específicos para públicos (`"all"` ou lista de nomes). +## Declarando um flow conversacional em JSON/YAML + +Um [flow declarativo](/edge/en/concepts/cli) também pode ser conversacional. Adicione um bloco `conversational` no nível raiz e declare suas próprias rotas como métodos que escutam (`listen`) um rótulo de rota: + +```yaml +schema: crewai.flow/v1 +name: SupportFlow + +conversational: + system_prompt: You are a terse support assistant. + llm: gpt-4o-mini + router: + llm: gpt-4o-mini + +methods: + handle_order: + description: Order status, shipping and delivery questions. + listen: order + do: + call: agent + with: + role: Support specialist + goal: Answer order questions accurately + backstory: Knows the fulfilment pipeline. + input: "${state.current_user_message}" +``` + +Declarar o bloco já é o opt-in — `enabled` tem valor padrão `true`. Use `enabled: false` para manter a configuração e desligar o chat. + +Três coisas são fornecidas para você: + +| Fornecido | Detalhe | +|----------|--------| +| O grafo interno | `route_conversation`, `converse_turn`, `end_conversation` e `answer_from_history_turn` são adicionados automaticamente. Declare um método com um desses nomes para sobrescrevê-lo. | +| Estado da conversa | `ConversationState` é usado quando a declaração não tem bloco `state`. Para adicionar campos, aponte `state` para um modelo Pydantic que estenda `ConversationState`. | +| O catálogo de rotas | Construído a partir dos métodos que declaram um rótulo `listen`. O `description` de cada método é o que o modelo de roteamento lê ao escolher entre rotas. | + +Execute a partir do Python com as mesmas APIs de turno de um Flow conversacional baseado em classe: + +```python +from crewai.flow import Flow + +flow = Flow.from_declaration(path="flow.yaml") + +try: + flow.handle_turn("Where is my order?", session_id="session-1") +finally: + flow.finalize_session_traces() +``` + +### Nomeando rotas + +Rótulos de rota e nomes de métodos compartilham um único namespace de gatilhos, então um handler não pode ter o nome da rota que escuta — `create_video` escutando `create_video` é rejeitado na construção do flow. Use o prefixo `handle_*`. + +### O que uma declaração não consegue expressar + +| Não expressável | Use no lugar | +|-----------------|-------------| +| Uma instância `LLM` viva ou um `BaseLLM` customizado | Uma string de id de modelo, como `gpt-4o-mini` | +| `router.response_format` como classe de modelo | Omita; o framework sintetiza uma. Um ref ou schema é ignorado com um aviso | +| Overrides de `route_turn()` / `can_answer_from_history()` | Escreva o Flow em Python, ou aponte o `do` de um método para um ref `call: code` | + +O `crewai run` ainda não tem loop de chat: ele informa que o flow é conversacional e sai, em vez de rodar um único turno. Conduza um flow conversacional declarativo pelo Python com `handle_turn()`, `stream_turn()` ou `chat()`. + ## Tracing entre turnos Com `defer_trace_finalization=True` (padrão em `ConversationalConfig`): diff --git a/lib/cli/src/crewai_cli/run_declarative_flow.py b/lib/cli/src/crewai_cli/run_declarative_flow.py index 927965f59..0b7cb8cd2 100644 --- a/lib/cli/src/crewai_cli/run_declarative_flow.py +++ b/lib/cli/src/crewai_cli/run_declarative_flow.py @@ -72,6 +72,18 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N provided = parse_inputs_json(inputs) or {} flow = load_declarative_flow(definition) + + if _flow_is_conversational(flow): + click.secho( + " This flow declares `conversational`, and `crewai run` has no chat " + "loop yet — it would run a single turn and exit.\n" + " Drive it from Python for now: `flow.chat()` for a terminal REPL, " + "or `flow.handle_turn(message, session_id=...)` per message.", + fg="yellow", + err=True, + ) + raise SystemExit(1) + resolved_inputs = _resolve_flow_inputs(flow, provided) # The TUI is the interactive default. Headless contexts run directly on the @@ -156,6 +168,20 @@ def _run_declarative_flow_tui( return app._crew_result +def _flow_is_conversational(flow: Flow[Any]) -> bool: + """True if the declaration turns on conversational mode. + + Fails closed: a flow we cannot inspect runs the normal single-kickoff path + rather than being blocked from running at all. + """ + try: + conversational = flow._definition.conversational + except AttributeError: + logger.debug("Could not inspect flow for conversational mode", exc_info=True) + return False + return conversational is not None and conversational.enabled + + def _flow_uses_human_feedback(flow: Flow[Any]) -> bool: """True if any declarative method declares ``@human_feedback``. diff --git a/lib/cli/tests/test_run_declarative_flow.py b/lib/cli/tests/test_run_declarative_flow.py index 378156725..32df4be7a 100644 --- a/lib/cli/tests/test_run_declarative_flow.py +++ b/lib/cli/tests/test_run_declarative_flow.py @@ -611,3 +611,48 @@ def test_flow_method_types_from_definition() -> None: } # No definition → empty map, no error. assert run_declarative_flow_module._flow_method_types(SimpleNamespace()) == {} + + +CONVERSATIONAL_FLOW_YAML = """schema: crewai.flow/v1 +name: SupportFlow +conversational: + llm: gpt-4o-mini +methods: + handle_order: + description: Order status questions. + listen: order + do: + call: expression + expr: "'shipped'" +""" + + +def test_run_declarative_flow_refuses_a_conversational_flow( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + definition_path = tmp_path / "flow.yaml" + definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8") + + with pytest.raises(SystemExit): + run_declarative_flow_module.run_declarative_flow(str(definition_path)) + + err = capsys.readouterr().err + assert "has no chat loop yet" in err + assert "flow.chat()" in err + + +def test_run_declarative_flow_still_runs_a_disabled_conversational_flow( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + definition_path = tmp_path / "flow.yaml" + definition_path.write_text( + CONVERSATIONAL_FLOW_YAML.replace( + "conversational:\n llm: gpt-4o-mini", + "conversational:\n enabled: false", + ).replace(" listen: order\n", " start: true\n"), + encoding="utf-8", + ) + + run_declarative_flow_module.run_declarative_flow(str(definition_path)) + + assert capsys.readouterr().out == "shipped\n" diff --git a/lib/crewai/src/crewai/flow/skill.py b/lib/crewai/src/crewai/flow/skill.py index ad2862197..ca26113e8 100644 --- a/lib/crewai/src/crewai/flow/skill.py +++ b/lib/crewai/src/crewai/flow/skill.py @@ -26,6 +26,8 @@ SKIP_BY_MODEL: dict[str, str] = { "FlowConfigDefinition": "config", "FlowHumanFeedbackDefinition": "hitl", "FlowPersistenceDefinition": "persistence", + "FlowConversationalDefinition": "conversational", + "FlowConversationalRouterDefinition": "conversational", } FIELD_TYPE_OVERRIDES: dict[tuple[str, str], str] = { @@ -138,6 +140,8 @@ MODEL_TITLES = { "FlowConfigDefinition": "Config", "FlowPersistenceDefinition": "Persistence", "FlowHumanFeedbackDefinition": "Human Feedback", + "FlowConversationalDefinition": "Conversational", + "FlowConversationalRouterDefinition": "Conversational Router", } @@ -280,6 +284,35 @@ MODEL_SPECS: tuple[ModelSpec, ...] = ( "Human Feedback", "methods..human_feedback", ), + ModelSpec( + "FlowConversationalDefinition", + "Conversational", + "conversational", + descriptions={ + "enabled": "Whether conversational mode is active. Declaring the conversational block is the opt-in, so leave this out unless you are turning chat off with `false`.", + "llm": "Model id used by the built-in `converse` handler, and the router's fallback model.", + "system_prompt": "System message for the built-in `converse` handler. Omit for the framework default; use an empty string for none.", + "default_intents": "Outcome labels classified before routing. Requires `intent_llm`.", + "answer_from_history_llm": "Setting this enables the optional `answer_from_history` route, which answers from the transcript without invoking a route handler.", + "visible_agent_outputs": "Agent names whose recorded results are promoted to user-visible assistant messages, or `all`.", + "builtin_routes": "Route labels the framework handles itself. Do not add your own routes here; declare them as a method `listen` label.", + "internal_routes": "Route labels the framework handles that are excluded from the router's catalog.", + }, + examples=True, + ), + ModelSpec( + "FlowConversationalRouterDefinition", + "Conversational Router", + "conversational.router", + descriptions={ + "prompt": "Domain framing for the routing decision: persona, policy, voice. Do not list routes here; the catalog is built automatically.", + "routes": "Route labels the router may choose. Omit to infer them from the methods that declare a `listen` label.", + "route_descriptions": "Per-route text for the router's catalog. A method's `description` is used when a route has no entry here.", + "default_intent": "Route used when no routing model is configured or the call fails.", + "fallback_intent": "Route used when the model returns a label that is not in the catalog.", + "response_format": "Leave this out in a declaration. A declaration cannot carry a model class, so it is ignored with a warning and the framework synthesizes one.", + }, + ), ) _SPECS_BY_NAME: dict[str, ModelSpec] = {spec.name: spec for spec in MODEL_SPECS} diff --git a/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 index 987c252e2..5b270107d 100644 --- a/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 +++ b/lib/crewai/src/crewai/flow/templates/flow_definition_skill.md.j2 @@ -148,6 +148,10 @@ Dynamic value rules: - Do not set `config.stream: true` unless the caller is expected to consume a streaming result. For normal generated flows and CLI smoke tests, omit it. {% if include_conversational %} - Do not put conversational settings under `state`, `config`, or a method. Use top-level `conversational` only when the user asks for a chat or conversation flow. +- Do not declare `route_conversation`, `converse_turn`, `end_conversation`, or `answer_from_history_turn` unless you are deliberately replacing one. Declaring the `conversational` block adds them for you, and a method you declare under one of those names overrides the built-in. +- Do not name a route handler the same as the route it listens to. Route labels and method names share one trigger namespace, so `create_video` listening to `create_video` is rejected. Use `handle_create_video`. +- Do not declare `state` for a chat flow unless it needs extra fields. The conversational state shape is supplied by default, and a custom state must extend it. +- Do describe every custom route, either with the handler's `description` or an entry in `conversational.router.route_descriptions`. That text is what the routing model reads to choose between routes; a route with neither is offered to it unlabelled. {% endif %} {% if include_each_action %} - Do not use `each` without at least one named step. diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index 8a714676d..13a5c7cca 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -5,6 +5,7 @@ import importlib import inspect import logging from pathlib import Path +import re from typing import Annotated, Literal import pytest @@ -1479,6 +1480,40 @@ def test_skill_documents_flow_wiring(): assert example["code"] in skill +def test_skill_renders_both_conversational_sections(): + """Both models must render; the router shares no section with its parent. + + Non-union sections render only their first model, so grouping them would + drop the router and leave the link to it without a target. + """ + skill = flow_definition.FlowDefinition.skill() + + assert "### Conversational (`conversational`)" in skill + assert "### Conversational Router (`conversational.router`)" in skill + + link = "[Conversational Router (`conversational.router`)]" + assert link in skill + anchor = re.search(re.escape(link) + r"\((#[^)]+)\)", skill).group(1) + assert anchor == "#conversational-router-conversationalrouter" + + +def test_skill_documents_every_conversational_field(): + skill = flow_definition.FlowDefinition.skill() + section = skill[skill.index("### Conversational (`conversational`)") :] + + for field in flow_definition.FlowConversationalDefinition.model_fields: + assert f"`{field}`" in section, field + for field in flow_definition.FlowConversationalRouterDefinition.model_fields: + assert f"`{field}`" in section, field + + +def test_skill_conversational_skip_suppresses_both_sections(): + skill = flow_definition.FlowDefinition.skill(skips=["conversational"]) + + assert "### Conversational" not in skill + assert "conversational-router" not in skill + + def test_skill_can_render_json_examples(): skill = flow_definition.FlowDefinition.skill(examples_format="json")