mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-01 12:18:31 +00:00
Compare commits
4 Commits
main
...
heitor/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd5c46b42 | ||
|
|
97fed5229f | ||
|
|
0f28d14e61 | ||
|
|
b4b6434480 |
20
README.md
20
README.md
@@ -57,7 +57,7 @@
|
||||
> It empowers developers with both high-level simplicity and precise low-level control, ideal for creating autonomous AI agents tailored to any scenario.
|
||||
|
||||
- **CrewAI Crews**: Optimize for autonomy and collaborative intelligence.
|
||||
- **CrewAI Flows**: The **enterprise and production architecture** for building and deploying multi-agent systems. Enable granular, event-driven control, single LLM calls for precise task orchestration and supports Crews natively
|
||||
- **CrewAI Flows**: Enable granular, event-driven control, single LLM calls for precise task orchestration and supports Crews natively
|
||||
|
||||
With over 100,000 developers certified through our community courses at [learn.crewai.com](https://learn.crewai.com), CrewAI is rapidly becoming the
|
||||
standard for enterprise-ready AI automation.
|
||||
@@ -166,13 +166,13 @@ Ensure you have Python >=3.10 <3.14 installed on your system. CrewAI uses [UV](h
|
||||
First, install CrewAI:
|
||||
|
||||
```shell
|
||||
uv pip install crewai
|
||||
pip install crewai
|
||||
```
|
||||
|
||||
If you want to install the 'crewai' package along with its optional features that include additional tools for agents, you can do so by using the following command:
|
||||
|
||||
```shell
|
||||
uv pip install 'crewai[tools]'
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
The command above installs the basic package and also adds extra components which require more dependencies to function.
|
||||
@@ -185,14 +185,14 @@ If you encounter issues during installation or usage, here are some common solut
|
||||
|
||||
1. **ModuleNotFoundError: No module named 'tiktoken'**
|
||||
|
||||
- Install tiktoken explicitly: `uv pip install 'crewai[embeddings]'`
|
||||
- If using embedchain or other tools: `uv pip install 'crewai[tools]'`
|
||||
- Install tiktoken explicitly: `pip install 'crewai[embeddings]'`
|
||||
- If using embedchain or other tools: `pip install 'crewai[tools]'`
|
||||
2. **Failed building wheel for tiktoken**
|
||||
|
||||
- Ensure Rust compiler is installed (see installation steps above)
|
||||
- For Windows: Verify Visual C++ Build Tools are installed
|
||||
- Try upgrading pip: `uv pip install --upgrade pip`
|
||||
- If issues persist, use a pre-built wheel: `uv pip install tiktoken --prefer-binary`
|
||||
- Try upgrading pip: `pip install --upgrade pip`
|
||||
- If issues persist, use a pre-built wheel: `pip install tiktoken --prefer-binary`
|
||||
|
||||
### 2. Setting Up Your Crew with the YAML Configuration
|
||||
|
||||
@@ -611,7 +611,7 @@ uv build
|
||||
### Installing Locally
|
||||
|
||||
```bash
|
||||
uv pip install dist/*.tar.gz
|
||||
pip install dist/*.tar.gz
|
||||
```
|
||||
|
||||
## Telemetry
|
||||
@@ -687,13 +687,13 @@ A: CrewAI is a standalone, lean, and fast Python framework built specifically fo
|
||||
A: Install CrewAI using pip:
|
||||
|
||||
```shell
|
||||
uv pip install crewai
|
||||
pip install crewai
|
||||
```
|
||||
|
||||
For additional tools, use:
|
||||
|
||||
```shell
|
||||
uv pip install 'crewai[tools]'
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
### Q: Does CrewAI depend on LangChain?
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
"en/concepts/tasks",
|
||||
"en/concepts/crews",
|
||||
"en/concepts/flows",
|
||||
"en/concepts/production-architecture",
|
||||
"en/concepts/knowledge",
|
||||
"en/concepts/llms",
|
||||
"en/concepts/processes",
|
||||
@@ -309,7 +308,6 @@
|
||||
"en/learn/hierarchical-process",
|
||||
"en/learn/human-input-on-execution",
|
||||
"en/learn/human-in-the-loop",
|
||||
"en/learn/human-feedback-in-flows",
|
||||
"en/learn/kickoff-async",
|
||||
"en/learn/kickoff-for-each",
|
||||
"en/learn/llm-connections",
|
||||
@@ -560,7 +558,6 @@
|
||||
"pt-BR/concepts/tasks",
|
||||
"pt-BR/concepts/crews",
|
||||
"pt-BR/concepts/flows",
|
||||
"pt-BR/concepts/production-architecture",
|
||||
"pt-BR/concepts/knowledge",
|
||||
"pt-BR/concepts/llms",
|
||||
"pt-BR/concepts/processes",
|
||||
@@ -705,7 +702,6 @@
|
||||
{
|
||||
"group": "Observabilidade",
|
||||
"pages": [
|
||||
"pt-BR/observability/tracing",
|
||||
"pt-BR/observability/overview",
|
||||
"pt-BR/observability/arize-phoenix",
|
||||
"pt-BR/observability/braintrust",
|
||||
@@ -739,7 +735,6 @@
|
||||
"pt-BR/learn/hierarchical-process",
|
||||
"pt-BR/learn/human-input-on-execution",
|
||||
"pt-BR/learn/human-in-the-loop",
|
||||
"pt-BR/learn/human-feedback-in-flows",
|
||||
"pt-BR/learn/kickoff-async",
|
||||
"pt-BR/learn/kickoff-for-each",
|
||||
"pt-BR/learn/llm-connections",
|
||||
@@ -987,7 +982,6 @@
|
||||
"ko/concepts/tasks",
|
||||
"ko/concepts/crews",
|
||||
"ko/concepts/flows",
|
||||
"ko/concepts/production-architecture",
|
||||
"ko/concepts/knowledge",
|
||||
"ko/concepts/llms",
|
||||
"ko/concepts/processes",
|
||||
@@ -1144,7 +1138,6 @@
|
||||
{
|
||||
"group": "Observability",
|
||||
"pages": [
|
||||
"ko/observability/tracing",
|
||||
"ko/observability/overview",
|
||||
"ko/observability/arize-phoenix",
|
||||
"ko/observability/braintrust",
|
||||
@@ -1178,7 +1171,6 @@
|
||||
"ko/learn/hierarchical-process",
|
||||
"ko/learn/human-input-on-execution",
|
||||
"ko/learn/human-in-the-loop",
|
||||
"ko/learn/human-feedback-in-flows",
|
||||
"ko/learn/kickoff-async",
|
||||
"ko/learn/kickoff-for-each",
|
||||
"ko/learn/llm-connections",
|
||||
|
||||
@@ -572,55 +572,6 @@ The `third_method` and `fourth_method` listen to the output of the `second_metho
|
||||
|
||||
When you run this Flow, the output will change based on the random boolean value generated by the `start_method`.
|
||||
|
||||
### Human in the Loop (human feedback)
|
||||
|
||||
The `@human_feedback` decorator enables human-in-the-loop workflows by pausing flow execution to collect feedback from a human. This is useful for approval gates, quality review, and decision points that require human judgment.
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback, HumanFeedbackResult
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Do you approve this content?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def generate_content(self):
|
||||
return "Content to be reviewed..."
|
||||
|
||||
@listen("approved")
|
||||
def on_approval(self, result: HumanFeedbackResult):
|
||||
print(f"Approved! Feedback: {result.feedback}")
|
||||
|
||||
@listen("rejected")
|
||||
def on_rejection(self, result: HumanFeedbackResult):
|
||||
print(f"Rejected. Reason: {result.feedback}")
|
||||
```
|
||||
|
||||
When `emit` is specified, the human's free-form feedback is interpreted by an LLM and collapsed into one of the specified outcomes, which then triggers the corresponding `@listen` decorator.
|
||||
|
||||
You can also use `@human_feedback` without routing to simply collect feedback:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(message="Any comments on this output?")
|
||||
def my_method(self):
|
||||
return "Output for review"
|
||||
|
||||
@listen(my_method)
|
||||
def next_step(self, result: HumanFeedbackResult):
|
||||
# Access feedback via result.feedback
|
||||
# Access original output via result.output
|
||||
pass
|
||||
```
|
||||
|
||||
Access all feedback collected during a flow via `self.last_human_feedback` (most recent) or `self.human_feedback_history` (all feedback as a list).
|
||||
|
||||
For a complete guide on human feedback in flows, including **async/non-blocking feedback** with custom providers (Slack, webhooks, etc.), see [Human Feedback in Flows](/en/learn/human-feedback-in-flows).
|
||||
|
||||
## Adding Agents to Flows
|
||||
|
||||
Agents can be seamlessly integrated into your flows, providing a lightweight alternative to full Crews when you need simpler, focused task execution. Here's an example of how to use an Agent within a flow to perform market research:
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
title: Production Architecture
|
||||
description: Best practices for building production-ready AI applications with CrewAI
|
||||
icon: server
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# The Flow-First Mindset
|
||||
|
||||
When building production AI applications with CrewAI, **we recommend starting with a Flow**.
|
||||
|
||||
While it's possible to run individual Crews or Agents, wrapping them in a Flow provides the necessary structure for a robust, scalable application.
|
||||
|
||||
## Why Flows?
|
||||
|
||||
1. **State Management**: Flows provide a built-in way to manage state across different steps of your application. This is crucial for passing data between Crews, maintaining context, and handling user inputs.
|
||||
2. **Control**: Flows allow you to define precise execution paths, including loops, conditionals, and branching logic. This is essential for handling edge cases and ensuring your application behaves predictably.
|
||||
3. **Observability**: Flows provide a clear structure that makes it easier to trace execution, debug issues, and monitor performance. We recommend using [CrewAI Tracing](/en/observability/tracing) for detailed insights. Simply run `crewai login` to enable free observability features.
|
||||
|
||||
## The Architecture
|
||||
|
||||
A typical production CrewAI application looks like this:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Start((Start)) --> Flow[Flow Orchestrator]
|
||||
Flow --> State{State Management}
|
||||
State --> Step1[Step 1: Data Gathering]
|
||||
Step1 --> Crew1[Research Crew]
|
||||
Crew1 --> State
|
||||
State --> Step2{Condition Check}
|
||||
Step2 -- "Valid" --> Step3[Step 3: Execution]
|
||||
Step3 --> Crew2[Action Crew]
|
||||
Step2 -- "Invalid" --> End((End))
|
||||
Crew2 --> End
|
||||
```
|
||||
|
||||
### 1. The Flow Class
|
||||
Your `Flow` class is the entry point. It defines the state schema and the methods that execute your logic.
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AppState(BaseModel):
|
||||
user_input: str = ""
|
||||
research_results: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
@start()
|
||||
def gather_input(self):
|
||||
# ... logic to get input ...
|
||||
pass
|
||||
|
||||
@listen(gather_input)
|
||||
def run_research_crew(self):
|
||||
# ... trigger a Crew ...
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. State Management
|
||||
Use Pydantic models to define your state. This ensures type safety and makes it clear what data is available at each step.
|
||||
|
||||
- **Keep it minimal**: Store only what you need to persist between steps.
|
||||
- **Use structured data**: Avoid unstructured dictionaries when possible.
|
||||
|
||||
### 3. Crews as Units of Work
|
||||
Delegate complex tasks to Crews. A Crew should be focused on a specific goal (e.g., "Research a topic", "Write a blog post").
|
||||
|
||||
- **Don't over-engineer Crews**: Keep them focused.
|
||||
- **Pass state explicitly**: Pass the necessary data from the Flow state to the Crew inputs.
|
||||
|
||||
```python
|
||||
@listen(gather_input)
|
||||
def run_research_crew(self):
|
||||
crew = ResearchCrew()
|
||||
result = crew.kickoff(inputs={"topic": self.state.user_input})
|
||||
self.state.research_results = result.raw
|
||||
```
|
||||
|
||||
## Control Primitives
|
||||
|
||||
Leverage CrewAI's control primitives to add robustness and control to your Crews.
|
||||
|
||||
### 1. Task Guardrails
|
||||
Use [Task Guardrails](/en/concepts/tasks#task-guardrails) to validate task outputs before they are accepted. This ensures that your agents produce high-quality results.
|
||||
|
||||
```python
|
||||
def validate_content(result: TaskOutput) -> Tuple[bool, Any]:
|
||||
if len(result.raw) < 100:
|
||||
return (False, "Content is too short. Please expand.")
|
||||
return (True, result.raw)
|
||||
|
||||
task = Task(
|
||||
...,
|
||||
guardrail=validate_content
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Structured Outputs
|
||||
Always use structured outputs (`output_pydantic` or `output_json`) when passing data between tasks or to your application. This prevents parsing errors and ensures type safety.
|
||||
|
||||
```python
|
||||
class ResearchResult(BaseModel):
|
||||
summary: str
|
||||
sources: List[str]
|
||||
|
||||
task = Task(
|
||||
...,
|
||||
output_pydantic=ResearchResult
|
||||
)
|
||||
```
|
||||
|
||||
### 3. LLM Hooks
|
||||
Use [LLM Hooks](/en/learn/llm-hooks) to inspect or modify messages before they are sent to the LLM, or to sanitize responses.
|
||||
|
||||
```python
|
||||
@before_llm_call
|
||||
def log_request(context):
|
||||
print(f"Agent {context.agent.role} is calling the LLM...")
|
||||
```
|
||||
|
||||
## Deployment Patterns
|
||||
|
||||
When deploying your Flow, consider the following:
|
||||
|
||||
### CrewAI Enterprise
|
||||
The easiest way to deploy your Flow is using CrewAI Enterprise. It handles the infrastructure, authentication, and monitoring for you.
|
||||
|
||||
Check out the [Deployment Guide](/en/enterprise/guides/deploy-crew) to get started.
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
### Async Execution
|
||||
For long-running tasks, use `kickoff_async` to avoid blocking your API.
|
||||
|
||||
### Persistence
|
||||
Use the `@persist` decorator to save the state of your Flow to a database. This allows you to resume execution if the process crashes or if you need to wait for human input.
|
||||
|
||||
```python
|
||||
@persist
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
- **Start with a Flow.**
|
||||
- **Define a clear State.**
|
||||
- **Use Crews for complex tasks.**
|
||||
- **Deploy with an API and persistence.**
|
||||
@@ -62,13 +62,13 @@ Test your Gmail trigger integration locally using the CrewAI CLI:
|
||||
crewai triggers list
|
||||
|
||||
# Simulate a Gmail trigger with realistic payload
|
||||
crewai triggers run gmail/new_email_received
|
||||
crewai triggers run gmail/new_email
|
||||
```
|
||||
|
||||
The `crewai triggers run` command will execute your crew with a complete Gmail payload, allowing you to test your parsing logic before deployment.
|
||||
|
||||
<Warning>
|
||||
Use `crewai triggers run gmail/new_email_received` (not `crewai run`) to simulate trigger execution during development. After deployment, your crew will automatically receive the trigger payload.
|
||||
Use `crewai triggers run gmail/new_email` (not `crewai run`) to simulate trigger execution during development. After deployment, your crew will automatically receive the trigger payload.
|
||||
</Warning>
|
||||
|
||||
## Monitoring Executions
|
||||
@@ -83,6 +83,6 @@ Track history and performance of triggered runs:
|
||||
|
||||
- Ensure Gmail is connected in Tools & Integrations
|
||||
- Verify the Gmail Trigger is enabled on the Triggers tab
|
||||
- Test locally with `crewai triggers run gmail/new_email_received` to see the exact payload structure
|
||||
- Test locally with `crewai triggers run gmail/new_email` to see the exact payload structure
|
||||
- Check the execution logs and confirm the payload is passed as `crewai_trigger_payload`
|
||||
- Remember: use `crewai triggers run` (not `crewai run`) to simulate trigger execution
|
||||
|
||||
@@ -7,89 +7,110 @@ mode: "wide"
|
||||
|
||||
# What is CrewAI?
|
||||
|
||||
**CrewAI is the leading open-source framework for orchestrating autonomous AI agents and building complex workflows.**
|
||||
**CrewAI is a lean, lightning-fast Python framework built entirely from scratch—completely independent of LangChain or other agent frameworks.**
|
||||
|
||||
It empowers developers to build production-ready multi-agent systems by combining the collaborative intelligence of **Crews** with the precise control of **Flows**.
|
||||
CrewAI empowers developers with both high-level simplicity and precise low-level control, ideal for creating autonomous AI agents tailored to any scenario:
|
||||
|
||||
- **[CrewAI Flows](/en/guides/flows/first-flow)**: The backbone of your AI application. Flows allow you to create structured, event-driven workflows that manage state and control execution. They provide the scaffolding for your AI agents to work within.
|
||||
- **[CrewAI Crews](/en/guides/crews/first-crew)**: The units of work within your Flow. Crews are teams of autonomous agents that collaborate to solve specific tasks delegated to them by the Flow.
|
||||
- **[CrewAI Crews](/en/guides/crews/first-crew)**: Optimize for autonomy and collaborative intelligence, enabling you to create AI teams where each agent has specific roles, tools, and goals.
|
||||
- **[CrewAI Flows](/en/guides/flows/first-flow)**: Enable granular, event-driven control, single LLM calls for precise task orchestration and supports Crews natively.
|
||||
|
||||
With over 100,000 developers certified through our community courses, CrewAI is the standard for enterprise-ready AI automation.
|
||||
With over 100,000 developers certified through our community courses, CrewAI is rapidly becoming the standard for enterprise-ready AI automation.
|
||||
|
||||
## The CrewAI Architecture
|
||||
|
||||
CrewAI's architecture is designed to balance autonomy with control.
|
||||
|
||||
### 1. Flows: The Backbone
|
||||
## How Crews Work
|
||||
|
||||
<Note>
|
||||
Think of a Flow as the "manager" or the "process definition" of your application. It defines the steps, the logic, and how data moves through your system.
|
||||
</Note>
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/flows.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
Flows provide:
|
||||
- **State Management**: Persist data across steps and executions.
|
||||
- **Event-Driven Execution**: Trigger actions based on events or external inputs.
|
||||
- **Control Flow**: Use conditional logic, loops, and branching.
|
||||
|
||||
### 2. Crews: The Intelligence
|
||||
|
||||
<Note>
|
||||
Crews are the "teams" that do the heavy lifting. Within a Flow, you can trigger a Crew to tackle a complex problem requiring creativity and collaboration.
|
||||
Just like a company has departments (Sales, Engineering, Marketing) working together under leadership to achieve business goals, CrewAI helps you create an organization of AI agents with specialized roles collaborating to accomplish complex tasks.
|
||||
</Note>
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/crews.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
Crews provide:
|
||||
- **Role-Playing Agents**: Specialized agents with specific goals and tools.
|
||||
- **Autonomous Collaboration**: Agents work together to solve tasks.
|
||||
- **Task Delegation**: Tasks are assigned and executed based on agent capabilities.
|
||||
| Component | Description | Key Features |
|
||||
|:----------|:-----------:|:------------|
|
||||
| **Crew** | The top-level organization | • Manages AI agent teams<br/>• Oversees workflows<br/>• Ensures collaboration<br/>• Delivers outcomes |
|
||||
| **AI Agents** | Specialized team members | • Have specific roles (researcher, writer)<br/>• Use designated tools<br/>• Can delegate tasks<br/>• Make autonomous decisions |
|
||||
| **Process** | Workflow management system | • Defines collaboration patterns<br/>• Controls task assignments<br/>• Manages interactions<br/>• Ensures efficient execution |
|
||||
| **Tasks** | Individual assignments | • Have clear objectives<br/>• Use specific tools<br/>• Feed into larger process<br/>• Produce actionable results |
|
||||
|
||||
## How It All Works Together
|
||||
### How It All Works Together
|
||||
|
||||
1. **The Flow** triggers an event or starts a process.
|
||||
2. **The Flow** manages the state and decides what to do next.
|
||||
3. **The Flow** delegates a complex task to a **Crew**.
|
||||
4. **The Crew**'s agents collaborate to complete the task.
|
||||
5. **The Crew** returns the result to the **Flow**.
|
||||
6. **The Flow** continues execution based on the result.
|
||||
1. The **Crew** organizes the overall operation
|
||||
2. **AI Agents** work on their specialized tasks
|
||||
3. The **Process** ensures smooth collaboration
|
||||
4. **Tasks** get completed to achieve the goal
|
||||
|
||||
## Key Features
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Production-Grade Flows" icon="arrow-progress">
|
||||
Build reliable, stateful workflows that can handle long-running processes and complex logic.
|
||||
</Card>
|
||||
<Card title="Autonomous Crews" icon="users">
|
||||
Deploy teams of agents that can plan, execute, and collaborate to achieve high-level goals.
|
||||
<Card title="Role-Based Agents" icon="users">
|
||||
Create specialized agents with defined roles, expertise, and goals - from researchers to analysts to writers
|
||||
</Card>
|
||||
<Card title="Flexible Tools" icon="screwdriver-wrench">
|
||||
Connect your agents to any API, database, or local tool.
|
||||
Equip agents with custom tools and APIs to interact with external services and data sources
|
||||
</Card>
|
||||
<Card title="Enterprise Security" icon="lock">
|
||||
Designed with security and compliance in mind for enterprise deployments.
|
||||
<Card title="Intelligent Collaboration" icon="people-arrows">
|
||||
Agents work together, sharing insights and coordinating tasks to achieve complex objectives
|
||||
</Card>
|
||||
<Card title="Task Management" icon="list-check">
|
||||
Define sequential or parallel workflows, with agents automatically handling task dependencies
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## How Flows Work
|
||||
|
||||
<Note>
|
||||
While Crews excel at autonomous collaboration, Flows provide structured automations, offering granular control over workflow execution. Flows ensure tasks are executed reliably, securely, and efficiently, handling conditional logic, loops, and dynamic state management with precision. Flows integrate seamlessly with Crews, enabling you to balance high autonomy with exacting control.
|
||||
</Note>
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/flows.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
| Component | Description | Key Features |
|
||||
|:----------|:-----------:|:------------|
|
||||
| **Flow** | Structured workflow orchestration | • Manages execution paths<br/>• Handles state transitions<br/>• Controls task sequencing<br/>• Ensures reliable execution |
|
||||
| **Events** | Triggers for workflow actions | • Initiate specific processes<br/>• Enable dynamic responses<br/>• Support conditional branching<br/>• Allow for real-time adaptation |
|
||||
| **States** | Workflow execution contexts | • Maintain execution data<br/>• Enable persistence<br/>• Support resumability<br/>• Ensure execution integrity |
|
||||
| **Crew Support** | Enhances workflow automation | • Injects pockets of agency when needed<br/>• Complements structured workflows<br/>• Balances automation with intelligence<br/>• Enables adaptive decision-making |
|
||||
|
||||
### Key Capabilities
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Event-Driven Orchestration" icon="bolt">
|
||||
Define precise execution paths responding dynamically to events
|
||||
</Card>
|
||||
<Card title="Fine-Grained Control" icon="sliders">
|
||||
Manage workflow states and conditional execution securely and efficiently
|
||||
</Card>
|
||||
<Card title="Native Crew Integration" icon="puzzle-piece">
|
||||
Effortlessly combine with Crews for enhanced autonomy and intelligence
|
||||
</Card>
|
||||
<Card title="Deterministic Execution" icon="route">
|
||||
Ensure predictable outcomes with explicit control flow and error handling
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## When to Use Crews vs. Flows
|
||||
|
||||
**The short answer: Use both.**
|
||||
<Note>
|
||||
Understanding when to use [Crews](/en/guides/crews/first-crew) versus [Flows](/en/guides/flows/first-flow) is key to maximizing the potential of CrewAI in your applications.
|
||||
</Note>
|
||||
|
||||
For any production-ready application, **start with a Flow**.
|
||||
| Use Case | Recommended Approach | Why? |
|
||||
|:---------|:---------------------|:-----|
|
||||
| **Open-ended research** | [Crews](/en/guides/crews/first-crew) | When tasks require creative thinking, exploration, and adaptation |
|
||||
| **Content generation** | [Crews](/en/guides/crews/first-crew) | For collaborative creation of articles, reports, or marketing materials |
|
||||
| **Decision workflows** | [Flows](/en/guides/flows/first-flow) | When you need predictable, auditable decision paths with precise control |
|
||||
| **API orchestration** | [Flows](/en/guides/flows/first-flow) | For reliable integration with multiple external services in a specific sequence |
|
||||
| **Hybrid applications** | Combined approach | Use [Flows](/en/guides/flows/first-flow) to orchestrate overall process with [Crews](/en/guides/crews/first-crew) handling complex subtasks |
|
||||
|
||||
- **Use a Flow** to define the overall structure, state, and logic of your application.
|
||||
- **Use a Crew** within a Flow step when you need a team of agents to perform a specific, complex task that requires autonomy.
|
||||
### Decision Framework
|
||||
|
||||
| Use Case | Architecture |
|
||||
| :--- | :--- |
|
||||
| **Simple Automation** | Single Flow with Python tasks |
|
||||
| **Complex Research** | Flow managing state -> Crew performing research |
|
||||
| **Application Backend** | Flow handling API requests -> Crew generating content -> Flow saving to DB |
|
||||
- **Choose [Crews](/en/guides/crews/first-crew) when:** You need autonomous problem-solving, creative collaboration, or exploratory tasks
|
||||
- **Choose [Flows](/en/guides/flows/first-flow) when:** You require deterministic outcomes, auditability, or precise control over execution
|
||||
- **Combine both when:** Your application needs both structured processes and pockets of autonomous intelligence
|
||||
|
||||
## Why Choose CrewAI?
|
||||
|
||||
@@ -103,13 +124,6 @@ For any production-ready application, **start with a Flow**.
|
||||
## Ready to Start Building?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Build Your First Flow"
|
||||
icon="diagram-project"
|
||||
href="/en/guides/flows/first-flow"
|
||||
>
|
||||
Learn how to create structured, event-driven workflows with precise control over execution.
|
||||
</Card>
|
||||
<Card
|
||||
title="Build Your First Crew"
|
||||
icon="users-gear"
|
||||
@@ -117,6 +131,13 @@ For any production-ready application, **start with a Flow**.
|
||||
>
|
||||
Step-by-step tutorial to create a collaborative AI team that works together to solve complex problems.
|
||||
</Card>
|
||||
<Card
|
||||
title="Build Your First Flow"
|
||||
icon="diagram-project"
|
||||
href="/en/guides/flows/first-flow"
|
||||
>
|
||||
Learn how to create structured, event-driven workflows with precise control over execution.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
---
|
||||
title: Human Feedback in Flows
|
||||
description: Learn how to integrate human feedback directly into your CrewAI Flows using the @human_feedback decorator
|
||||
icon: user-check
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The `@human_feedback` decorator enables human-in-the-loop (HITL) workflows directly within CrewAI Flows. It allows you to pause flow execution, present output to a human for review, collect their feedback, and optionally route to different listeners based on the feedback outcome.
|
||||
|
||||
This is particularly valuable for:
|
||||
|
||||
- **Quality assurance**: Review AI-generated content before it's used downstream
|
||||
- **Decision gates**: Let humans make critical decisions in automated workflows
|
||||
- **Approval workflows**: Implement approve/reject/revise patterns
|
||||
- **Interactive refinement**: Collect feedback to improve outputs iteratively
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Flow Method] --> B[Output Generated]
|
||||
B --> C[Human Reviews]
|
||||
C --> D{Feedback}
|
||||
D -->|emit specified| E[LLM Collapses to Outcome]
|
||||
D -->|no emit| F[HumanFeedbackResult]
|
||||
E --> G["@listen('approved')"]
|
||||
E --> H["@listen('rejected')"]
|
||||
F --> I[Next Listener]
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Here's the simplest way to add human feedback to a flow:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback
|
||||
|
||||
class SimpleReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Please review this content:")
|
||||
def generate_content(self):
|
||||
return "This is AI-generated content that needs review."
|
||||
|
||||
@listen(generate_content)
|
||||
def process_feedback(self, result):
|
||||
print(f"Content: {result.output}")
|
||||
print(f"Human said: {result.feedback}")
|
||||
|
||||
flow = SimpleReviewFlow()
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
When this flow runs, it will:
|
||||
1. Execute `generate_content` and return the string
|
||||
2. Display the output to the user with the request message
|
||||
3. Wait for the user to type feedback (or press Enter to skip)
|
||||
4. Pass a `HumanFeedbackResult` object to `process_feedback`
|
||||
|
||||
## The @human_feedback Decorator
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `message` | `str` | Yes | The message shown to the human alongside the method output |
|
||||
| `emit` | `Sequence[str]` | No | List of possible outcomes. Feedback is collapsed to one of these, which triggers `@listen` decorators |
|
||||
| `llm` | `str \| BaseLLM` | When `emit` specified | LLM used to interpret feedback and map to an outcome |
|
||||
| `default_outcome` | `str` | No | Outcome to use if no feedback provided. Must be in `emit` |
|
||||
| `metadata` | `dict` | No | Additional data for enterprise integrations |
|
||||
| `provider` | `HumanFeedbackProvider` | No | Custom provider for async/non-blocking feedback. See [Async Human Feedback](#async-human-feedback-non-blocking) |
|
||||
|
||||
### Basic Usage (No Routing)
|
||||
|
||||
When you don't specify `emit`, the decorator simply collects feedback and passes a `HumanFeedbackResult` to the next listener:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(message="What do you think of this analysis?")
|
||||
def analyze_data(self):
|
||||
return "Analysis results: Revenue up 15%, costs down 8%"
|
||||
|
||||
@listen(analyze_data)
|
||||
def handle_feedback(self, result):
|
||||
# result is a HumanFeedbackResult
|
||||
print(f"Analysis: {result.output}")
|
||||
print(f"Feedback: {result.feedback}")
|
||||
```
|
||||
|
||||
### Routing with emit
|
||||
|
||||
When you specify `emit`, the decorator becomes a router. The human's free-form feedback is interpreted by an LLM and collapsed into one of the specified outcomes:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Do you approve this content for publication?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_content(self):
|
||||
return "Draft blog post content here..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
print(f"Publishing! User said: {result.feedback}")
|
||||
|
||||
@listen("rejected")
|
||||
def discard(self, result):
|
||||
print(f"Discarding. Reason: {result.feedback}")
|
||||
|
||||
@listen("needs_revision")
|
||||
def revise(self, result):
|
||||
print(f"Revising based on: {result.feedback}")
|
||||
```
|
||||
|
||||
<Tip>
|
||||
The LLM uses structured outputs (function calling) when available to guarantee the response is one of your specified outcomes. This makes routing reliable and predictable.
|
||||
</Tip>
|
||||
|
||||
## HumanFeedbackResult
|
||||
|
||||
The `HumanFeedbackResult` dataclass contains all information about a human feedback interaction:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult
|
||||
|
||||
@dataclass
|
||||
class HumanFeedbackResult:
|
||||
output: Any # The original method output shown to the human
|
||||
feedback: str # The raw feedback text from the human
|
||||
outcome: str | None # The collapsed outcome (if emit was specified)
|
||||
timestamp: datetime # When the feedback was received
|
||||
method_name: str # Name of the decorated method
|
||||
metadata: dict # Any metadata passed to the decorator
|
||||
```
|
||||
|
||||
### Accessing in Listeners
|
||||
|
||||
When a listener is triggered by a `@human_feedback` method with `emit`, it receives the `HumanFeedbackResult`:
|
||||
|
||||
```python Code
|
||||
@listen("approved")
|
||||
def on_approval(self, result: HumanFeedbackResult):
|
||||
print(f"Original output: {result.output}")
|
||||
print(f"User feedback: {result.feedback}")
|
||||
print(f"Outcome: {result.outcome}") # "approved"
|
||||
print(f"Received at: {result.timestamp}")
|
||||
```
|
||||
|
||||
## Accessing Feedback History
|
||||
|
||||
The `Flow` class provides two attributes for accessing human feedback:
|
||||
|
||||
### last_human_feedback
|
||||
|
||||
Returns the most recent `HumanFeedbackResult`:
|
||||
|
||||
```python Code
|
||||
@listen(some_method)
|
||||
def check_feedback(self):
|
||||
if self.last_human_feedback:
|
||||
print(f"Last feedback: {self.last_human_feedback.feedback}")
|
||||
```
|
||||
|
||||
### human_feedback_history
|
||||
|
||||
A list of all `HumanFeedbackResult` objects collected during the flow:
|
||||
|
||||
```python Code
|
||||
@listen(final_step)
|
||||
def summarize(self):
|
||||
print(f"Total feedback collected: {len(self.human_feedback_history)}")
|
||||
for i, fb in enumerate(self.human_feedback_history):
|
||||
print(f"{i+1}. {fb.method_name}: {fb.outcome or 'no routing'}")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Each `HumanFeedbackResult` is appended to `human_feedback_history`, so multiple feedback steps won't overwrite each other. Use this list to access all feedback collected during the flow.
|
||||
</Warning>
|
||||
|
||||
## Complete Example: Content Approval Workflow
|
||||
|
||||
Here's a full example implementing a content review and approval workflow:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback, HumanFeedbackResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ContentState(BaseModel):
|
||||
topic: str = ""
|
||||
draft: str = ""
|
||||
final_content: str = ""
|
||||
revision_count: int = 0
|
||||
|
||||
|
||||
class ContentApprovalFlow(Flow[ContentState]):
|
||||
"""A flow that generates content and gets human approval."""
|
||||
|
||||
@start()
|
||||
def get_topic(self):
|
||||
self.state.topic = input("What topic should I write about? ")
|
||||
return self.state.topic
|
||||
|
||||
@listen(get_topic)
|
||||
def generate_draft(self, topic):
|
||||
# In real use, this would call an LLM
|
||||
self.state.draft = f"# {topic}\n\nThis is a draft about {topic}..."
|
||||
return self.state.draft
|
||||
|
||||
@listen(generate_draft)
|
||||
@human_feedback(
|
||||
message="Please review this draft. Reply 'approved', 'rejected', or provide revision feedback:",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_draft(self, draft):
|
||||
return draft
|
||||
|
||||
@listen("approved")
|
||||
def publish_content(self, result: HumanFeedbackResult):
|
||||
self.state.final_content = result.output
|
||||
print("\n✅ Content approved and published!")
|
||||
print(f"Reviewer comment: {result.feedback}")
|
||||
return "published"
|
||||
|
||||
@listen("rejected")
|
||||
def handle_rejection(self, result: HumanFeedbackResult):
|
||||
print("\n❌ Content rejected")
|
||||
print(f"Reason: {result.feedback}")
|
||||
return "rejected"
|
||||
|
||||
@listen("needs_revision")
|
||||
def revise_content(self, result: HumanFeedbackResult):
|
||||
self.state.revision_count += 1
|
||||
print(f"\n📝 Revision #{self.state.revision_count} requested")
|
||||
print(f"Feedback: {result.feedback}")
|
||||
|
||||
# In a real flow, you might loop back to generate_draft
|
||||
# For this example, we just acknowledge
|
||||
return "revision_requested"
|
||||
|
||||
|
||||
# Run the flow
|
||||
flow = ContentApprovalFlow()
|
||||
result = flow.kickoff()
|
||||
print(f"\nFlow completed. Revisions requested: {flow.state.revision_count}")
|
||||
```
|
||||
|
||||
```text Output
|
||||
What topic should I write about? AI Safety
|
||||
|
||||
==================================================
|
||||
OUTPUT FOR REVIEW:
|
||||
==================================================
|
||||
# AI Safety
|
||||
|
||||
This is a draft about AI Safety...
|
||||
==================================================
|
||||
|
||||
Please review this draft. Reply 'approved', 'rejected', or provide revision feedback:
|
||||
(Press Enter to skip, or type your feedback)
|
||||
|
||||
Your feedback: Looks good, approved!
|
||||
|
||||
✅ Content approved and published!
|
||||
Reviewer comment: Looks good, approved!
|
||||
|
||||
Flow completed. Revisions requested: 0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Combining with Other Decorators
|
||||
|
||||
The `@human_feedback` decorator works with other flow decorators. Place it as the innermost decorator (closest to the function):
|
||||
|
||||
```python Code
|
||||
# Correct: @human_feedback is innermost (closest to the function)
|
||||
@start()
|
||||
@human_feedback(message="Review this:")
|
||||
def my_start_method(self):
|
||||
return "content"
|
||||
|
||||
@listen(other_method)
|
||||
@human_feedback(message="Review this too:")
|
||||
def my_listener(self, data):
|
||||
return f"processed: {data}"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Place `@human_feedback` as the innermost decorator (last/closest to the function) so it wraps the method directly and can capture the return value before passing to the flow system.
|
||||
</Tip>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Write Clear Request Messages
|
||||
|
||||
The `request` parameter is what the human sees. Make it actionable:
|
||||
|
||||
```python Code
|
||||
# ✅ Good - clear and actionable
|
||||
@human_feedback(message="Does this summary accurately capture the key points? Reply 'yes' or explain what's missing:")
|
||||
|
||||
# ❌ Bad - vague
|
||||
@human_feedback(message="Review this:")
|
||||
```
|
||||
|
||||
### 2. Choose Meaningful Outcomes
|
||||
|
||||
When using `emit`, pick outcomes that map naturally to human responses:
|
||||
|
||||
```python Code
|
||||
# ✅ Good - natural language outcomes
|
||||
emit=["approved", "rejected", "needs_more_detail"]
|
||||
|
||||
# ❌ Bad - technical or unclear
|
||||
emit=["state_1", "state_2", "state_3"]
|
||||
```
|
||||
|
||||
### 3. Always Provide a Default Outcome
|
||||
|
||||
Use `default_outcome` to handle cases where users press Enter without typing:
|
||||
|
||||
```python Code
|
||||
@human_feedback(
|
||||
message="Approve? (press Enter to request revision)",
|
||||
emit=["approved", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision", # Safe default
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Use Feedback History for Audit Trails
|
||||
|
||||
Access `human_feedback_history` to create audit logs:
|
||||
|
||||
```python Code
|
||||
@listen(final_step)
|
||||
def create_audit_log(self):
|
||||
log = []
|
||||
for fb in self.human_feedback_history:
|
||||
log.append({
|
||||
"step": fb.method_name,
|
||||
"outcome": fb.outcome,
|
||||
"feedback": fb.feedback,
|
||||
"timestamp": fb.timestamp.isoformat(),
|
||||
})
|
||||
return log
|
||||
```
|
||||
|
||||
### 5. Handle Both Routed and Non-Routed Feedback
|
||||
|
||||
When designing flows, consider whether you need routing:
|
||||
|
||||
| Scenario | Use |
|
||||
|----------|-----|
|
||||
| Simple review, just need the feedback text | No `emit` |
|
||||
| Need to branch to different paths based on response | Use `emit` |
|
||||
| Approval gates with approve/reject/revise | Use `emit` |
|
||||
| Collecting comments for logging only | No `emit` |
|
||||
|
||||
## Async Human Feedback (Non-Blocking)
|
||||
|
||||
By default, `@human_feedback` blocks execution waiting for console input. For production applications, you may need **async/non-blocking** feedback that integrates with external systems like Slack, email, webhooks, or APIs.
|
||||
|
||||
### The Provider Abstraction
|
||||
|
||||
Use the `provider` parameter to specify a custom feedback collection strategy:
|
||||
|
||||
```python Code
|
||||
from crewai.flow import Flow, start, human_feedback, HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext
|
||||
|
||||
class WebhookProvider(HumanFeedbackProvider):
|
||||
"""Provider that pauses flow and waits for webhook callback."""
|
||||
|
||||
def __init__(self, webhook_url: str):
|
||||
self.webhook_url = webhook_url
|
||||
|
||||
def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
|
||||
# Notify external system (e.g., send Slack message, create ticket)
|
||||
self.send_notification(context)
|
||||
|
||||
# Pause execution - framework handles persistence automatically
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={"webhook_url": f"{self.webhook_url}/{context.flow_id}"}
|
||||
)
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review this content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
provider=WebhookProvider("https://myapp.com/api"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "AI-generated content..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
return "Published!"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
The flow framework **automatically persists state** when `HumanFeedbackPending` is raised. Your provider only needs to notify the external system and raise the exception—no manual persistence calls required.
|
||||
</Tip>
|
||||
|
||||
### Handling Paused Flows
|
||||
|
||||
When using an async provider, `kickoff()` returns a `HumanFeedbackPending` object instead of raising an exception:
|
||||
|
||||
```python Code
|
||||
flow = ReviewFlow()
|
||||
result = flow.kickoff()
|
||||
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
# Flow is paused, state is automatically persisted
|
||||
print(f"Waiting for feedback at: {result.callback_info['webhook_url']}")
|
||||
print(f"Flow ID: {result.context.flow_id}")
|
||||
else:
|
||||
# Normal completion
|
||||
print(f"Flow completed: {result}")
|
||||
```
|
||||
|
||||
### Resuming a Paused Flow
|
||||
|
||||
When feedback arrives (e.g., via webhook), resume the flow:
|
||||
|
||||
```python Code
|
||||
# Sync handler:
|
||||
def handle_feedback_webhook(flow_id: str, feedback: str):
|
||||
flow = ReviewFlow.from_pending(flow_id)
|
||||
result = flow.resume(feedback)
|
||||
return result
|
||||
|
||||
# Async handler (FastAPI, aiohttp, etc.):
|
||||
async def handle_feedback_webhook(flow_id: str, feedback: str):
|
||||
flow = ReviewFlow.from_pending(flow_id)
|
||||
result = await flow.resume_async(feedback)
|
||||
return result
|
||||
```
|
||||
|
||||
### Key Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `HumanFeedbackProvider` | Protocol for custom feedback providers |
|
||||
| `PendingFeedbackContext` | Contains all info needed to resume a paused flow |
|
||||
| `HumanFeedbackPending` | Returned by `kickoff()` when flow is paused for feedback |
|
||||
| `ConsoleProvider` | Default blocking console input provider |
|
||||
|
||||
### PendingFeedbackContext
|
||||
|
||||
The context contains everything needed to resume:
|
||||
|
||||
```python Code
|
||||
@dataclass
|
||||
class PendingFeedbackContext:
|
||||
flow_id: str # Unique identifier for this flow execution
|
||||
flow_class: str # Fully qualified class name
|
||||
method_name: str # Method that triggered feedback
|
||||
method_output: Any # Output shown to the human
|
||||
message: str # The request message
|
||||
emit: list[str] | None # Possible outcomes for routing
|
||||
default_outcome: str | None
|
||||
metadata: dict # Custom metadata
|
||||
llm: str | None # LLM for outcome collapsing
|
||||
requested_at: datetime
|
||||
```
|
||||
|
||||
### Complete Async Flow Example
|
||||
|
||||
```python Code
|
||||
from crewai.flow import (
|
||||
Flow, start, listen, human_feedback,
|
||||
HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext
|
||||
)
|
||||
|
||||
class SlackNotificationProvider(HumanFeedbackProvider):
|
||||
"""Provider that sends Slack notifications and pauses for async feedback."""
|
||||
|
||||
def __init__(self, channel: str):
|
||||
self.channel = channel
|
||||
|
||||
def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
|
||||
# Send Slack notification (implement your own)
|
||||
slack_thread_id = self.post_to_slack(
|
||||
channel=self.channel,
|
||||
message=f"Review needed:\n\n{context.method_output}\n\n{context.message}",
|
||||
)
|
||||
|
||||
# Pause execution - framework handles persistence automatically
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={
|
||||
"slack_channel": self.channel,
|
||||
"thread_id": slack_thread_id,
|
||||
}
|
||||
)
|
||||
|
||||
class ContentPipeline(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Approve this content for publication?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
provider=SlackNotificationProvider("#content-reviews"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "AI-generated blog post content..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
print(f"Publishing! Reviewer said: {result.feedback}")
|
||||
return {"status": "published"}
|
||||
|
||||
@listen("rejected")
|
||||
def archive(self, result):
|
||||
print(f"Archived. Reason: {result.feedback}")
|
||||
return {"status": "archived"}
|
||||
|
||||
@listen("needs_revision")
|
||||
def queue_revision(self, result):
|
||||
print(f"Queued for revision: {result.feedback}")
|
||||
return {"status": "revision_needed"}
|
||||
|
||||
|
||||
# Starting the flow (will pause and wait for Slack response)
|
||||
def start_content_pipeline():
|
||||
flow = ContentPipeline()
|
||||
result = flow.kickoff()
|
||||
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
return {"status": "pending", "flow_id": result.context.flow_id}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Resuming when Slack webhook fires (sync handler)
|
||||
def on_slack_feedback(flow_id: str, slack_message: str):
|
||||
flow = ContentPipeline.from_pending(flow_id)
|
||||
result = flow.resume(slack_message)
|
||||
return result
|
||||
|
||||
|
||||
# If your handler is async (FastAPI, aiohttp, Slack Bolt async, etc.)
|
||||
async def on_slack_feedback_async(flow_id: str, slack_message: str):
|
||||
flow = ContentPipeline.from_pending(flow_id)
|
||||
result = await flow.resume_async(slack_message)
|
||||
return result
|
||||
```
|
||||
|
||||
<Warning>
|
||||
If you're using an async web framework (FastAPI, aiohttp, Slack Bolt async mode), use `await flow.resume_async()` instead of `flow.resume()`. Calling `resume()` from within a running event loop will raise a `RuntimeError`.
|
||||
</Warning>
|
||||
|
||||
### Best Practices for Async Feedback
|
||||
|
||||
1. **Check the return type**: `kickoff()` returns `HumanFeedbackPending` when paused—no try/except needed
|
||||
2. **Use the right resume method**: Use `resume()` in sync code, `await resume_async()` in async code
|
||||
3. **Store callback info**: Use `callback_info` to store webhook URLs, ticket IDs, etc.
|
||||
4. **Implement idempotency**: Your resume handler should be idempotent for safety
|
||||
5. **Automatic persistence**: State is automatically saved when `HumanFeedbackPending` is raised and uses `SQLiteFlowPersistence` by default
|
||||
6. **Custom persistence**: Pass a custom persistence instance to `from_pending()` if needed
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Flows Overview](/en/concepts/flows) - Learn about CrewAI Flows
|
||||
- [Flow State Management](/en/guides/flows/mastering-flow-state) - Managing state in flows
|
||||
- [Flow Persistence](/en/concepts/flows#persistence) - Persisting flow state
|
||||
- [Routing with @router](/en/concepts/flows#router) - More about conditional routing
|
||||
- [Human Input on Execution](/en/learn/human-input-on-execution) - Task-level human input
|
||||
@@ -5,22 +5,9 @@ icon: "user-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
Human-in-the-Loop (HITL) is a powerful approach that combines artificial intelligence with human expertise to enhance decision-making and improve task outcomes. CrewAI provides multiple ways to implement HITL depending on your needs.
|
||||
Human-in-the-Loop (HITL) is a powerful approach that combines artificial intelligence with human expertise to enhance decision-making and improve task outcomes. This guide shows you how to implement HITL within CrewAI.
|
||||
|
||||
## Choosing Your HITL Approach
|
||||
|
||||
CrewAI offers two main approaches for implementing human-in-the-loop workflows:
|
||||
|
||||
| Approach | Best For | Integration |
|
||||
|----------|----------|-------------|
|
||||
| **Flow-based** (`@human_feedback` decorator) | Local development, console-based review, synchronous workflows | [Human Feedback in Flows](/en/learn/human-feedback-in-flows) |
|
||||
| **Webhook-based** (Enterprise) | Production deployments, async workflows, external integrations (Slack, Teams, etc.) | This guide |
|
||||
|
||||
<Tip>
|
||||
If you're building flows and want to add human review steps with routing based on feedback, check out the [Human Feedback in Flows](/en/learn/human-feedback-in-flows) guide for the `@human_feedback` decorator.
|
||||
</Tip>
|
||||
|
||||
## Setting Up Webhook-Based HITL Workflows
|
||||
## Setting Up HITL Workflows
|
||||
|
||||
<Steps>
|
||||
<Step title="Configure Your Task">
|
||||
|
||||
@@ -565,55 +565,6 @@ Fourth method running
|
||||
|
||||
이 Flow를 실행하면, `start_method`에서 생성된 랜덤 불리언 값에 따라 출력값이 달라집니다.
|
||||
|
||||
### Human in the Loop (인간 피드백)
|
||||
|
||||
`@human_feedback` 데코레이터는 인간의 피드백을 수집하기 위해 플로우 실행을 일시 중지하는 human-in-the-loop 워크플로우를 가능하게 합니다. 이는 승인 게이트, 품질 검토, 인간의 판단이 필요한 결정 지점에 유용합니다.
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback, HumanFeedbackResult
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="이 콘텐츠를 승인하시겠습니까?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def generate_content(self):
|
||||
return "검토할 콘텐츠..."
|
||||
|
||||
@listen("approved")
|
||||
def on_approval(self, result: HumanFeedbackResult):
|
||||
print(f"승인됨! 피드백: {result.feedback}")
|
||||
|
||||
@listen("rejected")
|
||||
def on_rejection(self, result: HumanFeedbackResult):
|
||||
print(f"거부됨. 이유: {result.feedback}")
|
||||
```
|
||||
|
||||
`emit`이 지정되면, 인간의 자유 형식 피드백이 LLM에 의해 해석되어 지정된 outcome 중 하나로 매핑되고, 해당 `@listen` 데코레이터를 트리거합니다.
|
||||
|
||||
라우팅 없이 단순히 피드백만 수집할 수도 있습니다:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(message="이 출력에 대한 코멘트가 있으신가요?")
|
||||
def my_method(self):
|
||||
return "검토할 출력"
|
||||
|
||||
@listen(my_method)
|
||||
def next_step(self, result: HumanFeedbackResult):
|
||||
# result.feedback로 피드백에 접근
|
||||
# result.output으로 원래 출력에 접근
|
||||
pass
|
||||
```
|
||||
|
||||
플로우 실행 중 수집된 모든 피드백은 `self.last_human_feedback` (가장 최근) 또는 `self.human_feedback_history` (리스트 형태의 모든 피드백)를 통해 접근할 수 있습니다.
|
||||
|
||||
플로우에서의 인간 피드백에 대한 완전한 가이드는 비동기/논블로킹 피드백과 커스텀 프로바이더(Slack, 웹훅 등)를 포함하여 [Flow에서 인간 피드백](/ko/learn/human-feedback-in-flows)을 참조하세요.
|
||||
|
||||
## 플로우에 에이전트 추가하기
|
||||
|
||||
에이전트는 플로우에 원활하게 통합할 수 있으며, 단순하고 집중된 작업 실행이 필요할 때 전체 Crew의 경량 대안으로 활용됩니다. 아래는 에이전트를 플로우 내에서 사용하여 시장 조사를 수행하는 예시입니다:
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
title: 프로덕션 아키텍처
|
||||
description: CrewAI로 프로덕션 수준의 AI 애플리케이션을 구축하기 위한 모범 사례
|
||||
icon: server
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# Flow 우선 사고방식 (Flow-First Mindset)
|
||||
|
||||
CrewAI로 프로덕션 AI 애플리케이션을 구축할 때는 **Flow로 시작하는 것을 권장합니다**.
|
||||
|
||||
개별 Crews나 Agents를 실행하는 것도 가능하지만, 이를 Flow로 감싸면 견고하고 확장 가능한 애플리케이션에 필요한 구조를 제공합니다.
|
||||
|
||||
## 왜 Flows인가?
|
||||
|
||||
1. **상태 관리 (State Management)**: Flows는 애플리케이션의 여러 단계에 걸쳐 상태를 관리하는 내장된 방법을 제공합니다. 이는 Crews 간에 데이터를 전달하고, 컨텍스트를 유지하며, 사용자 입력을 처리하는 데 중요합니다.
|
||||
2. **제어 (Control)**: Flows를 사용하면 루프, 조건문, 분기 로직을 포함한 정확한 실행 경로를 정의할 수 있습니다. 이는 예외 상황을 처리하고 애플리케이션이 예측 가능하게 동작하도록 보장하는 데 필수적입니다.
|
||||
3. **관측 가능성 (Observability)**: Flows는 실행을 추적하고, 문제를 디버깅하며, 성능을 모니터링하기 쉽게 만드는 명확한 구조를 제공합니다. 자세한 통찰력을 얻으려면 [CrewAI Tracing](/ko/observability/tracing)을 사용하는 것이 좋습니다. `crewai login`을 실행하여 무료 관측 가능성 기능을 활성화하세요.
|
||||
|
||||
## 아키텍처
|
||||
|
||||
일반적인 프로덕션 CrewAI 애플리케이션은 다음과 같습니다:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Start((시작)) --> Flow[Flow 오케스트레이터]
|
||||
Flow --> State{상태 관리}
|
||||
State --> Step1[1단계: 데이터 수집]
|
||||
Step1 --> Crew1[연구 Crew]
|
||||
Crew1 --> State
|
||||
State --> Step2{조건 확인}
|
||||
Step2 -- "유효함" --> Step3[3단계: 실행]
|
||||
Step3 --> Crew2[액션 Crew]
|
||||
Step2 -- "유효하지 않음" --> End((종료))
|
||||
Crew2 --> End
|
||||
```
|
||||
|
||||
### 1. Flow 클래스
|
||||
`Flow` 클래스는 진입점입니다. 상태 스키마와 로직을 실행하는 메서드를 정의합니다.
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AppState(BaseModel):
|
||||
user_input: str = ""
|
||||
research_results: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
@start()
|
||||
def gather_input(self):
|
||||
# ... 입력 받는 로직 ...
|
||||
pass
|
||||
|
||||
@listen(gather_input)
|
||||
def run_research_crew(self):
|
||||
# ... Crew 트리거 ...
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 상태 관리 (State Management)
|
||||
Pydantic 모델을 사용하여 상태를 정의하세요. 이는 타입 안전성을 보장하고 각 단계에서 어떤 데이터를 사용할 수 있는지 명확하게 합니다.
|
||||
|
||||
- **최소한으로 유지**: 단계 간에 유지해야 할 것만 저장하세요.
|
||||
- **구조화된 데이터 사용**: 가능하면 비구조화된 딕셔너리는 피하세요.
|
||||
|
||||
### 3. 작업 단위로서의 Crews
|
||||
복잡한 작업은 Crews에게 위임하세요. Crew는 특정 목표(예: "주제 연구", "블로그 게시물 작성")에 집중해야 합니다.
|
||||
|
||||
- **Crews를 과도하게 설계하지 마세요**: 집중력을 유지하세요.
|
||||
- **상태를 명시적으로 전달하세요**: Flow 상태에서 필요한 데이터를 Crew 입력으로 전달하세요.
|
||||
|
||||
```python
|
||||
@listen(gather_input)
|
||||
def run_research_crew(self):
|
||||
crew = ResearchCrew()
|
||||
result = crew.kickoff(inputs={"topic": self.state.user_input})
|
||||
self.state.research_results = result.raw
|
||||
```
|
||||
|
||||
## Control Primitives
|
||||
|
||||
CrewAI의 Control Primitives를 활용하여 Crew에 견고함과 제어력을 더하세요.
|
||||
|
||||
### 1. Task Guardrails
|
||||
[Task Guardrails](/ko/concepts/tasks#task-guardrails)를 사용하여 작업 결과가 수락되기 전에 유효성을 검사하세요. 이를 통해 agent가 고품질 결과를 생성하도록 보장할 수 있습니다.
|
||||
|
||||
```python
|
||||
def validate_content(result: TaskOutput) -> Tuple[bool, Any]:
|
||||
if len(result.raw) < 100:
|
||||
return (False, "Content is too short. Please expand.")
|
||||
return (True, result.raw)
|
||||
|
||||
task = Task(
|
||||
...,
|
||||
guardrail=validate_content
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 구조화된 출력 (Structured Outputs)
|
||||
작업 간에 데이터를 전달하거나 애플리케이션으로 전달할 때는 항상 구조화된 출력(`output_pydantic` 또는 `output_json`)을 사용하세요. 이는 파싱 오류를 방지하고 타입 안전성을 보장합니다.
|
||||
|
||||
```python
|
||||
class ResearchResult(BaseModel):
|
||||
summary: str
|
||||
sources: List[str]
|
||||
|
||||
task = Task(
|
||||
...,
|
||||
output_pydantic=ResearchResult
|
||||
)
|
||||
```
|
||||
|
||||
### 3. LLM Hooks
|
||||
[LLM Hooks](/ko/learn/llm-hooks)를 사용하여 LLM으로 전송되기 전에 메시지를 검사하거나 수정하고, 응답을 정리(sanitize)하세요.
|
||||
|
||||
```python
|
||||
@before_llm_call
|
||||
def log_request(context):
|
||||
print(f"Agent {context.agent.role} is calling the LLM...")
|
||||
```
|
||||
|
||||
## 배포 패턴
|
||||
|
||||
Flow를 배포할 때 다음을 고려하세요:
|
||||
|
||||
### CrewAI Enterprise
|
||||
Flow를 배포하는 가장 쉬운 방법은 CrewAI Enterprise를 사용하는 것입니다. 인프라, 인증 및 모니터링을 대신 처리합니다.
|
||||
|
||||
시작하려면 [배포 가이드](/ko/enterprise/guides/deploy-crew)를 확인하세요.
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
### 비동기 실행 (Async Execution)
|
||||
장기 실행 작업의 경우 `kickoff_async`를 사용하여 API 차단을 방지하세요.
|
||||
|
||||
### 지속성 (Persistence)
|
||||
`@persist` 데코레이터를 사용하여 Flow의 상태를 데이터베이스에 저장하세요. 이를 통해 프로세스가 중단되거나 사람의 입력을 기다려야 할 때 실행을 재개할 수 있습니다.
|
||||
|
||||
```python
|
||||
@persist
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
## 요약
|
||||
|
||||
- **Flow로 시작하세요.**
|
||||
- **명확한 State를 정의하세요.**
|
||||
- **복잡한 작업에는 Crews를 사용하세요.**
|
||||
- **API와 지속성을 갖추어 배포하세요.**
|
||||
@@ -62,13 +62,13 @@ CrewAI CLI를 사용하여 Gmail 트리거 통합을 로컬에서 테스트하
|
||||
crewai triggers list
|
||||
|
||||
# 실제 payload로 Gmail 트리거 시뮬레이션
|
||||
crewai triggers run gmail/new_email_received
|
||||
crewai triggers run gmail/new_email
|
||||
```
|
||||
|
||||
`crewai triggers run` 명령은 완전한 Gmail payload로 크루를 실행하여 배포 전에 파싱 로직을 테스트할 수 있게 해줍니다.
|
||||
|
||||
<Warning>
|
||||
개발 중에는 `crewai triggers run gmail/new_email_received`을 사용하세요 (`crewai run`이 아님). 배포 후에는 크루가 자동으로 트리거 payload를 받습니다.
|
||||
개발 중에는 `crewai triggers run gmail/new_email`을 사용하세요 (`crewai run`이 아님). 배포 후에는 크루가 자동으로 트리거 payload를 받습니다.
|
||||
</Warning>
|
||||
|
||||
## Monitoring Executions
|
||||
@@ -83,6 +83,6 @@ Track history and performance of triggered runs:
|
||||
|
||||
- Ensure Gmail is connected in Tools & Integrations
|
||||
- Verify the Gmail Trigger is enabled on the Triggers tab
|
||||
- `crewai triggers run gmail/new_email_received`로 로컬 테스트하여 정확한 payload 구조를 확인하세요
|
||||
- `crewai triggers run gmail/new_email`로 로컬 테스트하여 정확한 payload 구조를 확인하세요
|
||||
- Check the execution logs and confirm the payload is passed as `crewai_trigger_payload`
|
||||
- 주의: 트리거 실행을 시뮬레이션하려면 `crewai triggers run`을 사용하세요 (`crewai run`이 아님)
|
||||
|
||||
@@ -7,89 +7,109 @@ mode: "wide"
|
||||
|
||||
# CrewAI란 무엇인가?
|
||||
|
||||
**CrewAI는 자율 AI agent를 조직하고 복잡한 workflow를 구축하기 위한 최고의 오픈 소스 프레임워크입니다.**
|
||||
**CrewAI는 LangChain이나 기타 agent 프레임워크에 의존하지 않고, 완전히 독립적으로 처음부터 스크래치로 개발된 가볍고 매우 빠른 Python 프레임워크입니다.**
|
||||
|
||||
**Crews**의 협업 지능과 **Flows**의 정밀한 제어를 결합하여 개발자가 프로덕션 수준의 멀티 에이전트 시스템을 구축할 수 있도록 지원합니다.
|
||||
CrewAI는 고수준의 간편함과 정밀한 저수준 제어를 모두 제공하여, 어떤 시나리오에도 맞춤화된 자율 AI agent를 만드는 데 이상적입니다:
|
||||
|
||||
- **[CrewAI Flows](/ko/guides/flows/first-flow)**: AI 애플리케이션의 중추(Backbone)입니다. Flows를 사용하면 상태를 관리하고 실행을 제어하는 구조화된 이벤트 기반 workflow를 만들 수 있습니다. AI agent가 작업할 수 있는 기반을 제공합니다.
|
||||
- **[CrewAI Crews](/ko/guides/crews/first-crew)**: Flow 내의 작업 단위입니다. Crews는 Flow가 위임한 특정 작업을 해결하기 위해 협력하는 자율 agent 팀입니다.
|
||||
- **[CrewAI Crews](/ko/guides/crews/first-crew)**: 자율성과 협업 지능을 극대화하여, 각 agent가 특정 역할, 도구, 목표를 가진 AI 팀을 만들 수 있습니다.
|
||||
- **[CrewAI Flows](/ko/guides/flows/first-flow)**: 이벤트 기반의 세밀한 제어와 단일 LLM 호출을 통한 정확한 작업 orchestration을 지원하며, Crews와 네이티브로 통합됩니다.
|
||||
|
||||
10만 명이 넘는 개발자가 커뮤니티 과정을 통해 인증을 받았으며, CrewAI는 기업용 AI 자동화의 표준입니다.
|
||||
10만 명이 넘는 개발자가 커뮤니티 과정을 통해 인증을 받았으며, CrewAI는 기업용 AI 자동화의 표준으로 빠르게 자리잡고 있습니다.
|
||||
|
||||
## CrewAI 아키텍처
|
||||
|
||||
CrewAI의 아키텍처는 자율성과 제어의 균형을 맞추도록 설계되었습니다.
|
||||
|
||||
### 1. Flows: 중추 (Backbone)
|
||||
## Crew의 작동 방식
|
||||
|
||||
<Note>
|
||||
Flow를 애플리케이션의 "관리자" 또는 "프로세스 정의"라고 생각하세요. 단계, 로직, 그리고 시스템 내에서 데이터가 이동하는 방식을 정의합니다.
|
||||
</Note>
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/flows.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
Flows의 기능:
|
||||
- **상태 관리**: 단계 및 실행 전반에 걸쳐 데이터를 유지합니다.
|
||||
- **이벤트 기반 실행**: 이벤트 또는 외부 입력을 기반으로 작업을 트리거합니다.
|
||||
- **제어 흐름**: 조건부 로직, 반복문, 분기를 사용합니다.
|
||||
|
||||
### 2. Crews: 지능 (Intelligence)
|
||||
|
||||
<Note>
|
||||
Crews는 힘든 일을 처리하는 "팀"입니다. Flow 내에서 창의성과 협업이 필요한 복잡한 문제를 해결하기 위해 Crew를 트리거할 수 있습니다.
|
||||
회사가 비즈니스 목표를 달성하기 위해 여러 부서(영업, 엔지니어링, 마케팅 등)가 리더십 아래에서 함께 일하는 것처럼, CrewAI는 복잡한 작업을 달성하기 위해 전문화된 역할의 AI agent들이 협력하는 조직을 만들 수 있도록 도와줍니다.
|
||||
</Note>
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/crews.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
Crews의 기능:
|
||||
- **역할 수행 Agent**: 특정 목표와 도구를 가진 전문 agent입니다.
|
||||
- **자율 협업**: agent들이 협력하여 작업을 해결합니다.
|
||||
- **작업 위임**: agent의 능력에 따라 작업이 할당되고 실행됩니다.
|
||||
| 구성 요소 | 설명 | 주요 특징 |
|
||||
|:----------|:----:|:----------|
|
||||
| **Crew** | 최상위 조직 | • AI agent 팀 관리<br/>• workflow 감독<br/>• 협업 보장<br/>• 결과 전달 |
|
||||
| **AI agents** | 전문 팀원 | • 특정 역할 보유(Researcher, Writer 등)<br/>• 지정된 도구 사용<br/>• 작업 위임 가능<br/>• 자율적 의사결정 가능 |
|
||||
| **Process** | workflow 관리 시스템 | • 협업 패턴 정의<br/>• 작업 할당 제어<br/>• 상호작용 관리<br/>• 효율적 실행 보장 |
|
||||
| **Task** | 개별 할당 | • 명확한 목표 보유<br/>• 특정 도구 사용<br/>• 더 큰 프로세스에 기여<br/>• 실행 가능한 결과 도출 |
|
||||
|
||||
## 전체 작동 방식
|
||||
### 전체 구조의 동작 방식
|
||||
|
||||
1. **Flow**가 이벤트를 트리거하거나 프로세스를 시작합니다.
|
||||
2. **Flow**가 상태를 관리하고 다음에 무엇을 할지 결정합니다.
|
||||
3. **Flow**가 복잡한 작업을 **Crew**에게 위임합니다.
|
||||
4. **Crew**의 agent들이 협력하여 작업을 완료합니다.
|
||||
5. **Crew**가 결과를 **Flow**에 반환합니다.
|
||||
6. **Flow**가 결과를 바탕으로 실행을 계속합니다.
|
||||
1. **Crew**가 전체 운영을 조직합니다
|
||||
2. **AI agents**가 자신들의 전문 작업을 수행합니다
|
||||
3. **Process**가 원활한 협업을 보장합니다
|
||||
4. **Tasks**가 완료되어 목표를 달성합니다
|
||||
|
||||
## 주요 기능
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="프로덕션 등급 Flows" icon="arrow-progress">
|
||||
장기 실행 프로세스와 복잡한 로직을 처리할 수 있는 신뢰할 수 있고 상태를 유지하는 workflow를 구축합니다.
|
||||
</Card>
|
||||
<Card title="자율 Crews" icon="users">
|
||||
높은 수준의 목표를 달성하기 위해 계획하고, 실행하고, 협력할 수 있는 agent 팀을 배포합니다.
|
||||
<Card title="역할 기반 agent" icon="users">
|
||||
Researcher, Analyst, Writer 등 다양한 역할과 전문성, 목표를 가진 agent를 생성할 수 있습니다
|
||||
</Card>
|
||||
<Card title="유연한 도구" icon="screwdriver-wrench">
|
||||
agent를 모든 API, 데이터베이스 또는 로컬 도구에 연결합니다.
|
||||
agent에게 외부 서비스 및 데이터 소스와 상호작용할 수 있는 맞춤형 도구와 API를 제공합니다
|
||||
</Card>
|
||||
<Card title="엔터프라이즈 보안" icon="lock">
|
||||
엔터프라이즈 배포를 위한 보안 및 규정 준수를 고려하여 설계되었습니다.
|
||||
<Card title="지능형 협업" icon="people-arrows">
|
||||
agent들이 함께 작업하며, 인사이트를 공유하고 작업을 조율하여 복잡한 목표를 달성합니다
|
||||
</Card>
|
||||
<Card title="작업 관리" icon="list-check">
|
||||
순차적 또는 병렬 workflow를 정의할 수 있으며, agent가 작업 의존성을 자동으로 처리합니다
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Crews vs Flows 사용 시기
|
||||
## Flow의 작동 원리
|
||||
|
||||
**짧은 답변: 둘 다 사용하세요.**
|
||||
<Note>
|
||||
Crew가 자율 협업에 탁월하다면, Flow는 구조화된 자동화를 제공하여 workflow 실행에 대한 세밀한 제어를 제공합니다. Flow는 조건부 로직, 반복문, 동적 상태 관리를 정확하게 처리하면서 작업이 신뢰성 있게, 안전하게, 효율적으로 실행되도록 보장합니다. Flow는 Crew와 원활하게 통합되어 높은 자율성과 엄격한 제어의 균형을 이룰 수 있게 해줍니다.
|
||||
</Note>
|
||||
|
||||
모든 프로덕션 애플리케이션의 경우, **Flow로 시작하세요**.
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/flows.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
- 애플리케이션의 전체 구조, 상태, 로직을 정의하려면 **Flow를 사용하세요**.
|
||||
- 자율성이 필요한 특정하고 복잡한 작업을 수행하기 위해 agent 팀이 필요할 때 Flow 단계 내에서 **Crew를 사용하세요**.
|
||||
| 구성 요소 | 설명 | 주요 기능 |
|
||||
|:----------|:-----------:|:------------|
|
||||
| **Flow** | 구조화된 workflow orchestration | • 실행 경로 관리<br/>• 상태 전환 처리<br/>• 작업 순서 제어<br/>• 신뢰성 있는 실행 보장 |
|
||||
| **Events** | workflow 액션 트리거 | • 특정 프로세스 시작<br/>• 동적 응답 가능<br/>• 조건부 분기 지원<br/>• 실시간 적응 허용 |
|
||||
| **States** | workflow 실행 컨텍스트 | • 실행 데이터 유지<br/>• 데이터 영속성 지원<br/>• 재개 가능성 보장<br/>• 실행 무결성 확보 |
|
||||
| **Crew Support** | workflow 자동화 강화 | • 필요할 때 agency 삽입<br/>• 구조화된 workflow 보완<br/>• 자동화와 인텔리전스의 균형<br/>• 적응적 의사결정 지원 |
|
||||
|
||||
| 사용 사례 | 아키텍처 |
|
||||
| :--- | :--- |
|
||||
| **간단한 자동화** | Python 작업이 포함된 단일 Flow |
|
||||
| **복잡한 연구** | 상태를 관리하는 Flow -> 연구를 수행하는 Crew |
|
||||
| **애플리케이션 백엔드** | API 요청을 처리하는 Flow -> 콘텐츠를 생성하는 Crew -> DB에 저장하는 Flow |
|
||||
### 주요 기능
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="이벤트 기반 orchestration" icon="bolt">
|
||||
이벤트에 동적으로 반응하여 정밀한 실행 경로를 정의합니다
|
||||
</Card>
|
||||
<Card title="세밀한 제어" icon="sliders">
|
||||
workflow 상태와 조건부 실행을 안전하고 효율적으로 관리합니다
|
||||
</Card>
|
||||
<Card title="네이티브 Crew 통합" icon="puzzle-piece">
|
||||
Crews와 손쉽게 결합하여 자율성과 지능을 강화합니다
|
||||
</Card>
|
||||
<Card title="결정론적 실행" icon="route">
|
||||
명시적 제어 흐름과 오류 처리로 예측 가능한 결과를 보장합니다
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Crew와 Flow를 언제 사용할까
|
||||
|
||||
<Note>
|
||||
[Crew](/ko/guides/crews/first-crew)와 [Flow](/ko/guides/flows/first-flow)를 언제 사용할지 이해하는 것은 CrewAI의 잠재력을 애플리케이션에서 극대화하는 데 핵심적입니다.
|
||||
</Note>
|
||||
|
||||
| 사용 사례 | 권장 접근 방식 | 이유 |
|
||||
|:---------|:---------------------|:-----|
|
||||
| **개방형 연구** | [Crew](/ko/guides/crews/first-crew) | 창의적 사고, 탐색, 적응이 필요한 작업에 적합 |
|
||||
| **콘텐츠 생성** | [Crew](/ko/guides/crews/first-crew) | 기사, 보고서, 마케팅 자료 등 협업형 생성에 적합 |
|
||||
| **의사결정 workflow** | [Flow](/ko/guides/flows/first-flow) | 예측 가능하고 감사 가능한 의사결정 경로 및 정밀 제어가 필요할 때 |
|
||||
| **API orchestration** | [Flow](/ko/guides/flows/first-flow) | 특정 순서로 여러 외부 서비스에 신뢰성 있게 통합할 때 |
|
||||
| **하이브리드 애플리케이션** | 혼합 접근 방식 | [Flow](/ko/guides/flows/first-flow)로 전체 프로세스를 orchestration하고, [Crew](/ko/guides/crews/first-crew)로 복잡한 하위 작업을 처리 |
|
||||
|
||||
### 의사결정 프레임워크
|
||||
|
||||
- **[Crews](/ko/guides/crews/first-crew)를 선택할 때:** 자율적인 문제 해결, 창의적 협업 또는 탐구적 작업이 필요할 때
|
||||
- **[Flows](/ko/guides/flows/first-flow)를 선택할 때:** 결정론적 결과, 감사 가능성, 또는 실행에 대한 정밀한 제어가 필요할 때
|
||||
- **둘 다 결합할 때:** 애플리케이션에 구조화된 프로세스와 자율적 지능이 모두 필요할 때
|
||||
|
||||
## CrewAI를 선택해야 하는 이유?
|
||||
|
||||
@@ -103,13 +123,6 @@ Crews의 기능:
|
||||
## 지금 바로 빌드를 시작해보세요!
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="첫 번째 Flow 만들기"
|
||||
icon="diagram-project"
|
||||
href="/ko/guides/flows/first-flow"
|
||||
>
|
||||
실행을 정밀하게 제어할 수 있는 구조화된, 이벤트 기반 workflow를 만드는 방법을 배워보세요.
|
||||
</Card>
|
||||
<Card
|
||||
title="첫 번째 Crew 만들기"
|
||||
icon="users-gear"
|
||||
@@ -117,6 +130,13 @@ Crews의 기능:
|
||||
>
|
||||
복잡한 문제를 함께 해결하는 협업 AI 팀을 단계별로 만드는 튜토리얼입니다.
|
||||
</Card>
|
||||
<Card
|
||||
title="첫 번째 Flow 만들기"
|
||||
icon="diagram-project"
|
||||
href="/ko/guides/flows/first-flow"
|
||||
>
|
||||
실행을 정밀하게 제어할 수 있는 구조화된, 이벤트 기반 workflow를 만드는 방법을 배워보세요.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
@@ -141,4 +161,4 @@ Crews의 기능:
|
||||
>
|
||||
다른 개발자와 소통하며, 도움을 받고 CrewAI 경험을 공유해보세요.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
</CardGroup>
|
||||
@@ -1,581 +0,0 @@
|
||||
---
|
||||
title: Flow에서 인간 피드백
|
||||
description: "@human_feedback 데코레이터를 사용하여 CrewAI Flow에 인간 피드백을 직접 통합하는 방법을 알아보세요"
|
||||
icon: user-check
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
`@human_feedback` 데코레이터는 CrewAI Flow 내에서 직접 human-in-the-loop(HITL) 워크플로우를 가능하게 합니다. Flow 실행을 일시 중지하고, 인간에게 검토를 위해 출력을 제시하고, 피드백을 수집하고, 선택적으로 피드백 결과에 따라 다른 리스너로 라우팅할 수 있습니다.
|
||||
|
||||
이는 특히 다음과 같은 경우에 유용합니다:
|
||||
|
||||
- **품질 보증**: AI가 생성한 콘텐츠를 다운스트림에서 사용하기 전에 검토
|
||||
- **결정 게이트**: 자동화된 워크플로우에서 인간이 중요한 결정을 내리도록 허용
|
||||
- **승인 워크플로우**: 승인/거부/수정 패턴 구현
|
||||
- **대화형 개선**: 출력을 반복적으로 개선하기 위해 피드백 수집
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Flow 메서드] --> B[출력 생성됨]
|
||||
B --> C[인간이 검토]
|
||||
C --> D{피드백}
|
||||
D -->|emit 지정됨| E[LLM이 Outcome으로 매핑]
|
||||
D -->|emit 없음| F[HumanFeedbackResult]
|
||||
E --> G["@listen('approved')"]
|
||||
E --> H["@listen('rejected')"]
|
||||
F --> I[다음 리스너]
|
||||
```
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
Flow에 인간 피드백을 추가하는 가장 간단한 방법은 다음과 같습니다:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback
|
||||
|
||||
class SimpleReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="이 콘텐츠를 검토해 주세요:")
|
||||
def generate_content(self):
|
||||
return "검토가 필요한 AI 생성 콘텐츠입니다."
|
||||
|
||||
@listen(generate_content)
|
||||
def process_feedback(self, result):
|
||||
print(f"콘텐츠: {result.output}")
|
||||
print(f"인간의 의견: {result.feedback}")
|
||||
|
||||
flow = SimpleReviewFlow()
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
이 Flow를 실행하면:
|
||||
1. `generate_content`를 실행하고 문자열을 반환합니다
|
||||
2. 요청 메시지와 함께 사용자에게 출력을 표시합니다
|
||||
3. 사용자가 피드백을 입력할 때까지 대기합니다 (또는 Enter를 눌러 건너뜁니다)
|
||||
4. `HumanFeedbackResult` 객체를 `process_feedback`에 전달합니다
|
||||
|
||||
## @human_feedback 데코레이터
|
||||
|
||||
### 매개변수
|
||||
|
||||
| 매개변수 | 타입 | 필수 | 설명 |
|
||||
|----------|------|------|------|
|
||||
| `message` | `str` | 예 | 메서드 출력과 함께 인간에게 표시되는 메시지 |
|
||||
| `emit` | `Sequence[str]` | 아니오 | 가능한 outcome 목록. 피드백이 이 중 하나로 매핑되어 `@listen` 데코레이터를 트리거합니다 |
|
||||
| `llm` | `str \| BaseLLM` | `emit` 지정 시 | 피드백을 해석하고 outcome에 매핑하는 데 사용되는 LLM |
|
||||
| `default_outcome` | `str` | 아니오 | 피드백이 제공되지 않을 때 사용할 outcome. `emit`에 있어야 합니다 |
|
||||
| `metadata` | `dict` | 아니오 | 엔터프라이즈 통합을 위한 추가 데이터 |
|
||||
| `provider` | `HumanFeedbackProvider` | 아니오 | 비동기/논블로킹 피드백을 위한 커스텀 프로바이더. [비동기 인간 피드백](#비동기-인간-피드백-논블로킹) 참조 |
|
||||
|
||||
### 기본 사용법 (라우팅 없음)
|
||||
|
||||
`emit`을 지정하지 않으면, 데코레이터는 단순히 피드백을 수집하고 다음 리스너에 `HumanFeedbackResult`를 전달합니다:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(message="이 분석에 대해 어떻게 생각하시나요?")
|
||||
def analyze_data(self):
|
||||
return "분석 결과: 매출 15% 증가, 비용 8% 감소"
|
||||
|
||||
@listen(analyze_data)
|
||||
def handle_feedback(self, result):
|
||||
# result는 HumanFeedbackResult입니다
|
||||
print(f"분석: {result.output}")
|
||||
print(f"피드백: {result.feedback}")
|
||||
```
|
||||
|
||||
### emit을 사용한 라우팅
|
||||
|
||||
`emit`을 지정하면, 데코레이터는 라우터가 됩니다. 인간의 자유 형식 피드백이 LLM에 의해 해석되어 지정된 outcome 중 하나로 매핑됩니다:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="이 콘텐츠의 출판을 승인하시겠습니까?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_content(self):
|
||||
return "블로그 게시물 초안 내용..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
print(f"출판 중! 사용자 의견: {result.feedback}")
|
||||
|
||||
@listen("rejected")
|
||||
def discard(self, result):
|
||||
print(f"폐기됨. 이유: {result.feedback}")
|
||||
|
||||
@listen("needs_revision")
|
||||
def revise(self, result):
|
||||
print(f"다음을 기반으로 수정 중: {result.feedback}")
|
||||
```
|
||||
|
||||
<Tip>
|
||||
LLM은 가능한 경우 구조화된 출력(function calling)을 사용하여 응답이 지정된 outcome 중 하나임을 보장합니다. 이로 인해 라우팅이 신뢰할 수 있고 예측 가능해집니다.
|
||||
</Tip>
|
||||
|
||||
## HumanFeedbackResult
|
||||
|
||||
`HumanFeedbackResult` 데이터클래스는 인간 피드백 상호작용에 대한 모든 정보를 포함합니다:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult
|
||||
|
||||
@dataclass
|
||||
class HumanFeedbackResult:
|
||||
output: Any # 인간에게 표시된 원래 메서드 출력
|
||||
feedback: str # 인간의 원시 피드백 텍스트
|
||||
outcome: str | None # 매핑된 outcome (emit이 지정된 경우)
|
||||
timestamp: datetime # 피드백이 수신된 시간
|
||||
method_name: str # 데코레이터된 메서드의 이름
|
||||
metadata: dict # 데코레이터에 전달된 모든 메타데이터
|
||||
```
|
||||
|
||||
### 리스너에서 접근하기
|
||||
|
||||
`emit`이 있는 `@human_feedback` 메서드에 의해 리스너가 트리거되면, `HumanFeedbackResult`를 받습니다:
|
||||
|
||||
```python Code
|
||||
@listen("approved")
|
||||
def on_approval(self, result: HumanFeedbackResult):
|
||||
print(f"원래 출력: {result.output}")
|
||||
print(f"사용자 피드백: {result.feedback}")
|
||||
print(f"Outcome: {result.outcome}") # "approved"
|
||||
print(f"수신 시간: {result.timestamp}")
|
||||
```
|
||||
|
||||
## 피드백 히스토리 접근하기
|
||||
|
||||
`Flow` 클래스는 인간 피드백에 접근하기 위한 두 가지 속성을 제공합니다:
|
||||
|
||||
### last_human_feedback
|
||||
|
||||
가장 최근의 `HumanFeedbackResult`를 반환합니다:
|
||||
|
||||
```python Code
|
||||
@listen(some_method)
|
||||
def check_feedback(self):
|
||||
if self.last_human_feedback:
|
||||
print(f"마지막 피드백: {self.last_human_feedback.feedback}")
|
||||
```
|
||||
|
||||
### human_feedback_history
|
||||
|
||||
Flow 동안 수집된 모든 `HumanFeedbackResult` 객체의 리스트입니다:
|
||||
|
||||
```python Code
|
||||
@listen(final_step)
|
||||
def summarize(self):
|
||||
print(f"수집된 총 피드백: {len(self.human_feedback_history)}")
|
||||
for i, fb in enumerate(self.human_feedback_history):
|
||||
print(f"{i+1}. {fb.method_name}: {fb.outcome or '라우팅 없음'}")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
각 `HumanFeedbackResult`는 `human_feedback_history`에 추가되므로, 여러 피드백 단계가 서로 덮어쓰지 않습니다. 이 리스트를 사용하여 Flow 동안 수집된 모든 피드백에 접근하세요.
|
||||
</Warning>
|
||||
|
||||
## 완전한 예제: 콘텐츠 승인 워크플로우
|
||||
|
||||
콘텐츠 검토 및 승인 워크플로우를 구현하는 전체 예제입니다:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback, HumanFeedbackResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ContentState(BaseModel):
|
||||
topic: str = ""
|
||||
draft: str = ""
|
||||
final_content: str = ""
|
||||
revision_count: int = 0
|
||||
|
||||
|
||||
class ContentApprovalFlow(Flow[ContentState]):
|
||||
"""콘텐츠를 생성하고 인간의 승인을 받는 Flow입니다."""
|
||||
|
||||
@start()
|
||||
def get_topic(self):
|
||||
self.state.topic = input("어떤 주제에 대해 글을 쓸까요? ")
|
||||
return self.state.topic
|
||||
|
||||
@listen(get_topic)
|
||||
def generate_draft(self, topic):
|
||||
# 실제 사용에서는 LLM을 호출합니다
|
||||
self.state.draft = f"# {topic}\n\n{topic}에 대한 초안입니다..."
|
||||
return self.state.draft
|
||||
|
||||
@listen(generate_draft)
|
||||
@human_feedback(
|
||||
message="이 초안을 검토해 주세요. 'approved', 'rejected'로 답하거나 수정 피드백을 제공해 주세요:",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_draft(self, draft):
|
||||
return draft
|
||||
|
||||
@listen("approved")
|
||||
def publish_content(self, result: HumanFeedbackResult):
|
||||
self.state.final_content = result.output
|
||||
print("\n✅ 콘텐츠가 승인되어 출판되었습니다!")
|
||||
print(f"검토자 코멘트: {result.feedback}")
|
||||
return "published"
|
||||
|
||||
@listen("rejected")
|
||||
def handle_rejection(self, result: HumanFeedbackResult):
|
||||
print("\n❌ 콘텐츠가 거부되었습니다")
|
||||
print(f"이유: {result.feedback}")
|
||||
return "rejected"
|
||||
|
||||
@listen("needs_revision")
|
||||
def revise_content(self, result: HumanFeedbackResult):
|
||||
self.state.revision_count += 1
|
||||
print(f"\n📝 수정 #{self.state.revision_count} 요청됨")
|
||||
print(f"피드백: {result.feedback}")
|
||||
|
||||
# 실제 Flow에서는 generate_draft로 돌아갈 수 있습니다
|
||||
# 이 예제에서는 단순히 확인합니다
|
||||
return "revision_requested"
|
||||
|
||||
|
||||
# Flow 실행
|
||||
flow = ContentApprovalFlow()
|
||||
result = flow.kickoff()
|
||||
print(f"\nFlow 완료. 요청된 수정: {flow.state.revision_count}")
|
||||
```
|
||||
|
||||
```text Output
|
||||
어떤 주제에 대해 글을 쓸까요? AI 안전
|
||||
|
||||
==================================================
|
||||
OUTPUT FOR REVIEW:
|
||||
==================================================
|
||||
# AI 안전
|
||||
|
||||
AI 안전에 대한 초안입니다...
|
||||
==================================================
|
||||
|
||||
이 초안을 검토해 주세요. 'approved', 'rejected'로 답하거나 수정 피드백을 제공해 주세요:
|
||||
(Press Enter to skip, or type your feedback)
|
||||
|
||||
Your feedback: 좋아 보입니다, 승인!
|
||||
|
||||
✅ 콘텐츠가 승인되어 출판되었습니다!
|
||||
검토자 코멘트: 좋아 보입니다, 승인!
|
||||
|
||||
Flow 완료. 요청된 수정: 0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## 다른 데코레이터와 결합하기
|
||||
|
||||
`@human_feedback` 데코레이터는 다른 Flow 데코레이터와 함께 작동합니다. 가장 안쪽 데코레이터(함수에 가장 가까운)로 배치하세요:
|
||||
|
||||
```python Code
|
||||
# 올바름: @human_feedback이 가장 안쪽(함수에 가장 가까움)
|
||||
@start()
|
||||
@human_feedback(message="이것을 검토해 주세요:")
|
||||
def my_start_method(self):
|
||||
return "content"
|
||||
|
||||
@listen(other_method)
|
||||
@human_feedback(message="이것도 검토해 주세요:")
|
||||
def my_listener(self, data):
|
||||
return f"processed: {data}"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
`@human_feedback`를 가장 안쪽 데코레이터(마지막/함수에 가장 가까움)로 배치하여 메서드를 직접 래핑하고 Flow 시스템에 전달하기 전에 반환 값을 캡처할 수 있도록 하세요.
|
||||
</Tip>
|
||||
|
||||
## 모범 사례
|
||||
|
||||
### 1. 명확한 요청 메시지 작성
|
||||
|
||||
`message` 매개변수는 인간이 보는 것입니다. 실행 가능하게 만드세요:
|
||||
|
||||
```python Code
|
||||
# ✅ 좋음 - 명확하고 실행 가능
|
||||
@human_feedback(message="이 요약이 핵심 포인트를 정확하게 캡처했나요? '예'로 답하거나 무엇이 빠졌는지 설명해 주세요:")
|
||||
|
||||
# ❌ 나쁨 - 모호함
|
||||
@human_feedback(message="이것을 검토해 주세요:")
|
||||
```
|
||||
|
||||
### 2. 의미 있는 Outcome 선택
|
||||
|
||||
`emit`을 사용할 때, 인간의 응답에 자연스럽게 매핑되는 outcome을 선택하세요:
|
||||
|
||||
```python Code
|
||||
# ✅ 좋음 - 자연어 outcome
|
||||
emit=["approved", "rejected", "needs_more_detail"]
|
||||
|
||||
# ❌ 나쁨 - 기술적이거나 불명확
|
||||
emit=["state_1", "state_2", "state_3"]
|
||||
```
|
||||
|
||||
### 3. 항상 기본 Outcome 제공
|
||||
|
||||
사용자가 입력 없이 Enter를 누르는 경우를 처리하기 위해 `default_outcome`을 사용하세요:
|
||||
|
||||
```python Code
|
||||
@human_feedback(
|
||||
message="승인하시겠습니까? (수정 요청하려면 Enter 누르세요)",
|
||||
emit=["approved", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision", # 안전한 기본값
|
||||
)
|
||||
```
|
||||
|
||||
### 4. 감사 추적을 위한 피드백 히스토리 사용
|
||||
|
||||
감사 로그를 생성하기 위해 `human_feedback_history`에 접근하세요:
|
||||
|
||||
```python Code
|
||||
@listen(final_step)
|
||||
def create_audit_log(self):
|
||||
log = []
|
||||
for fb in self.human_feedback_history:
|
||||
log.append({
|
||||
"step": fb.method_name,
|
||||
"outcome": fb.outcome,
|
||||
"feedback": fb.feedback,
|
||||
"timestamp": fb.timestamp.isoformat(),
|
||||
})
|
||||
return log
|
||||
```
|
||||
|
||||
### 5. 라우팅된 피드백과 라우팅되지 않은 피드백 모두 처리
|
||||
|
||||
Flow를 설계할 때, 라우팅이 필요한지 고려하세요:
|
||||
|
||||
| 시나리오 | 사용 |
|
||||
|----------|------|
|
||||
| 간단한 검토, 피드백 텍스트만 필요 | `emit` 없음 |
|
||||
| 응답에 따라 다른 경로로 분기 필요 | `emit` 사용 |
|
||||
| 승인/거부/수정이 있는 승인 게이트 | `emit` 사용 |
|
||||
| 로깅만을 위한 코멘트 수집 | `emit` 없음 |
|
||||
|
||||
## 비동기 인간 피드백 (논블로킹)
|
||||
|
||||
기본적으로 `@human_feedback`은 콘솔 입력을 기다리며 실행을 차단합니다. 프로덕션 애플리케이션에서는 Slack, 이메일, 웹훅 또는 API와 같은 외부 시스템과 통합되는 **비동기/논블로킹** 피드백이 필요할 수 있습니다.
|
||||
|
||||
### Provider 추상화
|
||||
|
||||
커스텀 피드백 수집 전략을 지정하려면 `provider` 매개변수를 사용하세요:
|
||||
|
||||
```python Code
|
||||
from crewai.flow import Flow, start, human_feedback, HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext
|
||||
|
||||
class WebhookProvider(HumanFeedbackProvider):
|
||||
"""웹훅 콜백을 기다리며 Flow를 일시 중지하는 Provider."""
|
||||
|
||||
def __init__(self, webhook_url: str):
|
||||
self.webhook_url = webhook_url
|
||||
|
||||
def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
|
||||
# 외부 시스템에 알림 (예: Slack 메시지 전송, 티켓 생성)
|
||||
self.send_notification(context)
|
||||
|
||||
# 실행 일시 중지 - 프레임워크가 자동으로 영속성 처리
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={"webhook_url": f"{self.webhook_url}/{context.flow_id}"}
|
||||
)
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="이 콘텐츠를 검토해 주세요:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
provider=WebhookProvider("https://myapp.com/api"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "AI가 생성한 콘텐츠..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
return "출판됨!"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Flow 프레임워크는 `HumanFeedbackPending`이 발생하면 **자동으로 상태를 영속화**합니다. Provider는 외부 시스템에 알리고 예외를 발생시키기만 하면 됩니다—수동 영속성 호출이 필요하지 않습니다.
|
||||
</Tip>
|
||||
|
||||
### 일시 중지된 Flow 처리
|
||||
|
||||
비동기 provider를 사용하면 `kickoff()`는 예외를 발생시키는 대신 `HumanFeedbackPending` 객체를 반환합니다:
|
||||
|
||||
```python Code
|
||||
flow = ReviewFlow()
|
||||
result = flow.kickoff()
|
||||
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
# Flow가 일시 중지됨, 상태가 자동으로 영속화됨
|
||||
print(f"피드백 대기 중: {result.callback_info['webhook_url']}")
|
||||
print(f"Flow ID: {result.context.flow_id}")
|
||||
else:
|
||||
# 정상 완료
|
||||
print(f"Flow 완료: {result}")
|
||||
```
|
||||
|
||||
### 일시 중지된 Flow 재개
|
||||
|
||||
피드백이 도착하면 (예: 웹훅을 통해) Flow를 재개합니다:
|
||||
|
||||
```python Code
|
||||
# 동기 핸들러:
|
||||
def handle_feedback_webhook(flow_id: str, feedback: str):
|
||||
flow = ReviewFlow.from_pending(flow_id)
|
||||
result = flow.resume(feedback)
|
||||
return result
|
||||
|
||||
# 비동기 핸들러 (FastAPI, aiohttp 등):
|
||||
async def handle_feedback_webhook(flow_id: str, feedback: str):
|
||||
flow = ReviewFlow.from_pending(flow_id)
|
||||
result = await flow.resume_async(feedback)
|
||||
return result
|
||||
```
|
||||
|
||||
### 주요 타입
|
||||
|
||||
| 타입 | 설명 |
|
||||
|------|------|
|
||||
| `HumanFeedbackProvider` | 커스텀 피드백 provider를 위한 프로토콜 |
|
||||
| `PendingFeedbackContext` | 일시 중지된 Flow를 재개하는 데 필요한 모든 정보 포함 |
|
||||
| `HumanFeedbackPending` | Flow가 피드백을 위해 일시 중지되면 `kickoff()`에서 반환됨 |
|
||||
| `ConsoleProvider` | 기본 블로킹 콘솔 입력 provider |
|
||||
|
||||
### PendingFeedbackContext
|
||||
|
||||
컨텍스트는 재개에 필요한 모든 것을 포함합니다:
|
||||
|
||||
```python Code
|
||||
@dataclass
|
||||
class PendingFeedbackContext:
|
||||
flow_id: str # 이 Flow 실행의 고유 식별자
|
||||
flow_class: str # 정규화된 클래스 이름
|
||||
method_name: str # 피드백을 트리거한 메서드
|
||||
method_output: Any # 인간에게 표시된 출력
|
||||
message: str # 요청 메시지
|
||||
emit: list[str] | None # 라우팅을 위한 가능한 outcome
|
||||
default_outcome: str | None
|
||||
metadata: dict # 커스텀 메타데이터
|
||||
llm: str | None # outcome 매핑을 위한 LLM
|
||||
requested_at: datetime
|
||||
```
|
||||
|
||||
### 완전한 비동기 Flow 예제
|
||||
|
||||
```python Code
|
||||
from crewai.flow import (
|
||||
Flow, start, listen, human_feedback,
|
||||
HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext
|
||||
)
|
||||
|
||||
class SlackNotificationProvider(HumanFeedbackProvider):
|
||||
"""Slack 알림을 보내고 비동기 피드백을 위해 일시 중지하는 Provider."""
|
||||
|
||||
def __init__(self, channel: str):
|
||||
self.channel = channel
|
||||
|
||||
def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
|
||||
# Slack 알림 전송 (직접 구현)
|
||||
slack_thread_id = self.post_to_slack(
|
||||
channel=self.channel,
|
||||
message=f"검토 필요:\n\n{context.method_output}\n\n{context.message}",
|
||||
)
|
||||
|
||||
# 실행 일시 중지 - 프레임워크가 자동으로 영속성 처리
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={
|
||||
"slack_channel": self.channel,
|
||||
"thread_id": slack_thread_id,
|
||||
}
|
||||
)
|
||||
|
||||
class ContentPipeline(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="이 콘텐츠의 출판을 승인하시겠습니까?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
provider=SlackNotificationProvider("#content-reviews"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "AI가 생성한 블로그 게시물 콘텐츠..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
print(f"출판 중! 검토자 의견: {result.feedback}")
|
||||
return {"status": "published"}
|
||||
|
||||
@listen("rejected")
|
||||
def archive(self, result):
|
||||
print(f"보관됨. 이유: {result.feedback}")
|
||||
return {"status": "archived"}
|
||||
|
||||
@listen("needs_revision")
|
||||
def queue_revision(self, result):
|
||||
print(f"수정 대기열에 추가됨: {result.feedback}")
|
||||
return {"status": "revision_needed"}
|
||||
|
||||
|
||||
# Flow 시작 (Slack 응답을 기다리며 일시 중지)
|
||||
def start_content_pipeline():
|
||||
flow = ContentPipeline()
|
||||
result = flow.kickoff()
|
||||
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
return {"status": "pending", "flow_id": result.context.flow_id}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Slack 웹훅이 실행될 때 재개 (동기 핸들러)
|
||||
def on_slack_feedback(flow_id: str, slack_message: str):
|
||||
flow = ContentPipeline.from_pending(flow_id)
|
||||
result = flow.resume(slack_message)
|
||||
return result
|
||||
|
||||
|
||||
# 핸들러가 비동기인 경우 (FastAPI, aiohttp, Slack Bolt 비동기 등)
|
||||
async def on_slack_feedback_async(flow_id: str, slack_message: str):
|
||||
flow = ContentPipeline.from_pending(flow_id)
|
||||
result = await flow.resume_async(slack_message)
|
||||
return result
|
||||
```
|
||||
|
||||
<Warning>
|
||||
비동기 웹 프레임워크(FastAPI, aiohttp, Slack Bolt 비동기 모드)를 사용하는 경우 `flow.resume()` 대신 `await flow.resume_async()`를 사용하세요. 실행 중인 이벤트 루프 내에서 `resume()`을 호출하면 `RuntimeError`가 발생합니다.
|
||||
</Warning>
|
||||
|
||||
### 비동기 피드백 모범 사례
|
||||
|
||||
1. **반환 타입 확인**: `kickoff()`는 일시 중지되면 `HumanFeedbackPending`을 반환합니다—try/except가 필요하지 않습니다
|
||||
2. **올바른 resume 메서드 사용**: 동기 코드에서는 `resume()`, 비동기 코드에서는 `await resume_async()` 사용
|
||||
3. **콜백 정보 저장**: `callback_info`를 사용하여 웹훅 URL, 티켓 ID 등을 저장
|
||||
4. **멱등성 구현**: 안전을 위해 resume 핸들러는 멱등해야 합니다
|
||||
5. **자동 영속성**: `HumanFeedbackPending`이 발생하면 상태가 자동으로 저장되며 기본적으로 `SQLiteFlowPersistence` 사용
|
||||
6. **커스텀 영속성**: 필요한 경우 `from_pending()`에 커스텀 영속성 인스턴스 전달
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [Flow 개요](/ko/concepts/flows) - CrewAI Flow에 대해 알아보기
|
||||
- [Flow 상태 관리](/ko/guides/flows/mastering-flow-state) - Flow에서 상태 관리하기
|
||||
- [Flow 영속성](/ko/concepts/flows#persistence) - Flow 상태 영속화
|
||||
- [@router를 사용한 라우팅](/ko/concepts/flows#router) - 조건부 라우팅에 대해 더 알아보기
|
||||
- [실행 시 인간 입력](/ko/learn/human-input-on-execution) - 태스크 수준 인간 입력
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
title: CrewAI Tracing
|
||||
description: CrewAI AOP 플랫폼을 사용한 CrewAI Crews 및 Flows의 내장 추적
|
||||
icon: magnifying-glass-chart
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# CrewAI 내장 추적 (Built-in Tracing)
|
||||
|
||||
CrewAI는 Crews와 Flows를 실시간으로 모니터링하고 디버깅할 수 있는 내장 추적 기능을 제공합니다. 이 가이드는 CrewAI의 통합 관측 가능성 플랫폼을 사용하여 **Crews**와 **Flows** 모두에 대한 추적을 활성화하는 방법을 보여줍니다.
|
||||
|
||||
> **CrewAI Tracing이란?** CrewAI의 내장 추적은 agent 결정, 작업 실행 타임라인, 도구 사용, LLM 호출을 포함한 AI agent에 대한 포괄적인 관측 가능성을 제공하며, 모두 [CrewAI AOP 플랫폼](https://app.crewai.com)을 통해 액세스할 수 있습니다.
|
||||
|
||||

|
||||
|
||||
## 사전 요구 사항
|
||||
|
||||
CrewAI 추적을 사용하기 전에 다음이 필요합니다:
|
||||
|
||||
1. **CrewAI AOP 계정**: [app.crewai.com](https://app.crewai.com)에서 무료 계정에 가입하세요
|
||||
2. **CLI 인증**: CrewAI CLI를 사용하여 로컬 환경을 인증하세요
|
||||
|
||||
```bash
|
||||
crewai login
|
||||
```
|
||||
|
||||
## 설정 지침
|
||||
|
||||
### 1단계: CrewAI AOP 계정 생성
|
||||
|
||||
[app.crewai.com](https://app.crewai.com)을 방문하여 무료 계정을 만드세요. 이를 통해 추적, 메트릭을 보고 crews를 관리할 수 있는 CrewAI AOP 플랫폼에 액세스할 수 있습니다.
|
||||
|
||||
### 2단계: CrewAI CLI 설치 및 인증
|
||||
|
||||
아직 설치하지 않았다면 CLI 도구와 함께 CrewAI를 설치하세요:
|
||||
|
||||
```bash
|
||||
uv add crewai[tools]
|
||||
```
|
||||
|
||||
그런 다음 CrewAI AOP 계정으로 CLI를 인증하세요:
|
||||
|
||||
```bash
|
||||
crewai login
|
||||
```
|
||||
|
||||
이 명령은 다음을 수행합니다:
|
||||
1. 브라우저에서 인증 페이지를 엽니다
|
||||
2. 장치 코드를 입력하라는 메시지를 표시합니다
|
||||
3. CrewAI AOP 계정으로 로컬 환경을 인증합니다
|
||||
4. 로컬 개발을 위한 추적 기능을 활성화합니다
|
||||
|
||||
### 3단계: Crew에서 추적 활성화
|
||||
|
||||
`tracing` 매개변수를 `True`로 설정하여 Crew에 대한 추적을 활성화할 수 있습니다:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai_tools import SerperDevTool
|
||||
|
||||
# Define your agents
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Uncover cutting-edge developments in AI and data science",
|
||||
backstory=\"\"\"You work at a leading tech think tank.
|
||||
Your expertise lies in identifying emerging trends.
|
||||
You have a knack for dissecting complex data and presenting actionable insights.\"\"\",
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()],
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
role="Tech Content Strategist",
|
||||
goal="Craft compelling content on tech advancements",
|
||||
backstory=\"\"\"You are a renowned Content Strategist, known for your insightful and engaging articles.
|
||||
You transform complex concepts into compelling narratives.\"\"\",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create tasks for your agents
|
||||
research_task = Task(
|
||||
description=\"\"\"Conduct a comprehensive analysis of the latest advancements in AI in 2024.
|
||||
Identify key trends, breakthrough technologies, and potential industry impacts.\"\"\",
|
||||
expected_output="Full analysis report in bullet points",
|
||||
agent=researcher,
|
||||
)
|
||||
|
||||
writing_task = Task(
|
||||
description=\"\"\"Using the insights provided, develop an engaging blog
|
||||
post that highlights the most significant AI advancements.
|
||||
Your post should be informative yet accessible, catering to a tech-savvy audience.\"\"\",
|
||||
expected_output="Full blog post of at least 4 paragraphs",
|
||||
agent=writer,
|
||||
)
|
||||
|
||||
# Enable tracing in your crew
|
||||
crew = Crew(
|
||||
agents=[researcher, writer],
|
||||
tasks=[research_task, writing_task],
|
||||
process=Process.sequential,
|
||||
tracing=True, # Enable built-in tracing
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Execute your crew
|
||||
result = crew.kickoff()
|
||||
```
|
||||
|
||||
### 4단계: Flow에서 추적 활성화
|
||||
|
||||
마찬가지로 CrewAI Flows에 대한 추적을 활성화할 수 있습니다:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ExampleState(BaseModel):
|
||||
counter: int = 0
|
||||
message: str = ""
|
||||
|
||||
class ExampleFlow(Flow[ExampleState]):
|
||||
def __init__(self):
|
||||
super().__init__(tracing=True) # Enable tracing for the flow
|
||||
|
||||
@start()
|
||||
def first_method(self):
|
||||
print("Starting the flow")
|
||||
self.state.counter = 1
|
||||
self.state.message = "Flow started"
|
||||
return "continue"
|
||||
|
||||
@listen("continue")
|
||||
def second_method(self):
|
||||
print("Continuing the flow")
|
||||
self.state.counter += 1
|
||||
self.state.message = "Flow continued"
|
||||
return "finish"
|
||||
|
||||
@listen("finish")
|
||||
def final_method(self):
|
||||
print("Finishing the flow")
|
||||
self.state.counter += 1
|
||||
self.state.message = "Flow completed"
|
||||
|
||||
# Create and run the flow with tracing enabled
|
||||
flow = ExampleFlow(tracing=True)
|
||||
result = flow.kickoff()
|
||||
```
|
||||
|
||||
### 5단계: CrewAI AOP 대시보드에서 추적 보기
|
||||
|
||||
crew 또는 flow를 실행한 후 CrewAI AOP 대시보드에서 CrewAI 애플리케이션이 생성한 추적을 볼 수 있습니다. agent 상호 작용, 도구 사용 및 LLM 호출의 세부 단계를 볼 수 있습니다.
|
||||
아래 링크를 클릭하여 추적을 보거나 대시보드의 추적 탭으로 이동하세요 [여기](https://app.crewai.com/crewai_plus/trace_batches)
|
||||

|
||||
|
||||
|
||||
### 대안: 환경 변수 구성
|
||||
|
||||
환경 변수를 설정하여 전역적으로 추적을 활성화할 수도 있습니다:
|
||||
|
||||
```bash
|
||||
export CREWAI_TRACING_ENABLED=true
|
||||
```
|
||||
|
||||
또는 `.env` 파일에 추가하세요:
|
||||
|
||||
```env
|
||||
CREWAI_TRACING_ENABLED=true
|
||||
```
|
||||
|
||||
이 환경 변수가 설정되면 `tracing=True`를 명시적으로 설정하지 않아도 모든 Crews와 Flows에 자동으로 추적이 활성화됩니다.
|
||||
|
||||
## 추적 보기
|
||||
|
||||
### CrewAI AOP 대시보드 액세스
|
||||
|
||||
1. [app.crewai.com](https://app.crewai.com)을 방문하여 계정에 로그인하세요
|
||||
2. 프로젝트 대시보드로 이동하세요
|
||||
3. **Traces** 탭을 클릭하여 실행 세부 정보를 확인하세요
|
||||
|
||||
### 추적에서 볼 수 있는 내용
|
||||
|
||||
CrewAI 추적은 다음에 대한 포괄적인 가시성을 제공합니다:
|
||||
|
||||
- **Agent 결정**: agent가 작업을 통해 어떻게 추론하고 결정을 내리는지 확인하세요
|
||||
- **작업 실행 타임라인**: 작업 시퀀스 및 종속성의 시각적 표현
|
||||
- **도구 사용**: 어떤 도구가 호출되고 그 결과를 모니터링하세요
|
||||
- **LLM 호출**: 프롬프트 및 응답을 포함한 모든 언어 모델 상호 작용을 추적하세요
|
||||
- **성능 메트릭**: 실행 시간, 토큰 사용량 및 비용
|
||||
- **오류 추적**: 세부 오류 정보 및 스택 추적
|
||||
|
||||
### 추적 기능
|
||||
- **실행 타임라인**: 실행의 다양한 단계를 클릭하여 확인하세요
|
||||
- **세부 로그**: 디버깅을 위한 포괄적인 로그에 액세스하세요
|
||||
- **성능 분석**: 실행 패턴을 분석하고 성능을 최적화하세요
|
||||
- **내보내기 기능**: 추가 분석을 위해 추적을 다운로드하세요
|
||||
|
||||
### 인증 문제
|
||||
|
||||
인증 문제가 발생하는 경우:
|
||||
|
||||
1. 로그인되어 있는지 확인하세요: `crewai login`
|
||||
2. 인터넷 연결을 확인하세요
|
||||
3. [app.crewai.com](https://app.crewai.com)에서 계정을 확인하세요
|
||||
|
||||
### 추적이 나타나지 않음
|
||||
|
||||
대시보드에 추적이 표시되지 않는 경우:
|
||||
|
||||
1. Crew/Flow에서 `tracing=True`가 설정되어 있는지 확인하세요
|
||||
2. 환경 변수를 사용하는 경우 `CREWAI_TRACING_ENABLED=true`인지 확인하세요
|
||||
3. `crewai login`으로 인증되었는지 확인하세요
|
||||
4. crew/flow가 실제로 실행되고 있는지 확인하세요
|
||||
@@ -307,55 +307,6 @@ Os métodos `third_method` e `fourth_method` escutam a saída do `second_method`
|
||||
|
||||
Ao executar esse Flow, a saída será diferente dependendo do valor booleano aleatório gerado pelo `start_method`.
|
||||
|
||||
### Human in the Loop (feedback humano)
|
||||
|
||||
O decorador `@human_feedback` permite fluxos de trabalho human-in-the-loop, pausando a execução do flow para coletar feedback de um humano. Isso é útil para portões de aprovação, revisão de qualidade e pontos de decisão que requerem julgamento humano.
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback, HumanFeedbackResult
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Você aprova este conteúdo?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def generate_content(self):
|
||||
return "Conteúdo para revisão..."
|
||||
|
||||
@listen("approved")
|
||||
def on_approval(self, result: HumanFeedbackResult):
|
||||
print(f"Aprovado! Feedback: {result.feedback}")
|
||||
|
||||
@listen("rejected")
|
||||
def on_rejection(self, result: HumanFeedbackResult):
|
||||
print(f"Rejeitado. Motivo: {result.feedback}")
|
||||
```
|
||||
|
||||
Quando `emit` é especificado, o feedback livre do humano é interpretado por um LLM e mapeado para um dos outcomes especificados, que então dispara o decorador `@listen` correspondente.
|
||||
|
||||
Você também pode usar `@human_feedback` sem roteamento para simplesmente coletar feedback:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(message="Algum comentário sobre esta saída?")
|
||||
def my_method(self):
|
||||
return "Saída para revisão"
|
||||
|
||||
@listen(my_method)
|
||||
def next_step(self, result: HumanFeedbackResult):
|
||||
# Acesse o feedback via result.feedback
|
||||
# Acesse a saída original via result.output
|
||||
pass
|
||||
```
|
||||
|
||||
Acesse todo o feedback coletado durante um flow via `self.last_human_feedback` (mais recente) ou `self.human_feedback_history` (todo o feedback em uma lista).
|
||||
|
||||
Para um guia completo sobre feedback humano em flows, incluindo feedback assíncrono/não-bloqueante com providers customizados (Slack, webhooks, etc.), veja [Feedback Humano em Flows](/pt-BR/learn/human-feedback-in-flows).
|
||||
|
||||
## Adicionando Agentes aos Flows
|
||||
|
||||
Os agentes podem ser integrados facilmente aos seus flows, oferecendo uma alternativa leve às crews completas quando você precisar executar tarefas simples e focadas. Veja um exemplo de como utilizar um agente em um flow para realizar uma pesquisa de mercado:
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
title: Arquitetura de Produção
|
||||
description: Melhores práticas para construir aplicações de IA prontas para produção com CrewAI
|
||||
icon: server
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# A Mentalidade Flow-First
|
||||
|
||||
Ao construir aplicações de IA de produção com CrewAI, **recomendamos começar com um Flow**.
|
||||
|
||||
Embora seja possível executar Crews ou Agentes individuais, envolvê-los em um Flow fornece a estrutura necessária para uma aplicação robusta e escalável.
|
||||
|
||||
## Por que Flows?
|
||||
|
||||
1. **Gerenciamento de Estado**: Flows fornecem uma maneira integrada de gerenciar o estado em diferentes etapas da sua aplicação. Isso é crucial para passar dados entre Crews, manter o contexto e lidar com entradas do usuário.
|
||||
2. **Controle**: Flows permitem definir caminhos de execução precisos, incluindo loops, condicionais e lógica de ramificação. Isso é essencial para lidar com casos extremos e garantir que sua aplicação se comporte de maneira previsível.
|
||||
3. **Observabilidade**: Flows fornecem uma estrutura clara que facilita o rastreamento da execução, a depuração de problemas e o monitoramento do desempenho. Recomendamos o uso do [CrewAI Tracing](/pt-BR/observability/tracing) para insights detalhados. Basta executar `crewai login` para habilitar recursos de observabilidade gratuitos.
|
||||
|
||||
## A Arquitetura
|
||||
|
||||
Uma aplicação CrewAI de produção típica se parece com isso:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Start((Início)) --> Flow[Orquestrador de Flow]
|
||||
Flow --> State{Gerenciamento de Estado}
|
||||
State --> Step1[Etapa 1: Coleta de Dados]
|
||||
Step1 --> Crew1[Crew de Pesquisa]
|
||||
Crew1 --> State
|
||||
State --> Step2{Verificação de Condição}
|
||||
Step2 -- "Válido" --> Step3[Etapa 3: Execução]
|
||||
Step3 --> Crew2[Crew de Ação]
|
||||
Step2 -- "Inválido" --> End((Fim))
|
||||
Crew2 --> End
|
||||
```
|
||||
|
||||
### 1. A Classe Flow
|
||||
Sua classe `Flow` é o ponto de entrada. Ela define o esquema de estado e os métodos que executam sua lógica.
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AppState(BaseModel):
|
||||
user_input: str = ""
|
||||
research_results: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
@start()
|
||||
def gather_input(self):
|
||||
# ... lógica para obter entrada ...
|
||||
pass
|
||||
|
||||
@listen(gather_input)
|
||||
def run_research_crew(self):
|
||||
# ... acionar um Crew ...
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. Gerenciamento de Estado
|
||||
Use modelos Pydantic para definir seu estado. Isso garante a segurança de tipos e deixa claro quais dados estão disponíveis em cada etapa.
|
||||
|
||||
- **Mantenha o mínimo**: Armazene apenas o que você precisa persistir entre as etapas.
|
||||
- **Use dados estruturados**: Evite dicionários não estruturados quando possível.
|
||||
|
||||
### 3. Crews como Unidades de Trabalho
|
||||
Delegue tarefas complexas para Crews. Um Crew deve ser focado em um objetivo específico (por exemplo, "Pesquisar um tópico", "Escrever uma postagem no blog").
|
||||
|
||||
- **Não superengendre Crews**: Mantenha-os focados.
|
||||
- **Passe o estado explicitamente**: Passe os dados necessários do estado do Flow para as entradas do Crew.
|
||||
|
||||
```python
|
||||
@listen(gather_input)
|
||||
def run_research_crew(self):
|
||||
crew = ResearchCrew()
|
||||
result = crew.kickoff(inputs={"topic": self.state.user_input})
|
||||
self.state.research_results = result.raw
|
||||
```
|
||||
|
||||
## Primitivas de Controle
|
||||
|
||||
Aproveite as primitivas de controle do CrewAI para adicionar robustez e controle aos seus Crews.
|
||||
|
||||
### 1. Task Guardrails
|
||||
Use [Task Guardrails](/pt-BR/concepts/tasks#task-guardrails) para validar as saídas das tarefas antes que sejam aceitas. Isso garante que seus agentes produzam resultados de alta qualidade.
|
||||
|
||||
```python
|
||||
def validate_content(result: TaskOutput) -> Tuple[bool, Any]:
|
||||
if len(result.raw) < 100:
|
||||
return (False, "Content is too short. Please expand.")
|
||||
return (True, result.raw)
|
||||
|
||||
task = Task(
|
||||
...,
|
||||
guardrail=validate_content
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Saídas Estruturadas
|
||||
Sempre use saídas estruturadas (`output_pydantic` ou `output_json`) ao passar dados entre tarefas ou para sua aplicação. Isso evita erros de análise e garante a segurança de tipos.
|
||||
|
||||
```python
|
||||
class ResearchResult(BaseModel):
|
||||
summary: str
|
||||
sources: List[str]
|
||||
|
||||
task = Task(
|
||||
...,
|
||||
output_pydantic=ResearchResult
|
||||
)
|
||||
```
|
||||
|
||||
### 3. LLM Hooks
|
||||
Use [LLM Hooks](/pt-BR/learn/llm-hooks) para inspecionar ou modificar mensagens antes que elas sejam enviadas para o LLM, ou para higienizar respostas.
|
||||
|
||||
```python
|
||||
@before_llm_call
|
||||
def log_request(context):
|
||||
print(f"Agent {context.agent.role} is calling the LLM...")
|
||||
```
|
||||
|
||||
## Padrões de Implantação
|
||||
|
||||
Ao implantar seu Flow, considere o seguinte:
|
||||
|
||||
### CrewAI Enterprise
|
||||
A maneira mais fácil de implantar seu Flow é usando o CrewAI Enterprise. Ele lida com a infraestrutura, autenticação e monitoramento para você.
|
||||
|
||||
Confira o [Guia de Implantação](/pt-BR/enterprise/guides/deploy-crew) para começar.
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
### Execução Assíncrona
|
||||
Para tarefas de longa duração, use `kickoff_async` para evitar bloquear sua API.
|
||||
|
||||
### Persistência
|
||||
Use o decorador `@persist` para salvar o estado do seu Flow em um banco de dados. Isso permite retomar a execução se o processo falhar ou se você precisar esperar pela entrada humana.
|
||||
|
||||
```python
|
||||
@persist
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
## Resumo
|
||||
|
||||
- **Comece com um Flow.**
|
||||
- **Defina um Estado claro.**
|
||||
- **Use Crews para tarefas complexas.**
|
||||
- **Implante com uma API e persistência.**
|
||||
@@ -62,13 +62,13 @@ Teste sua integração de trigger do Gmail localmente usando a CLI da CrewAI:
|
||||
crewai triggers list
|
||||
|
||||
# Simule um trigger do Gmail com payload realista
|
||||
crewai triggers run gmail/new_email_received
|
||||
crewai triggers run gmail/new_email
|
||||
```
|
||||
|
||||
O comando `crewai triggers run` executará sua crew com um payload completo do Gmail, permitindo que você teste sua lógica de parsing antes do deployment.
|
||||
|
||||
<Warning>
|
||||
Use `crewai triggers run gmail/new_email_received` (não `crewai run`) para simular execução de trigger durante o desenvolvimento. Após o deployment, sua crew receberá automaticamente o payload do trigger.
|
||||
Use `crewai triggers run gmail/new_email` (não `crewai run`) para simular execução de trigger durante o desenvolvimento. Após o deployment, sua crew receberá automaticamente o payload do trigger.
|
||||
</Warning>
|
||||
|
||||
## Monitoring Executions
|
||||
@@ -83,6 +83,6 @@ Track history and performance of triggered runs:
|
||||
|
||||
- Ensure Gmail is connected in Tools & Integrations
|
||||
- Verify the Gmail Trigger is enabled on the Triggers tab
|
||||
- Teste localmente com `crewai triggers run gmail/new_email_received` para ver a estrutura exata do payload
|
||||
- Teste localmente com `crewai triggers run gmail/new_email` para ver a estrutura exata do payload
|
||||
- Check the execution logs and confirm the payload is passed as `crewai_trigger_payload`
|
||||
- Lembre-se: use `crewai triggers run` (não `crewai run`) para simular execução de trigger
|
||||
|
||||
@@ -7,89 +7,110 @@ mode: "wide"
|
||||
|
||||
# O que é CrewAI?
|
||||
|
||||
**CrewAI é o principal framework open-source para orquestrar agentes de IA autônomos e construir fluxos de trabalho complexos.**
|
||||
**CrewAI é um framework Python enxuto e ultrarrápido, construído totalmente do zero—completamente independente do LangChain ou de outros frameworks de agentes.**
|
||||
|
||||
Ele capacita desenvolvedores a construir sistemas multi-agente prontos para produção, combinando a inteligência colaborativa dos **Crews** com o controle preciso dos **Flows**.
|
||||
O CrewAI capacita desenvolvedores tanto com simplicidade de alto nível quanto com controle detalhado de baixo nível, ideal para criar agentes de IA autônomos sob medida para qualquer cenário:
|
||||
|
||||
- **[Flows do CrewAI](/pt-BR/guides/flows/first-flow)**: A espinha dorsal da sua aplicação de IA. Flows permitem criar fluxos de trabalho estruturados e orientados a eventos que gerenciam estado e controlam a execução. Eles fornecem a estrutura para seus agentes de IA trabalharem.
|
||||
- **[Crews do CrewAI](/pt-BR/guides/crews/first-crew)**: As unidades de trabalho dentro do seu Flow. Crews são equipes de agentes autônomos que colaboram para resolver tarefas específicas delegadas a eles pelo Flow.
|
||||
- **[Crews do CrewAI](/pt-BR/guides/crews/first-crew)**: Otimizados para autonomia e inteligência colaborativa, permitindo criar equipes de IA onde cada agente possui funções, ferramentas e objetivos específicos.
|
||||
- **[Flows do CrewAI](/pt-BR/guides/flows/first-flow)**: Proporcionam controle granular, orientado por eventos, com chamadas LLM individuais para uma orquestração precisa das tarefas, além de suportar Crews nativamente.
|
||||
|
||||
Com mais de 100.000 desenvolvedores certificados em nossos cursos comunitários, o CrewAI é o padrão para automação de IA pronta para empresas.
|
||||
Com mais de 100.000 desenvolvedores certificados em nossos cursos comunitários, o CrewAI está se tornando rapidamente o padrão para automação de IA pronta para empresas.
|
||||
|
||||
## A Arquitetura do CrewAI
|
||||
|
||||
A arquitetura do CrewAI foi projetada para equilibrar autonomia com controle.
|
||||
|
||||
### 1. Flows: A Espinha Dorsal
|
||||
## Como funcionam os Crews
|
||||
|
||||
<Note>
|
||||
Pense em um Flow como o "gerente" ou a "definição do processo" da sua aplicação. Ele define as etapas, a lógica e como os dados se movem através do seu sistema.
|
||||
</Note>
|
||||
|
||||
<Frame caption="Visão Geral do Framework CrewAI">
|
||||
<img src="/images/flows.png" alt="Visão Geral do Framework CrewAI" />
|
||||
</Frame>
|
||||
|
||||
Flows fornecem:
|
||||
- **Gerenciamento de Estado**: Persistem dados através de etapas e execuções.
|
||||
- **Execução Orientada a Eventos**: Acionam ações com base em eventos ou entradas externas.
|
||||
- **Controle de Fluxo**: Usam lógica condicional, loops e ramificações.
|
||||
|
||||
### 2. Crews: A Inteligência
|
||||
|
||||
<Note>
|
||||
Crews são as "equipes" que fazem o trabalho pesado. Dentro de um Flow, você pode acionar um Crew para lidar com um problema complexo que requer criatividade e colaboração.
|
||||
Assim como uma empresa possui departamentos (Vendas, Engenharia, Marketing) trabalhando juntos sob uma liderança para atingir objetivos de negócio, o CrewAI ajuda você a criar uma “organização” de agentes de IA com funções especializadas colaborando para realizar tarefas complexas.
|
||||
</Note>
|
||||
|
||||
<Frame caption="Visão Geral do Framework CrewAI">
|
||||
<img src="/images/crews.png" alt="Visão Geral do Framework CrewAI" />
|
||||
</Frame>
|
||||
|
||||
Crews fornecem:
|
||||
- **Agentes com Funções**: Agentes especializados com objetivos e ferramentas específicas.
|
||||
- **Colaboração Autônoma**: Agentes trabalham juntos para resolver tarefas.
|
||||
- **Delegação de Tarefas**: Tarefas são atribuídas e executadas com base nas capacidades dos agentes.
|
||||
| Componente | Descrição | Principais Funcionalidades |
|
||||
|:-----------|:-----------:|:-------------------------|
|
||||
| **Crew** | Organização de mais alto nível | • Gerencia equipes de agentes de IA<br/>• Supervisiona fluxos de trabalho<br/>• Garante colaboração<br/>• Entrega resultados |
|
||||
| **Agentes de IA** | Membros especializados da equipe | • Possuem funções específicas (pesquisador, escritor)<br/>• Utilizam ferramentas designadas<br/>• Podem delegar tarefas<br/>• Tomam decisões autônomas |
|
||||
| **Process** | Sistema de gestão do fluxo de trabalho | • Define padrões de colaboração<br/>• Controla designação de tarefas<br/>• Gerencia interações<br/>• Garante execução eficiente |
|
||||
| **Tasks** | Atribuições individuais | • Objetivos claros<br/>• Utilizam ferramentas específicas<br/>• Alimentam processos maiores<br/>• Geram resultados acionáveis |
|
||||
|
||||
## Como Tudo Funciona Junto
|
||||
### Como tudo trabalha junto
|
||||
|
||||
1. **O Flow** aciona um evento ou inicia um processo.
|
||||
2. **O Flow** gerencia o estado e decide o que fazer a seguir.
|
||||
3. **O Flow** delega uma tarefa complexa para um **Crew**.
|
||||
4. Os agentes do **Crew** colaboram para completar a tarefa.
|
||||
5. **O Crew** retorna o resultado para o **Flow**.
|
||||
6. **O Flow** continua a execução com base no resultado.
|
||||
1. O **Crew** organiza toda a operação
|
||||
2. **Agentes de IA** realizam tarefas especializadas
|
||||
3. O **Process** garante colaboração fluida
|
||||
4. **Tasks** são concluídas para alcançar o objetivo
|
||||
|
||||
## Principais Funcionalidades
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Flows de Nível de Produção" icon="arrow-progress">
|
||||
Construa fluxos de trabalho confiáveis e com estado que podem lidar com processos de longa duração e lógica complexa.
|
||||
</Card>
|
||||
<Card title="Crews Autônomos" icon="users">
|
||||
Implante equipes de agentes que podem planejar, executar e colaborar para alcançar objetivos de alto nível.
|
||||
<Card title="Agentes Baseados em Funções" icon="users">
|
||||
Crie agentes especializados com funções, conhecimentos e objetivos definidos – de pesquisadores e analistas a escritores
|
||||
</Card>
|
||||
<Card title="Ferramentas Flexíveis" icon="screwdriver-wrench">
|
||||
Conecte seus agentes a qualquer API, banco de dados ou ferramenta local.
|
||||
Equipe os agentes com ferramentas e APIs personalizadas para interagir com serviços e fontes de dados externas
|
||||
</Card>
|
||||
<Card title="Segurança Empresarial" icon="lock">
|
||||
Projetado com segurança e conformidade em mente para implantações empresariais.
|
||||
<Card title="Colaboração Inteligente" icon="people-arrows">
|
||||
Agentes trabalham juntos, compartilhando insights e coordenando tarefas para conquistar objetivos complexos
|
||||
</Card>
|
||||
<Card title="Gerenciamento de Tarefas" icon="list-check">
|
||||
Defina fluxos de trabalho sequenciais ou paralelos, com agentes lidando automaticamente com dependências entre tarefas
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Quando usar Crews vs. Flows
|
||||
## Como funcionam os Flows
|
||||
|
||||
**A resposta curta: Use ambos.**
|
||||
<Note>
|
||||
Enquanto Crews se destacam na colaboração autônoma, Flows proporcionam automações estruturadas, oferecendo controle granular sobre a execução dos fluxos de trabalho. Flows garantem execução confiável, segura e eficiente, lidando com lógica condicional, loops e gerenciamento dinâmico de estados com precisão. Flows se integram perfeitamente com Crews, permitindo equilibrar alta autonomia com controle rigoroso.
|
||||
</Note>
|
||||
|
||||
Para qualquer aplicação pronta para produção, **comece com um Flow**.
|
||||
<Frame caption="Visão Geral do Framework CrewAI">
|
||||
<img src="/images/flows.png" alt="Visão Geral do Framework CrewAI" />
|
||||
</Frame>
|
||||
|
||||
- **Use um Flow** para definir a estrutura geral, estado e lógica da sua aplicação.
|
||||
- **Use um Crew** dentro de uma etapa do Flow quando precisar de uma equipe de agentes para realizar uma tarefa específica e complexa que requer autonomia.
|
||||
| Componente | Descrição | Principais Funcionalidades |
|
||||
|:-----------|:-----------:|:-------------------------|
|
||||
| **Flow** | Orquestração de fluxo de trabalho estruturada | • Gerencia caminhos de execução<br/>• Lida com transições de estado<br/>• Controla a sequência de tarefas<br/>• Garante execução confiável |
|
||||
| **Events** | Gatilhos para ações nos fluxos | • Iniciam processos específicos<br/>• Permitem respostas dinâmicas<br/>• Suportam ramificações condicionais<br/>• Adaptam-se em tempo real |
|
||||
| **States** | Contextos de execução dos fluxos | • Mantêm dados de execução<br/>• Permitem persistência<br/>• Suportam retomada<br/>• Garantem integridade na execução |
|
||||
| **Crew Support** | Aprimora automação de fluxos | • Injeta autonomia quando necessário<br/>• Complementa fluxos estruturados<br/>• Equilibra automação e inteligência<br/>• Permite tomada de decisão adaptativa |
|
||||
|
||||
| Caso de Uso | Arquitetura |
|
||||
| :--- | :--- |
|
||||
| **Automação Simples** | Flow único com tarefas Python |
|
||||
| **Pesquisa Complexa** | Flow gerenciando estado -> Crew realizando pesquisa |
|
||||
| **Backend de Aplicação** | Flow lidando com requisições API -> Crew gerando conteúdo -> Flow salvando no BD |
|
||||
### Capacidades-Chave
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Orquestração Orientada por Eventos" icon="bolt">
|
||||
Defina caminhos de execução precisos respondendo dinamicamente a eventos
|
||||
</Card>
|
||||
<Card title="Controle Detalhado" icon="sliders">
|
||||
Gerencie estados de fluxo de trabalho e execução condicional de forma segura e eficiente
|
||||
</Card>
|
||||
<Card title="Integração Nativa com Crew" icon="puzzle-piece">
|
||||
Combine de forma simples com Crews para maior autonomia e inteligência
|
||||
</Card>
|
||||
<Card title="Execução Determinística" icon="route">
|
||||
Garanta resultados previsíveis com controle explícito de fluxo e tratamento de erros
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Quando usar Crews versus Flows
|
||||
|
||||
<Note>
|
||||
Entender quando utilizar [Crews](/pt-BR/guides/crews/first-crew) ou [Flows](/pt-BR/guides/flows/first-flow) é fundamental para maximizar o potencial do CrewAI em suas aplicações.
|
||||
</Note>
|
||||
|
||||
| Caso de uso | Abordagem recomendada | Por quê? |
|
||||
|:------------|:---------------------|:---------|
|
||||
| **Pesquisa aberta** | [Crews](/pt-BR/guides/crews/first-crew) | Quando as tarefas exigem criatividade, exploração e adaptação |
|
||||
| **Geração de conteúdo** | [Crews](/pt-BR/guides/crews/first-crew) | Para criação colaborativa de artigos, relatórios ou materiais de marketing |
|
||||
| **Fluxos de decisão** | [Flows](/pt-BR/guides/flows/first-flow) | Quando é necessário caminhos de decisão previsíveis, auditáveis e com controle preciso |
|
||||
| **Orquestração de APIs** | [Flows](/pt-BR/guides/flows/first-flow) | Para integração confiável com múltiplos serviços externos em sequência específica |
|
||||
| **Aplicações híbridas** | Abordagem combinada | Use [Flows](/pt-BR/guides/flows/first-flow) para orquestrar o processo geral com [Crews](/pt-BR/guides/crews/first-crew) lidando com subtarefas complexas |
|
||||
|
||||
### Framework de Decisão
|
||||
|
||||
- **Escolha [Crews](/pt-BR/guides/crews/first-crew) quando:** Precisa de resolução autônoma de problemas, colaboração criativa ou tarefas exploratórias
|
||||
- **Escolha [Flows](/pt-BR/guides/flows/first-flow) quando:** Requer resultados determinísticos, auditabilidade ou controle preciso sobre a execução
|
||||
- **Combine ambos quando:** Sua aplicação precisa de processos estruturados e também de bolsões de inteligência autônoma
|
||||
|
||||
## Por que escolher o CrewAI?
|
||||
|
||||
@@ -103,13 +124,6 @@ Para qualquer aplicação pronta para produção, **comece com um Flow**.
|
||||
## Pronto para começar a construir?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Crie Seu Primeiro Flow"
|
||||
icon="diagram-project"
|
||||
href="/pt-BR/guides/flows/first-flow"
|
||||
>
|
||||
Aprenda a criar fluxos de trabalho estruturados e orientados por eventos com controle preciso de execução.
|
||||
</Card>
|
||||
<Card
|
||||
title="Crie Seu Primeiro Crew"
|
||||
icon="users-gear"
|
||||
@@ -117,6 +131,13 @@ Para qualquer aplicação pronta para produção, **comece com um Flow**.
|
||||
>
|
||||
Tutorial passo a passo para criar uma equipe de IA colaborativa que trabalha junto para resolver problemas complexos.
|
||||
</Card>
|
||||
<Card
|
||||
title="Crie Seu Primeiro Flow"
|
||||
icon="diagram-project"
|
||||
href="/pt-BR/guides/flows/first-flow"
|
||||
>
|
||||
Aprenda a criar fluxos de trabalho estruturados e orientados por eventos com controle preciso de execução.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
---
|
||||
title: Feedback Humano em Flows
|
||||
description: Aprenda como integrar feedback humano diretamente nos seus CrewAI Flows usando o decorador @human_feedback
|
||||
icon: user-check
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O decorador `@human_feedback` permite fluxos de trabalho human-in-the-loop (HITL) diretamente nos CrewAI Flows. Ele permite pausar a execução do flow, apresentar a saída para um humano revisar, coletar seu feedback e, opcionalmente, rotear para diferentes listeners com base no resultado do feedback.
|
||||
|
||||
Isso é particularmente valioso para:
|
||||
|
||||
- **Garantia de qualidade**: Revisar conteúdo gerado por IA antes de ser usado downstream
|
||||
- **Portões de decisão**: Deixar humanos tomarem decisões críticas em fluxos automatizados
|
||||
- **Fluxos de aprovação**: Implementar padrões de aprovar/rejeitar/revisar
|
||||
- **Refinamento interativo**: Coletar feedback para melhorar saídas iterativamente
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Método do Flow] --> B[Saída Gerada]
|
||||
B --> C[Humano Revisa]
|
||||
C --> D{Feedback}
|
||||
D -->|emit especificado| E[LLM Mapeia para Outcome]
|
||||
D -->|sem emit| F[HumanFeedbackResult]
|
||||
E --> G["@listen('approved')"]
|
||||
E --> H["@listen('rejected')"]
|
||||
F --> I[Próximo Listener]
|
||||
```
|
||||
|
||||
## Início Rápido
|
||||
|
||||
Aqui está a maneira mais simples de adicionar feedback humano a um flow:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback
|
||||
|
||||
class SimpleReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Por favor, revise este conteúdo:")
|
||||
def generate_content(self):
|
||||
return "Este é um conteúdo gerado por IA que precisa de revisão."
|
||||
|
||||
@listen(generate_content)
|
||||
def process_feedback(self, result):
|
||||
print(f"Conteúdo: {result.output}")
|
||||
print(f"Humano disse: {result.feedback}")
|
||||
|
||||
flow = SimpleReviewFlow()
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
Quando este flow é executado, ele irá:
|
||||
1. Executar `generate_content` e retornar a string
|
||||
2. Exibir a saída para o usuário com a mensagem de solicitação
|
||||
3. Aguardar o usuário digitar o feedback (ou pressionar Enter para pular)
|
||||
4. Passar um objeto `HumanFeedbackResult` para `process_feedback`
|
||||
|
||||
## O Decorador @human_feedback
|
||||
|
||||
### Parâmetros
|
||||
|
||||
| Parâmetro | Tipo | Obrigatório | Descrição |
|
||||
|-----------|------|-------------|-----------|
|
||||
| `message` | `str` | Sim | A mensagem mostrada ao humano junto com a saída do método |
|
||||
| `emit` | `Sequence[str]` | Não | Lista de possíveis outcomes. O feedback é mapeado para um destes, que dispara decoradores `@listen` |
|
||||
| `llm` | `str \| BaseLLM` | Quando `emit` especificado | LLM usado para interpretar o feedback e mapear para um outcome |
|
||||
| `default_outcome` | `str` | Não | Outcome a usar se nenhum feedback for fornecido. Deve estar em `emit` |
|
||||
| `metadata` | `dict` | Não | Dados adicionais para integrações enterprise |
|
||||
| `provider` | `HumanFeedbackProvider` | Não | Provider customizado para feedback assíncrono/não-bloqueante. Veja [Feedback Humano Assíncrono](#feedback-humano-assíncrono-não-bloqueante) |
|
||||
|
||||
### Uso Básico (Sem Roteamento)
|
||||
|
||||
Quando você não especifica `emit`, o decorador simplesmente coleta o feedback e passa um `HumanFeedbackResult` para o próximo listener:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(message="O que você acha desta análise?")
|
||||
def analyze_data(self):
|
||||
return "Resultados da análise: Receita aumentou 15%, custos diminuíram 8%"
|
||||
|
||||
@listen(analyze_data)
|
||||
def handle_feedback(self, result):
|
||||
# result é um HumanFeedbackResult
|
||||
print(f"Análise: {result.output}")
|
||||
print(f"Feedback: {result.feedback}")
|
||||
```
|
||||
|
||||
### Roteamento com emit
|
||||
|
||||
Quando você especifica `emit`, o decorador se torna um roteador. O feedback livre do humano é interpretado por um LLM e mapeado para um dos outcomes especificados:
|
||||
|
||||
```python Code
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Você aprova este conteúdo para publicação?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_content(self):
|
||||
return "Rascunho do post do blog aqui..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
print(f"Publicando! Usuário disse: {result.feedback}")
|
||||
|
||||
@listen("rejected")
|
||||
def discard(self, result):
|
||||
print(f"Descartando. Motivo: {result.feedback}")
|
||||
|
||||
@listen("needs_revision")
|
||||
def revise(self, result):
|
||||
print(f"Revisando baseado em: {result.feedback}")
|
||||
```
|
||||
|
||||
<Tip>
|
||||
O LLM usa saídas estruturadas (function calling) quando disponível para garantir que a resposta seja um dos seus outcomes especificados. Isso torna o roteamento confiável e previsível.
|
||||
</Tip>
|
||||
|
||||
## HumanFeedbackResult
|
||||
|
||||
O dataclass `HumanFeedbackResult` contém todas as informações sobre uma interação de feedback humano:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult
|
||||
|
||||
@dataclass
|
||||
class HumanFeedbackResult:
|
||||
output: Any # A saída original do método mostrada ao humano
|
||||
feedback: str # O texto bruto do feedback do humano
|
||||
outcome: str | None # O outcome mapeado (se emit foi especificado)
|
||||
timestamp: datetime # Quando o feedback foi recebido
|
||||
method_name: str # Nome do método decorado
|
||||
metadata: dict # Qualquer metadata passado ao decorador
|
||||
```
|
||||
|
||||
### Acessando em Listeners
|
||||
|
||||
Quando um listener é disparado por um método `@human_feedback` com `emit`, ele recebe o `HumanFeedbackResult`:
|
||||
|
||||
```python Code
|
||||
@listen("approved")
|
||||
def on_approval(self, result: HumanFeedbackResult):
|
||||
print(f"Saída original: {result.output}")
|
||||
print(f"Feedback do usuário: {result.feedback}")
|
||||
print(f"Outcome: {result.outcome}") # "approved"
|
||||
print(f"Recebido em: {result.timestamp}")
|
||||
```
|
||||
|
||||
## Acessando o Histórico de Feedback
|
||||
|
||||
A classe `Flow` fornece dois atributos para acessar o feedback humano:
|
||||
|
||||
### last_human_feedback
|
||||
|
||||
Retorna o `HumanFeedbackResult` mais recente:
|
||||
|
||||
```python Code
|
||||
@listen(some_method)
|
||||
def check_feedback(self):
|
||||
if self.last_human_feedback:
|
||||
print(f"Último feedback: {self.last_human_feedback.feedback}")
|
||||
```
|
||||
|
||||
### human_feedback_history
|
||||
|
||||
Uma lista de todos os objetos `HumanFeedbackResult` coletados durante o flow:
|
||||
|
||||
```python Code
|
||||
@listen(final_step)
|
||||
def summarize(self):
|
||||
print(f"Total de feedbacks coletados: {len(self.human_feedback_history)}")
|
||||
for i, fb in enumerate(self.human_feedback_history):
|
||||
print(f"{i+1}. {fb.method_name}: {fb.outcome or 'sem roteamento'}")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Cada `HumanFeedbackResult` é adicionado a `human_feedback_history`, então múltiplos passos de feedback não sobrescrevem uns aos outros. Use esta lista para acessar todo o feedback coletado durante o flow.
|
||||
</Warning>
|
||||
|
||||
## Exemplo Completo: Fluxo de Aprovação de Conteúdo
|
||||
|
||||
Aqui está um exemplo completo implementando um fluxo de revisão e aprovação de conteúdo:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.human_feedback import human_feedback, HumanFeedbackResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ContentState(BaseModel):
|
||||
topic: str = ""
|
||||
draft: str = ""
|
||||
final_content: str = ""
|
||||
revision_count: int = 0
|
||||
|
||||
|
||||
class ContentApprovalFlow(Flow[ContentState]):
|
||||
"""Um flow que gera conteúdo e obtém aprovação humana."""
|
||||
|
||||
@start()
|
||||
def get_topic(self):
|
||||
self.state.topic = input("Sobre qual tópico devo escrever? ")
|
||||
return self.state.topic
|
||||
|
||||
@listen(get_topic)
|
||||
def generate_draft(self, topic):
|
||||
# Em uso real, isso chamaria um LLM
|
||||
self.state.draft = f"# {topic}\n\nEste é um rascunho sobre {topic}..."
|
||||
return self.state.draft
|
||||
|
||||
@listen(generate_draft)
|
||||
@human_feedback(
|
||||
message="Por favor, revise este rascunho. Responda 'approved', 'rejected', ou forneça feedback de revisão:",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_draft(self, draft):
|
||||
return draft
|
||||
|
||||
@listen("approved")
|
||||
def publish_content(self, result: HumanFeedbackResult):
|
||||
self.state.final_content = result.output
|
||||
print("\n✅ Conteúdo aprovado e publicado!")
|
||||
print(f"Comentário do revisor: {result.feedback}")
|
||||
return "published"
|
||||
|
||||
@listen("rejected")
|
||||
def handle_rejection(self, result: HumanFeedbackResult):
|
||||
print("\n❌ Conteúdo rejeitado")
|
||||
print(f"Motivo: {result.feedback}")
|
||||
return "rejected"
|
||||
|
||||
@listen("needs_revision")
|
||||
def revise_content(self, result: HumanFeedbackResult):
|
||||
self.state.revision_count += 1
|
||||
print(f"\n📝 Revisão #{self.state.revision_count} solicitada")
|
||||
print(f"Feedback: {result.feedback}")
|
||||
|
||||
# Em um flow real, você pode voltar para generate_draft
|
||||
# Para este exemplo, apenas reconhecemos
|
||||
return "revision_requested"
|
||||
|
||||
|
||||
# Executar o flow
|
||||
flow = ContentApprovalFlow()
|
||||
result = flow.kickoff()
|
||||
print(f"\nFlow concluído. Revisões solicitadas: {flow.state.revision_count}")
|
||||
```
|
||||
|
||||
```text Output
|
||||
Sobre qual tópico devo escrever? Segurança em IA
|
||||
|
||||
==================================================
|
||||
OUTPUT FOR REVIEW:
|
||||
==================================================
|
||||
# Segurança em IA
|
||||
|
||||
Este é um rascunho sobre Segurança em IA...
|
||||
==================================================
|
||||
|
||||
Por favor, revise este rascunho. Responda 'approved', 'rejected', ou forneça feedback de revisão:
|
||||
(Press Enter to skip, or type your feedback)
|
||||
|
||||
Your feedback: Parece bom, aprovado!
|
||||
|
||||
✅ Conteúdo aprovado e publicado!
|
||||
Comentário do revisor: Parece bom, aprovado!
|
||||
|
||||
Flow concluído. Revisões solicitadas: 0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Combinando com Outros Decoradores
|
||||
|
||||
O decorador `@human_feedback` funciona com outros decoradores de flow. Coloque-o como o decorador mais interno (mais próximo da função):
|
||||
|
||||
```python Code
|
||||
# Correto: @human_feedback é o mais interno (mais próximo da função)
|
||||
@start()
|
||||
@human_feedback(message="Revise isto:")
|
||||
def my_start_method(self):
|
||||
return "content"
|
||||
|
||||
@listen(other_method)
|
||||
@human_feedback(message="Revise isto também:")
|
||||
def my_listener(self, data):
|
||||
return f"processed: {data}"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Coloque `@human_feedback` como o decorador mais interno (último/mais próximo da função) para que ele envolva o método diretamente e possa capturar o valor de retorno antes de passar para o sistema de flow.
|
||||
</Tip>
|
||||
|
||||
## Melhores Práticas
|
||||
|
||||
### 1. Escreva Mensagens de Solicitação Claras
|
||||
|
||||
O parâmetro `message` é o que o humano vê. Torne-o acionável:
|
||||
|
||||
```python Code
|
||||
# ✅ Bom - claro e acionável
|
||||
@human_feedback(message="Este resumo captura com precisão os pontos-chave? Responda 'sim' ou explique o que está faltando:")
|
||||
|
||||
# ❌ Ruim - vago
|
||||
@human_feedback(message="Revise isto:")
|
||||
```
|
||||
|
||||
### 2. Escolha Outcomes Significativos
|
||||
|
||||
Ao usar `emit`, escolha outcomes que mapeiem naturalmente para respostas humanas:
|
||||
|
||||
```python Code
|
||||
# ✅ Bom - outcomes em linguagem natural
|
||||
emit=["approved", "rejected", "needs_more_detail"]
|
||||
|
||||
# ❌ Ruim - técnico ou pouco claro
|
||||
emit=["state_1", "state_2", "state_3"]
|
||||
```
|
||||
|
||||
### 3. Sempre Forneça um Outcome Padrão
|
||||
|
||||
Use `default_outcome` para lidar com casos onde usuários pressionam Enter sem digitar:
|
||||
|
||||
```python Code
|
||||
@human_feedback(
|
||||
message="Aprovar? (pressione Enter para solicitar revisão)",
|
||||
emit=["approved", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision", # Padrão seguro
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Use o Histórico de Feedback para Trilhas de Auditoria
|
||||
|
||||
Acesse `human_feedback_history` para criar logs de auditoria:
|
||||
|
||||
```python Code
|
||||
@listen(final_step)
|
||||
def create_audit_log(self):
|
||||
log = []
|
||||
for fb in self.human_feedback_history:
|
||||
log.append({
|
||||
"step": fb.method_name,
|
||||
"outcome": fb.outcome,
|
||||
"feedback": fb.feedback,
|
||||
"timestamp": fb.timestamp.isoformat(),
|
||||
})
|
||||
return log
|
||||
```
|
||||
|
||||
### 5. Trate Feedback Roteado e Não Roteado
|
||||
|
||||
Ao projetar flows, considere se você precisa de roteamento:
|
||||
|
||||
| Cenário | Use |
|
||||
|---------|-----|
|
||||
| Revisão simples, só precisa do texto do feedback | Sem `emit` |
|
||||
| Precisa ramificar para caminhos diferentes baseado na resposta | Use `emit` |
|
||||
| Portões de aprovação com aprovar/rejeitar/revisar | Use `emit` |
|
||||
| Coletando comentários apenas para logging | Sem `emit` |
|
||||
|
||||
## Feedback Humano Assíncrono (Não-Bloqueante - Human in the loop)
|
||||
|
||||
Por padrão, `@human_feedback` bloqueia a execução aguardando entrada no console. Para aplicações de produção, você pode precisar de feedback **assíncrono/não-bloqueante** que se integre com sistemas externos como Slack, email, webhooks ou APIs.
|
||||
|
||||
### A Abstração de Provider
|
||||
|
||||
Use o parâmetro `provider` para especificar uma estratégia customizada de coleta de feedback:
|
||||
|
||||
```python Code
|
||||
from crewai.flow import Flow, start, human_feedback, HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext
|
||||
|
||||
class WebhookProvider(HumanFeedbackProvider):
|
||||
"""Provider que pausa o flow e aguarda callback de webhook."""
|
||||
|
||||
def __init__(self, webhook_url: str):
|
||||
self.webhook_url = webhook_url
|
||||
|
||||
def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
|
||||
# Notifica sistema externo (ex: envia mensagem Slack, cria ticket)
|
||||
self.send_notification(context)
|
||||
|
||||
# Pausa execução - framework cuida da persistência automaticamente
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={"webhook_url": f"{self.webhook_url}/{context.flow_id}"}
|
||||
)
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Revise este conteúdo:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
provider=WebhookProvider("https://myapp.com/api"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "Conteúdo gerado por IA..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
return "Publicado!"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
O framework de flow **persiste automaticamente o estado** quando `HumanFeedbackPending` é lançado. Seu provider só precisa notificar o sistema externo e lançar a exceção—não são necessárias chamadas manuais de persistência.
|
||||
</Tip>
|
||||
|
||||
### Tratando Flows Pausados
|
||||
|
||||
Ao usar um provider assíncrono, `kickoff()` retorna um objeto `HumanFeedbackPending` em vez de lançar uma exceção:
|
||||
|
||||
```python Code
|
||||
flow = ReviewFlow()
|
||||
result = flow.kickoff()
|
||||
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
# Flow está pausado, estado é automaticamente persistido
|
||||
print(f"Aguardando feedback em: {result.callback_info['webhook_url']}")
|
||||
print(f"Flow ID: {result.context.flow_id}")
|
||||
else:
|
||||
# Conclusão normal
|
||||
print(f"Flow concluído: {result}")
|
||||
```
|
||||
|
||||
### Retomando um Flow Pausado
|
||||
|
||||
Quando o feedback chega (ex: via webhook), retome o flow:
|
||||
|
||||
```python Code
|
||||
# Handler síncrono:
|
||||
def handle_feedback_webhook(flow_id: str, feedback: str):
|
||||
flow = ReviewFlow.from_pending(flow_id)
|
||||
result = flow.resume(feedback)
|
||||
return result
|
||||
|
||||
# Handler assíncrono (FastAPI, aiohttp, etc.):
|
||||
async def handle_feedback_webhook(flow_id: str, feedback: str):
|
||||
flow = ReviewFlow.from_pending(flow_id)
|
||||
result = await flow.resume_async(feedback)
|
||||
return result
|
||||
```
|
||||
|
||||
### Tipos Principais
|
||||
|
||||
| Tipo | Descrição |
|
||||
|------|-----------|
|
||||
| `HumanFeedbackProvider` | Protocolo para providers de feedback customizados |
|
||||
| `PendingFeedbackContext` | Contém todas as informações necessárias para retomar um flow pausado |
|
||||
| `HumanFeedbackPending` | Retornado por `kickoff()` quando o flow está pausado para feedback |
|
||||
| `ConsoleProvider` | Provider padrão de entrada bloqueante no console |
|
||||
|
||||
### PendingFeedbackContext
|
||||
|
||||
O contexto contém tudo necessário para retomar:
|
||||
|
||||
```python Code
|
||||
@dataclass
|
||||
class PendingFeedbackContext:
|
||||
flow_id: str # Identificador único desta execução de flow
|
||||
flow_class: str # Nome qualificado completo da classe
|
||||
method_name: str # Método que disparou o feedback
|
||||
method_output: Any # Saída mostrada ao humano
|
||||
message: str # A mensagem de solicitação
|
||||
emit: list[str] | None # Outcomes possíveis para roteamento
|
||||
default_outcome: str | None
|
||||
metadata: dict # Metadata customizado
|
||||
llm: str | None # LLM para mapeamento de outcome
|
||||
requested_at: datetime
|
||||
```
|
||||
|
||||
### Exemplo Completo de Flow Assíncrono
|
||||
|
||||
```python Code
|
||||
from crewai.flow import (
|
||||
Flow, start, listen, human_feedback,
|
||||
HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext
|
||||
)
|
||||
|
||||
class SlackNotificationProvider(HumanFeedbackProvider):
|
||||
"""Provider que envia notificações Slack e pausa para feedback assíncrono."""
|
||||
|
||||
def __init__(self, channel: str):
|
||||
self.channel = channel
|
||||
|
||||
def request_feedback(self, context: PendingFeedbackContext, flow: Flow) -> str:
|
||||
# Envia notificação Slack (implemente você mesmo)
|
||||
slack_thread_id = self.post_to_slack(
|
||||
channel=self.channel,
|
||||
message=f"Revisão necessária:\n\n{context.method_output}\n\n{context.message}",
|
||||
)
|
||||
|
||||
# Pausa execução - framework cuida da persistência automaticamente
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={
|
||||
"slack_channel": self.channel,
|
||||
"thread_id": slack_thread_id,
|
||||
}
|
||||
)
|
||||
|
||||
class ContentPipeline(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Aprova este conteúdo para publicação?",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
provider=SlackNotificationProvider("#content-reviews"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "Conteúdo de blog post gerado por IA..."
|
||||
|
||||
@listen("approved")
|
||||
def publish(self, result):
|
||||
print(f"Publicando! Revisor disse: {result.feedback}")
|
||||
return {"status": "published"}
|
||||
|
||||
@listen("rejected")
|
||||
def archive(self, result):
|
||||
print(f"Arquivado. Motivo: {result.feedback}")
|
||||
return {"status": "archived"}
|
||||
|
||||
@listen("needs_revision")
|
||||
def queue_revision(self, result):
|
||||
print(f"Na fila para revisão: {result.feedback}")
|
||||
return {"status": "revision_needed"}
|
||||
|
||||
|
||||
# Iniciando o flow (vai pausar e aguardar resposta do Slack)
|
||||
def start_content_pipeline():
|
||||
flow = ContentPipeline()
|
||||
result = flow.kickoff()
|
||||
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
return {"status": "pending", "flow_id": result.context.flow_id}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Retomando quando webhook do Slack dispara (handler síncrono)
|
||||
def on_slack_feedback(flow_id: str, slack_message: str):
|
||||
flow = ContentPipeline.from_pending(flow_id)
|
||||
result = flow.resume(slack_message)
|
||||
return result
|
||||
|
||||
|
||||
# Se seu handler é assíncrono (FastAPI, aiohttp, Slack Bolt async, etc.)
|
||||
async def on_slack_feedback_async(flow_id: str, slack_message: str):
|
||||
flow = ContentPipeline.from_pending(flow_id)
|
||||
result = await flow.resume_async(slack_message)
|
||||
return result
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Se você está usando um framework web assíncrono (FastAPI, aiohttp, Slack Bolt modo async), use `await flow.resume_async()` em vez de `flow.resume()`. Chamar `resume()` de dentro de um event loop em execução vai lançar um `RuntimeError`.
|
||||
</Warning>
|
||||
|
||||
### Melhores Práticas para Feedback Assíncrono
|
||||
|
||||
1. **Verifique o tipo de retorno**: `kickoff()` retorna `HumanFeedbackPending` quando pausado—não precisa de try/except
|
||||
2. **Use o método resume correto**: Use `resume()` em código síncrono, `await resume_async()` em código assíncrono
|
||||
3. **Armazene informações de callback**: Use `callback_info` para armazenar URLs de webhook, IDs de tickets, etc.
|
||||
4. **Implemente idempotência**: Seu handler de resume deve ser idempotente por segurança
|
||||
5. **Persistência automática**: O estado é automaticamente salvo quando `HumanFeedbackPending` é lançado e usa `SQLiteFlowPersistence` por padrão
|
||||
6. **Persistência customizada**: Passe uma instância de persistência customizada para `from_pending()` se necessário
|
||||
|
||||
## Documentação Relacionada
|
||||
|
||||
- [Visão Geral de Flows](/pt-BR/concepts/flows) - Aprenda sobre CrewAI Flows
|
||||
- [Gerenciamento de Estado em Flows](/pt-BR/guides/flows/mastering-flow-state) - Gerenciando estado em flows
|
||||
- [Persistência de Flows](/pt-BR/concepts/flows#persistence) - Persistindo estado de flows
|
||||
- [Roteamento com @router](/pt-BR/concepts/flows#router) - Mais sobre roteamento condicional
|
||||
- [Input Humano na Execução](/pt-BR/learn/human-input-on-execution) - Input humano no nível de task
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
title: CrewAI Tracing
|
||||
description: Rastreamento integrado para Crews e Flows do CrewAI com a plataforma CrewAI AOP
|
||||
icon: magnifying-glass-chart
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# Rastreamento Integrado do CrewAI
|
||||
|
||||
O CrewAI fornece recursos de rastreamento integrados que permitem monitorar e depurar seus Crews e Flows em tempo real. Este guia demonstra como habilitar o rastreamento para **Crews** e **Flows** usando a plataforma de observabilidade integrada do CrewAI.
|
||||
|
||||
> **O que é o CrewAI Tracing?** O rastreamento integrado do CrewAI fornece observabilidade abrangente para seus agentes de IA, incluindo decisões de agentes, cronogramas de execução de tarefas, uso de ferramentas e chamadas de LLM - tudo acessível através da [plataforma CrewAI AOP](https://app.crewai.com).
|
||||
|
||||

|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
Antes de usar o rastreamento do CrewAI, você precisa:
|
||||
|
||||
1. **Conta CrewAI AOP**: Cadastre-se para uma conta gratuita em [app.crewai.com](https://app.crewai.com)
|
||||
2. **Autenticação CLI**: Use a CLI do CrewAI para autenticar seu ambiente local
|
||||
|
||||
```bash
|
||||
crewai login
|
||||
```
|
||||
|
||||
## Instruções de Configuração
|
||||
|
||||
### Passo 1: Crie sua Conta CrewAI AOP
|
||||
|
||||
Visite [app.crewai.com](https://app.crewai.com) e crie sua conta gratuita. Isso lhe dará acesso à plataforma CrewAI AOP, onde você pode visualizar rastreamentos, métricas e gerenciar seus crews.
|
||||
|
||||
### Passo 2: Instale a CLI do CrewAI e Autentique
|
||||
|
||||
Se você ainda não o fez, instale o CrewAI com as ferramentas CLI:
|
||||
|
||||
```bash
|
||||
uv add crewai[tools]
|
||||
```
|
||||
|
||||
Em seguida, autentique sua CLI com sua conta CrewAI AOP:
|
||||
|
||||
```bash
|
||||
crewai login
|
||||
```
|
||||
|
||||
Este comando irá:
|
||||
1. Abrir seu navegador na página de autenticação
|
||||
2. Solicitar que você insira um código de dispositivo
|
||||
3. Autenticar seu ambiente local com sua conta CrewAI AOP
|
||||
4. Habilitar recursos de rastreamento para seu desenvolvimento local
|
||||
|
||||
### Passo 3: Habilite o Rastreamento em seu Crew
|
||||
|
||||
Você pode habilitar o rastreamento para seu Crew definindo o parâmetro `tracing` como `True`:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai_tools import SerperDevTool
|
||||
|
||||
# Define your agents
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Uncover cutting-edge developments in AI and data science",
|
||||
backstory=\"\"\"You work at a leading tech think tank.
|
||||
Your expertise lies in identifying emerging trends.
|
||||
You have a knack for dissecting complex data and presenting actionable insights.\"\"\",
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()],
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
role="Tech Content Strategist",
|
||||
goal="Craft compelling content on tech advancements",
|
||||
backstory=\"\"\"You are a renowned Content Strategist, known for your insightful and engaging articles.
|
||||
You transform complex concepts into compelling narratives.\"\"\",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create tasks for your agents
|
||||
research_task = Task(
|
||||
description=\"\"\"Conduct a comprehensive analysis of the latest advancements in AI in 2024.
|
||||
Identify key trends, breakthrough technologies, and potential industry impacts.\"\"\",
|
||||
expected_output="Full analysis report in bullet points",
|
||||
agent=researcher,
|
||||
)
|
||||
|
||||
writing_task = Task(
|
||||
description=\"\"\"Using the insights provided, develop an engaging blog
|
||||
post that highlights the most significant AI advancements.
|
||||
Your post should be informative yet accessible, catering to a tech-savvy audience.\"\"\",
|
||||
expected_output="Full blog post of at least 4 paragraphs",
|
||||
agent=writer,
|
||||
)
|
||||
|
||||
# Enable tracing in your crew
|
||||
crew = Crew(
|
||||
agents=[researcher, writer],
|
||||
tasks=[research_task, writing_task],
|
||||
process=Process.sequential,
|
||||
tracing=True, # Enable built-in tracing
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Execute your crew
|
||||
result = crew.kickoff()
|
||||
```
|
||||
|
||||
### Passo 4: Habilite o Rastreamento em seu Flow
|
||||
|
||||
Da mesma forma, você pode habilitar o rastreamento para Flows do CrewAI:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ExampleState(BaseModel):
|
||||
counter: int = 0
|
||||
message: str = ""
|
||||
|
||||
class ExampleFlow(Flow[ExampleState]):
|
||||
def __init__(self):
|
||||
super().__init__(tracing=True) # Enable tracing for the flow
|
||||
|
||||
@start()
|
||||
def first_method(self):
|
||||
print("Starting the flow")
|
||||
self.state.counter = 1
|
||||
self.state.message = "Flow started"
|
||||
return "continue"
|
||||
|
||||
@listen("continue")
|
||||
def second_method(self):
|
||||
print("Continuing the flow")
|
||||
self.state.counter += 1
|
||||
self.state.message = "Flow continued"
|
||||
return "finish"
|
||||
|
||||
@listen("finish")
|
||||
def final_method(self):
|
||||
print("Finishing the flow")
|
||||
self.state.counter += 1
|
||||
self.state.message = "Flow completed"
|
||||
|
||||
# Create and run the flow with tracing enabled
|
||||
flow = ExampleFlow(tracing=True)
|
||||
result = flow.kickoff()
|
||||
```
|
||||
|
||||
### Passo 5: Visualize os Rastreamentos no Painel CrewAI AOP
|
||||
|
||||
Após executar o crew ou flow, você pode visualizar os rastreamentos gerados pela sua aplicação CrewAI no painel CrewAI AOP. Você verá etapas detalhadas das interações dos agentes, usos de ferramentas e chamadas de LLM.
|
||||
Basta clicar no link abaixo para visualizar os rastreamentos ou ir para a aba de rastreamentos no painel [aqui](https://app.crewai.com/crewai_plus/trace_batches)
|
||||

|
||||
|
||||
|
||||
### Alternativa: Configuração de Variável de Ambiente
|
||||
|
||||
Você também pode habilitar o rastreamento globalmente definindo uma variável de ambiente:
|
||||
|
||||
```bash
|
||||
export CREWAI_TRACING_ENABLED=true
|
||||
```
|
||||
|
||||
Ou adicione-a ao seu arquivo `.env`:
|
||||
|
||||
```env
|
||||
CREWAI_TRACING_ENABLED=true
|
||||
```
|
||||
|
||||
Quando esta variável de ambiente estiver definida, todos os Crews e Flows terão automaticamente o rastreamento habilitado, mesmo sem definir explicitamente `tracing=True`.
|
||||
|
||||
## Visualizando seus Rastreamentos
|
||||
|
||||
### Acesse o Painel CrewAI AOP
|
||||
|
||||
1. Visite [app.crewai.com](https://app.crewai.com) e faça login em sua conta
|
||||
2. Navegue até o painel do seu projeto
|
||||
3. Clique na aba **Traces** para visualizar os detalhes de execução
|
||||
|
||||
### O que Você Verá nos Rastreamentos
|
||||
|
||||
O rastreamento do CrewAI fornece visibilidade abrangente sobre:
|
||||
|
||||
- **Decisões dos Agentes**: Veja como os agentes raciocinam através das tarefas e tomam decisões
|
||||
- **Cronograma de Execução de Tarefas**: Representação visual de sequências e dependências de tarefas
|
||||
- **Uso de Ferramentas**: Monitore quais ferramentas são chamadas e seus resultados
|
||||
- **Chamadas de LLM**: Rastreie todas as interações do modelo de linguagem, incluindo prompts e respostas
|
||||
- **Métricas de Desempenho**: Tempos de execução, uso de tokens e custos
|
||||
- **Rastreamento de Erros**: Informações detalhadas de erros e rastreamentos de pilha
|
||||
|
||||
### Recursos de Rastreamento
|
||||
- **Cronograma de Execução**: Clique através de diferentes estágios de execução
|
||||
- **Logs Detalhados**: Acesse logs abrangentes para depuração
|
||||
- **Análise de Desempenho**: Analise padrões de execução e otimize o desempenho
|
||||
- **Capacidades de Exportação**: Baixe rastreamentos para análise adicional
|
||||
|
||||
### Problemas de Autenticação
|
||||
|
||||
Se você encontrar problemas de autenticação:
|
||||
|
||||
1. Certifique-se de estar logado: `crewai login`
|
||||
2. Verifique sua conexão com a internet
|
||||
3. Verifique sua conta em [app.crewai.com](https://app.crewai.com)
|
||||
|
||||
### Rastreamentos Não Aparecem
|
||||
|
||||
Se os rastreamentos não estiverem aparecendo no painel:
|
||||
|
||||
1. Confirme que `tracing=True` está definido em seu Crew/Flow
|
||||
2. Verifique se `CREWAI_TRACING_ENABLED=true` se estiver usando variáveis de ambiente
|
||||
3. Certifique-se de estar autenticado com `crewai login`
|
||||
4. Verifique se seu crew/flow está realmente executando
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.7.2",
|
||||
"crewai==1.7.1",
|
||||
"lancedb~=0.5.4",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
|
||||
@@ -291,4 +291,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.7.2"
|
||||
__version__ = "1.7.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Crewai Enterprise Tools."""
|
||||
import os
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Optional, Union, cast, get_origin
|
||||
@@ -432,11 +432,7 @@ class CrewAIPlatformActionTool(BaseTool):
|
||||
payload = cleaned_kwargs
|
||||
|
||||
response = requests.post(
|
||||
url=api_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=60,
|
||||
verify=os.environ.get("CREWAI_FACTORY", "false").lower() != "true",
|
||||
url=api_url, headers=headers, json=payload, timeout=60
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
import requests
|
||||
|
||||
@@ -37,7 +37,6 @@ class CrewaiPlatformToolBuilder:
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
params={"apps": ",".join(self._apps)},
|
||||
verify=os.environ.get("CREWAI_FACTORY", "false").lower() != "true",
|
||||
)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from typing import Union, get_args, get_origin
|
||||
from unittest.mock import patch, Mock
|
||||
import os
|
||||
|
||||
from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import (
|
||||
CrewAIPlatformActionTool,
|
||||
@@ -251,109 +249,3 @@ class TestSchemaProcessing:
|
||||
result_type = tool._process_schema_type(test_schema, "TestFieldAllOfMixed")
|
||||
|
||||
assert result_type is str
|
||||
|
||||
class TestCrewAIPlatformActionToolVerify:
|
||||
"""Test suite for SSL verification behavior based on CREWAI_FACTORY environment variable"""
|
||||
|
||||
def setup_method(self):
|
||||
self.action_schema = {
|
||||
"function": {
|
||||
"name": "test_action",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"test_param": {
|
||||
"type": "string",
|
||||
"description": "Test parameter"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def create_test_tool(self):
|
||||
return CrewAIPlatformActionTool(
|
||||
description="Test action tool",
|
||||
action_name="test_action",
|
||||
action_schema=self.action_schema
|
||||
)
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, clear=True)
|
||||
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
|
||||
def test_run_with_ssl_verification_default(self, mock_post):
|
||||
"""Test that _run uses SSL verification by default when CREWAI_FACTORY is not set"""
|
||||
mock_response = Mock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = self.create_test_tool()
|
||||
tool._run(test_param="test_value")
|
||||
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
assert call_args.kwargs["verify"] is True
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "false"}, clear=True)
|
||||
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
|
||||
def test_run_with_ssl_verification_factory_false(self, mock_post):
|
||||
"""Test that _run uses SSL verification when CREWAI_FACTORY is 'false'"""
|
||||
mock_response = Mock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = self.create_test_tool()
|
||||
tool._run(test_param="test_value")
|
||||
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
assert call_args.kwargs["verify"] is True
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "FALSE"}, clear=True)
|
||||
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
|
||||
def test_run_with_ssl_verification_factory_false_uppercase(self, mock_post):
|
||||
"""Test that _run uses SSL verification when CREWAI_FACTORY is 'FALSE' (case-insensitive)"""
|
||||
mock_response = Mock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = self.create_test_tool()
|
||||
tool._run(test_param="test_value")
|
||||
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
assert call_args.kwargs["verify"] is True
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "true"}, clear=True)
|
||||
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
|
||||
def test_run_without_ssl_verification_factory_true(self, mock_post):
|
||||
"""Test that _run disables SSL verification when CREWAI_FACTORY is 'true'"""
|
||||
mock_response = Mock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = self.create_test_tool()
|
||||
tool._run(test_param="test_value")
|
||||
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
assert call_args.kwargs["verify"] is False
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "TRUE"}, clear=True)
|
||||
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
|
||||
def test_run_without_ssl_verification_factory_true_uppercase(self, mock_post):
|
||||
"""Test that _run disables SSL verification when CREWAI_FACTORY is 'TRUE' (case-insensitive)"""
|
||||
mock_response = Mock()
|
||||
mock_response.ok = True
|
||||
mock_response.json.return_value = {"result": "success"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = self.create_test_tool()
|
||||
tool._run(test_param="test_value")
|
||||
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
assert call_args.kwargs["verify"] is False
|
||||
|
||||
@@ -258,98 +258,3 @@ class TestCrewaiPlatformToolBuilder(unittest.TestCase):
|
||||
assert "simple_string" in description_text
|
||||
assert "nested_object" in description_text
|
||||
assert "array_prop" in description_text
|
||||
|
||||
|
||||
|
||||
class TestCrewaiPlatformToolBuilderVerify(unittest.TestCase):
|
||||
"""Test suite for SSL verification behavior in CrewaiPlatformToolBuilder"""
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, clear=True)
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_fetch_actions_with_ssl_verification_default(self, mock_get):
|
||||
"""Test that _fetch_actions uses SSL verification by default when CREWAI_FACTORY is not set"""
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"actions": {}}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github"])
|
||||
builder._fetch_actions()
|
||||
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args
|
||||
assert call_args.kwargs["verify"] is True
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "false"}, clear=True)
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_fetch_actions_with_ssl_verification_factory_false(self, mock_get):
|
||||
"""Test that _fetch_actions uses SSL verification when CREWAI_FACTORY is 'false'"""
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"actions": {}}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github"])
|
||||
builder._fetch_actions()
|
||||
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args
|
||||
assert call_args.kwargs["verify"] is True
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "FALSE"}, clear=True)
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_fetch_actions_with_ssl_verification_factory_false_uppercase(self, mock_get):
|
||||
"""Test that _fetch_actions uses SSL verification when CREWAI_FACTORY is 'FALSE' (case-insensitive)"""
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"actions": {}}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github"])
|
||||
builder._fetch_actions()
|
||||
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args
|
||||
assert call_args.kwargs["verify"] is True
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "true"}, clear=True)
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_fetch_actions_without_ssl_verification_factory_true(self, mock_get):
|
||||
"""Test that _fetch_actions disables SSL verification when CREWAI_FACTORY is 'true'"""
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"actions": {}}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github"])
|
||||
builder._fetch_actions()
|
||||
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args
|
||||
assert call_args.kwargs["verify"] is False
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "TRUE"}, clear=True)
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_fetch_actions_without_ssl_verification_factory_true_uppercase(self, mock_get):
|
||||
"""Test that _fetch_actions disables SSL verification when CREWAI_FACTORY is 'TRUE' (case-insensitive)"""
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"actions": {}}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github"])
|
||||
builder._fetch_actions()
|
||||
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args
|
||||
assert call_args.kwargs["verify"] is False
|
||||
|
||||
@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.7.2",
|
||||
"crewai-tools==1.7.1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.7.2"
|
||||
__version__ = "1.7.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Sequence
|
||||
from collections.abc import Sequence
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
@@ -44,7 +44,6 @@ from crewai.events.types.memory_events import (
|
||||
MemoryRetrievalCompletedEvent,
|
||||
MemoryRetrievalStartedEvent,
|
||||
)
|
||||
from crewai.experimental.crew_agent_executor_flow import CrewAgentExecutorFlow
|
||||
from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||
from crewai.lite_agent import LiteAgent
|
||||
@@ -106,7 +105,7 @@ class Agent(BaseAgent):
|
||||
The agent can also have memory, can operate in verbose mode, and can delegate tasks to other agents.
|
||||
|
||||
Attributes:
|
||||
agent_executor: An instance of the CrewAgentExecutor or CrewAgentExecutorFlow class.
|
||||
agent_executor: An instance of the CrewAgentExecutor class.
|
||||
role: The role of the agent.
|
||||
goal: The objective of the agent.
|
||||
backstory: The backstory of the agent.
|
||||
@@ -222,10 +221,6 @@ class Agent(BaseAgent):
|
||||
default=None,
|
||||
description="A2A (Agent-to-Agent) configuration for delegating tasks to remote agents. Can be a single A2AConfig or a dict mapping agent IDs to configs.",
|
||||
)
|
||||
executor_class: type[CrewAgentExecutor] | type[CrewAgentExecutorFlow] = Field(
|
||||
default=CrewAgentExecutor,
|
||||
description="Class to use for the agent executor. Defaults to CrewAgentExecutor, can optionally use CrewAgentExecutorFlow.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
def validate_from_repository(cls, v: Any) -> dict[str, Any] | None | Any: # noqa: N805
|
||||
@@ -726,83 +721,29 @@ class Agent(BaseAgent):
|
||||
self.response_template.split("{{ .Response }}")[1].strip()
|
||||
)
|
||||
|
||||
rpm_limit_fn = (
|
||||
self._rpm_controller.check_or_wait if self._rpm_controller else None
|
||||
self.agent_executor = CrewAgentExecutor(
|
||||
llm=self.llm, # type: ignore[arg-type]
|
||||
task=task, # type: ignore[arg-type]
|
||||
agent=self,
|
||||
crew=self.crew,
|
||||
tools=parsed_tools,
|
||||
prompt=prompt,
|
||||
original_tools=raw_tools,
|
||||
stop_words=stop_words,
|
||||
max_iter=self.max_iter,
|
||||
tools_handler=self.tools_handler,
|
||||
tools_names=get_tool_names(parsed_tools),
|
||||
tools_description=render_text_description_and_args(parsed_tools),
|
||||
step_callback=self.step_callback,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
respect_context_window=self.respect_context_window,
|
||||
request_within_rpm_limit=(
|
||||
self._rpm_controller.check_or_wait if self._rpm_controller else None
|
||||
),
|
||||
callbacks=[TokenCalcHandler(self._token_process)],
|
||||
response_model=task.response_model if task else None,
|
||||
)
|
||||
|
||||
if self.agent_executor is not None:
|
||||
self._update_executor_parameters(
|
||||
task=task,
|
||||
tools=parsed_tools,
|
||||
raw_tools=raw_tools,
|
||||
prompt=prompt,
|
||||
stop_words=stop_words,
|
||||
rpm_limit_fn=rpm_limit_fn,
|
||||
)
|
||||
else:
|
||||
self.agent_executor = self.executor_class(
|
||||
llm=cast(BaseLLM, self.llm),
|
||||
task=task,
|
||||
i18n=self.i18n,
|
||||
agent=self,
|
||||
crew=self.crew,
|
||||
tools=parsed_tools,
|
||||
prompt=prompt,
|
||||
original_tools=raw_tools,
|
||||
stop_words=stop_words,
|
||||
max_iter=self.max_iter,
|
||||
tools_handler=self.tools_handler,
|
||||
tools_names=get_tool_names(parsed_tools),
|
||||
tools_description=render_text_description_and_args(parsed_tools),
|
||||
step_callback=self.step_callback,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
respect_context_window=self.respect_context_window,
|
||||
request_within_rpm_limit=rpm_limit_fn,
|
||||
callbacks=[TokenCalcHandler(self._token_process)],
|
||||
response_model=task.response_model if task else None,
|
||||
)
|
||||
|
||||
def _update_executor_parameters(
|
||||
self,
|
||||
task: Task | None,
|
||||
tools: list,
|
||||
raw_tools: list[BaseTool],
|
||||
prompt: dict,
|
||||
stop_words: list[str],
|
||||
rpm_limit_fn: Callable | None,
|
||||
) -> None:
|
||||
"""Update executor parameters without recreating instance.
|
||||
|
||||
Args:
|
||||
task: Task to execute.
|
||||
tools: Parsed tools.
|
||||
raw_tools: Original tools.
|
||||
prompt: Generated prompt.
|
||||
stop_words: Stop words list.
|
||||
rpm_limit_fn: RPM limit callback function.
|
||||
"""
|
||||
self.agent_executor.task = task
|
||||
self.agent_executor.tools = tools
|
||||
self.agent_executor.original_tools = raw_tools
|
||||
self.agent_executor.prompt = prompt
|
||||
self.agent_executor.stop = stop_words
|
||||
self.agent_executor.tools_names = get_tool_names(tools)
|
||||
self.agent_executor.tools_description = render_text_description_and_args(tools)
|
||||
self.agent_executor.response_model = task.response_model if task else None
|
||||
|
||||
self.agent_executor.tools_handler = self.tools_handler
|
||||
self.agent_executor.request_within_rpm_limit = rpm_limit_fn
|
||||
|
||||
if self.agent_executor.llm:
|
||||
existing_stop = getattr(self.agent_executor.llm, "stop", [])
|
||||
self.agent_executor.llm.stop = list(
|
||||
set(
|
||||
existing_stop + stop_words
|
||||
if isinstance(existing_stop, list)
|
||||
else stop_words
|
||||
)
|
||||
)
|
||||
|
||||
def get_delegation_tools(self, agents: list[BaseAgent]) -> list[BaseTool]:
|
||||
agent_tools = AgentTools(agents=agents)
|
||||
return agent_tools.tools()
|
||||
|
||||
@@ -457,6 +457,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
if self.cache:
|
||||
self.cache_handler = cache_handler
|
||||
self.tools_handler.cache = cache_handler
|
||||
self.create_agent_executor()
|
||||
|
||||
def set_rpm_controller(self, rpm_controller: RPMController) -> None:
|
||||
"""Set the rpm controller for the agent.
|
||||
@@ -466,6 +467,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
"""
|
||||
if not self._rpm_controller:
|
||||
self._rpm_controller = rpm_controller
|
||||
self.create_agent_executor()
|
||||
|
||||
def set_knowledge(self, crew_embedder: EmbedderConfig | None = None) -> None:
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from crewai.agents.parser import AgentFinish
|
||||
from crewai.events.event_listener import event_listener
|
||||
from crewai.memory.entity.entity_memory_item import EntityMemoryItem
|
||||
from crewai.memory.long_term.long_term_memory_item import LongTermMemoryItem
|
||||
@@ -30,7 +29,7 @@ class CrewAgentExecutorMixin:
|
||||
_i18n: I18N
|
||||
_printer: Printer = Printer()
|
||||
|
||||
def _create_short_term_memory(self, output: AgentFinish) -> None:
|
||||
def _create_short_term_memory(self, output) -> None:
|
||||
"""Create and save a short-term memory item if conditions are met."""
|
||||
if (
|
||||
self.crew
|
||||
@@ -54,7 +53,7 @@ class CrewAgentExecutorMixin:
|
||||
"error", f"Failed to add to short term memory: {e}"
|
||||
)
|
||||
|
||||
def _create_external_memory(self, output: AgentFinish) -> None:
|
||||
def _create_external_memory(self, output) -> None:
|
||||
"""Create and save a external-term memory item if conditions are met."""
|
||||
if (
|
||||
self.crew
|
||||
@@ -76,7 +75,7 @@ class CrewAgentExecutorMixin:
|
||||
"error", f"Failed to add to external memory: {e}"
|
||||
)
|
||||
|
||||
def _create_long_term_memory(self, output: AgentFinish) -> None:
|
||||
def _create_long_term_memory(self, output) -> None:
|
||||
"""Create and save long-term and entity memory items based on evaluation."""
|
||||
if (
|
||||
self.crew
|
||||
@@ -137,50 +136,40 @@ class CrewAgentExecutorMixin:
|
||||
)
|
||||
|
||||
def _ask_human_input(self, final_answer: str) -> str:
|
||||
"""Prompt human input with mode-appropriate messaging.
|
||||
|
||||
Note: The final answer is already displayed via the AgentLogsExecutionEvent
|
||||
panel, so we only show the feedback prompt here.
|
||||
"""
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
formatter = event_listener.formatter
|
||||
formatter.pause_live_updates()
|
||||
|
||||
"""Prompt human input with mode-appropriate messaging."""
|
||||
event_listener.formatter.pause_live_updates()
|
||||
try:
|
||||
self._printer.print(
|
||||
content=f"\033[1m\033[95m ## Final Result:\033[00m \033[92m{final_answer}\033[00m"
|
||||
)
|
||||
|
||||
# Training mode prompt (single iteration)
|
||||
if self.crew and getattr(self.crew, "_train", False):
|
||||
prompt_text = (
|
||||
"TRAINING MODE: Provide feedback to improve the agent's performance.\n\n"
|
||||
prompt = (
|
||||
"\n\n=====\n"
|
||||
"## TRAINING MODE: Provide feedback to improve the agent's performance.\n"
|
||||
"This will be used to train better versions of the agent.\n"
|
||||
"Please provide detailed feedback about the result quality and reasoning process."
|
||||
"Please provide detailed feedback about the result quality and reasoning process.\n"
|
||||
"=====\n"
|
||||
)
|
||||
title = "🎓 Training Feedback Required"
|
||||
# Regular human-in-the-loop prompt (multiple iterations)
|
||||
else:
|
||||
prompt_text = (
|
||||
"Provide feedback on the Final Result above.\n\n"
|
||||
"• If you are happy with the result, simply hit Enter without typing anything.\n"
|
||||
"• Otherwise, provide specific improvement requests.\n"
|
||||
"• You can provide multiple rounds of feedback until satisfied."
|
||||
prompt = (
|
||||
"\n\n=====\n"
|
||||
"## HUMAN FEEDBACK: Provide feedback on the Final Result and Agent's actions.\n"
|
||||
"Please follow these guidelines:\n"
|
||||
" - If you are happy with the result, simply hit Enter without typing anything.\n"
|
||||
" - Otherwise, provide specific improvement requests.\n"
|
||||
" - You can provide multiple rounds of feedback until satisfied.\n"
|
||||
"=====\n"
|
||||
)
|
||||
title = "💬 Human Feedback Required"
|
||||
|
||||
content = Text()
|
||||
content.append(prompt_text, style="yellow")
|
||||
|
||||
prompt_panel = Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
)
|
||||
formatter.console.print(prompt_panel)
|
||||
|
||||
self._printer.print(content=prompt, color="bold_yellow")
|
||||
response = input()
|
||||
if response.strip() != "":
|
||||
formatter.console.print("\n[cyan]Processing your feedback...[/cyan]")
|
||||
self._printer.print(
|
||||
content="\nProcessing your feedback...", color="cyan"
|
||||
)
|
||||
return response
|
||||
finally:
|
||||
formatter.resume_live_updates()
|
||||
event_listener.formatter.resume_live_updates()
|
||||
|
||||
@@ -7,7 +7,6 @@ and memory management.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from pydantic import BaseModel, GetCoreSchemaHandler
|
||||
@@ -52,8 +51,6 @@ from crewai.utilities.tool_utils import (
|
||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
@@ -94,7 +91,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
request_within_rpm_limit: Callable[[], bool] | None = None,
|
||||
callbacks: list[Any] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
i18n: I18N | None = None,
|
||||
) -> None:
|
||||
"""Initialize executor.
|
||||
|
||||
@@ -118,7 +114,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
callbacks: Optional callbacks list.
|
||||
response_model: Optional Pydantic model for structured outputs.
|
||||
"""
|
||||
self._i18n: I18N = i18n or get_i18n()
|
||||
self._i18n: I18N = get_i18n()
|
||||
self.llm = llm
|
||||
self.task = task
|
||||
self.agent = agent
|
||||
@@ -544,7 +540,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
if self.agent is None:
|
||||
raise ValueError("Agent cannot be None")
|
||||
|
||||
future = crewai_event_bus.emit(
|
||||
crewai_event_bus.emit(
|
||||
self.agent,
|
||||
AgentLogsExecutionEvent(
|
||||
agent_role=self.agent.role,
|
||||
@@ -554,12 +550,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
),
|
||||
)
|
||||
|
||||
if future is not None:
|
||||
try:
|
||||
future.result(timeout=5.0)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to show logs for agent execution event: {e}")
|
||||
|
||||
def _handle_crew_training_output(
|
||||
self, result: AgentFinish, human_feedback: str | None = None
|
||||
) -> None:
|
||||
|
||||
@@ -149,9 +149,7 @@ class AuthenticationCommand:
|
||||
return
|
||||
|
||||
if token_data["error"] not in ("authorization_pending", "slow_down"):
|
||||
raise requests.HTTPError(
|
||||
token_data.get("error_description") or token_data.get("error")
|
||||
)
|
||||
raise requests.HTTPError(token_data["error_description"])
|
||||
|
||||
time.sleep(device_code_data["interval"])
|
||||
attempts += 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
from crewai.cli.config import Settings
|
||||
@@ -33,7 +33,9 @@ class PlusAPI:
|
||||
if settings.org_uuid:
|
||||
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
|
||||
|
||||
self.base_url = os.getenv("CREWAI_PLUS_URL") or str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
self.base_url = (
|
||||
str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
)
|
||||
|
||||
def _make_request(
|
||||
self, method: str, endpoint: str, **kwargs: Any
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.7.2"
|
||||
"crewai[tools]==1.7.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.7.2"
|
||||
"crewai[tools]==1.7.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -12,7 +12,6 @@ from rich.console import Console
|
||||
from crewai.cli import git
|
||||
from crewai.cli.command import BaseCommand, PlusAPIMixin
|
||||
from crewai.cli.config import Settings
|
||||
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai.cli.utils import (
|
||||
build_env_with_tool_repository_credentials,
|
||||
extract_available_exports,
|
||||
@@ -132,13 +131,10 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
self._validate_response(publish_response)
|
||||
|
||||
published_handle = publish_response.json()["handle"]
|
||||
settings = Settings()
|
||||
base_url = settings.enterprise_base_url or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
|
||||
console.print(
|
||||
f"Successfully published `{published_handle}` ({project_version}).\n\n"
|
||||
+ "⚠️ Security checks are running in the background. Your tool will be available once these are complete.\n"
|
||||
+ f"You can monitor the status or access your tool here:\n{base_url}/crewai_plus/tools/{published_handle}",
|
||||
+ f"You can monitor the status or access your tool here:\nhttps://app.crewai.com/crewai_plus/tools/{published_handle}",
|
||||
style="bold green",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import StringIO
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import Field, PrivateAttr
|
||||
@@ -16,6 +17,8 @@ from crewai.events.types.a2a_events import (
|
||||
A2AResponseReceivedEvent,
|
||||
)
|
||||
from crewai.events.types.agent_events import (
|
||||
AgentExecutionCompletedEvent,
|
||||
AgentExecutionStartedEvent,
|
||||
LiteAgentExecutionCompletedEvent,
|
||||
LiteAgentExecutionErrorEvent,
|
||||
LiteAgentExecutionStartedEvent,
|
||||
@@ -35,16 +38,15 @@ from crewai.events.types.crew_events import (
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowFinishedEvent,
|
||||
FlowPausedEvent,
|
||||
FlowStartedEvent,
|
||||
MethodExecutionFailedEvent,
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionPausedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
)
|
||||
from crewai.events.types.knowledge_events import (
|
||||
KnowledgeQueryCompletedEvent,
|
||||
KnowledgeQueryFailedEvent,
|
||||
KnowledgeQueryStartedEvent,
|
||||
KnowledgeRetrievalCompletedEvent,
|
||||
KnowledgeRetrievalStartedEvent,
|
||||
KnowledgeSearchQueryFailedEvent,
|
||||
@@ -108,6 +110,7 @@ class EventListener(BaseEventListener):
|
||||
text_stream: StringIO = StringIO()
|
||||
knowledge_retrieval_in_progress: bool = False
|
||||
knowledge_query_in_progress: bool = False
|
||||
method_branches: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def __new__(cls) -> EventListener:
|
||||
if cls._instance is None:
|
||||
@@ -121,8 +124,10 @@ class EventListener(BaseEventListener):
|
||||
self._telemetry = Telemetry()
|
||||
self._telemetry.set_tracer()
|
||||
self.execution_spans = {}
|
||||
self.method_branches = {}
|
||||
self._initialized = True
|
||||
self.formatter = ConsoleFormatter(verbose=True)
|
||||
self._crew_tree_lock = threading.Condition()
|
||||
|
||||
# Initialize trace listener with formatter for memory event handling
|
||||
trace_listener = TraceCollectionListener()
|
||||
@@ -133,10 +138,12 @@ class EventListener(BaseEventListener):
|
||||
def setup_listeners(self, crewai_event_bus: CrewAIEventsBus) -> None:
|
||||
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
self.formatter.handle_crew_started(event.crew_name or "Crew", source.id)
|
||||
source._execution_span = self._telemetry.crew_execution_span(
|
||||
source, event.inputs
|
||||
)
|
||||
with self._crew_tree_lock:
|
||||
self.formatter.create_crew_tree(event.crew_name or "Crew", source.id)
|
||||
source._execution_span = self._telemetry.crew_execution_span(
|
||||
source, event.inputs
|
||||
)
|
||||
self._crew_tree_lock.notify_all()
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffCompletedEvent)
|
||||
def on_crew_completed(source: Any, event: CrewKickoffCompletedEvent) -> None:
|
||||
@@ -144,7 +151,8 @@ class EventListener(BaseEventListener):
|
||||
final_string_output = event.output.raw
|
||||
self._telemetry.end_crew(source, final_string_output)
|
||||
|
||||
self.formatter.handle_crew_status(
|
||||
self.formatter.update_crew_tree(
|
||||
self.formatter.current_crew_tree,
|
||||
event.crew_name or "Crew",
|
||||
source.id,
|
||||
"completed",
|
||||
@@ -153,7 +161,8 @@ class EventListener(BaseEventListener):
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffFailedEvent)
|
||||
def on_crew_failed(source: Any, event: CrewKickoffFailedEvent) -> None:
|
||||
self.formatter.handle_crew_status(
|
||||
self.formatter.update_crew_tree(
|
||||
self.formatter.current_crew_tree,
|
||||
event.crew_name or "Crew",
|
||||
source.id,
|
||||
"failed",
|
||||
@@ -186,22 +195,23 @@ class EventListener(BaseEventListener):
|
||||
|
||||
# ----------- TASK EVENTS -----------
|
||||
|
||||
def get_task_name(source: Any) -> str | None:
|
||||
return (
|
||||
source.name
|
||||
if hasattr(source, "name") and source.name
|
||||
else source.description
|
||||
if hasattr(source, "description") and source.description
|
||||
else None
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(TaskStartedEvent)
|
||||
def on_task_started(source: Any, event: TaskStartedEvent) -> None:
|
||||
span = self._telemetry.task_started(crew=source.agent.crew, task=source)
|
||||
self.execution_spans[source] = span
|
||||
|
||||
task_name = get_task_name(source)
|
||||
self.formatter.handle_task_started(source.id, task_name)
|
||||
with self._crew_tree_lock:
|
||||
self._crew_tree_lock.wait_for(
|
||||
lambda: self.formatter.current_crew_tree is not None, timeout=5.0
|
||||
)
|
||||
|
||||
if self.formatter.current_crew_tree is not None:
|
||||
task_name = (
|
||||
source.name if hasattr(source, "name") and source.name else None
|
||||
)
|
||||
self.formatter.create_task_branch(
|
||||
self.formatter.current_crew_tree, source.id, task_name
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(TaskCompletedEvent)
|
||||
def on_task_completed(source: Any, event: TaskCompletedEvent) -> None:
|
||||
@@ -212,9 +222,13 @@ class EventListener(BaseEventListener):
|
||||
self.execution_spans[source] = None
|
||||
|
||||
# Pass task name if it exists
|
||||
task_name = get_task_name(source)
|
||||
self.formatter.handle_task_status(
|
||||
source.id, source.agent.role, "completed", task_name
|
||||
task_name = source.name if hasattr(source, "name") and source.name else None
|
||||
self.formatter.update_task_status(
|
||||
self.formatter.current_crew_tree,
|
||||
source.id,
|
||||
source.agent.role,
|
||||
"completed",
|
||||
task_name,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(TaskFailedEvent)
|
||||
@@ -226,12 +240,37 @@ class EventListener(BaseEventListener):
|
||||
self.execution_spans[source] = None
|
||||
|
||||
# Pass task name if it exists
|
||||
task_name = get_task_name(source)
|
||||
self.formatter.handle_task_status(
|
||||
source.id, source.agent.role, "failed", task_name
|
||||
task_name = source.name if hasattr(source, "name") and source.name else None
|
||||
self.formatter.update_task_status(
|
||||
self.formatter.current_crew_tree,
|
||||
source.id,
|
||||
source.agent.role,
|
||||
"failed",
|
||||
task_name,
|
||||
)
|
||||
|
||||
# ----------- AGENT EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(AgentExecutionStartedEvent)
|
||||
def on_agent_execution_started(
|
||||
_: Any, event: AgentExecutionStartedEvent
|
||||
) -> None:
|
||||
self.formatter.create_agent_branch(
|
||||
self.formatter.current_task_branch,
|
||||
event.agent.role,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentExecutionCompletedEvent)
|
||||
def on_agent_execution_completed(
|
||||
_: Any, event: AgentExecutionCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.update_agent_status(
|
||||
self.formatter.current_agent_branch,
|
||||
event.agent.role,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
# ----------- LITE AGENT EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(LiteAgentExecutionStartedEvent)
|
||||
@@ -275,61 +314,57 @@ class EventListener(BaseEventListener):
|
||||
self._telemetry.flow_execution_span(
|
||||
event.flow_name, list(source._methods.keys())
|
||||
)
|
||||
self.formatter.handle_flow_created(event.flow_name, str(source.flow_id))
|
||||
self.formatter.handle_flow_started(event.flow_name, str(source.flow_id))
|
||||
tree = self.formatter.create_flow_tree(event.flow_name, str(source.flow_id))
|
||||
self.formatter.current_flow_tree = tree
|
||||
self.formatter.start_flow(event.flow_name, str(source.flow_id))
|
||||
|
||||
@crewai_event_bus.on(FlowFinishedEvent)
|
||||
def on_flow_finished(source: Any, event: FlowFinishedEvent) -> None:
|
||||
self.formatter.handle_flow_status(
|
||||
event.flow_name,
|
||||
source.flow_id,
|
||||
self.formatter.update_flow_status(
|
||||
self.formatter.current_flow_tree, event.flow_name, source.flow_id
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionStartedEvent)
|
||||
def on_method_execution_started(
|
||||
_: Any, event: MethodExecutionStartedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_method_status(
|
||||
method_branch = self.method_branches.get(event.method_name)
|
||||
updated_branch = self.formatter.update_method_status(
|
||||
method_branch,
|
||||
self.formatter.current_flow_tree,
|
||||
event.method_name,
|
||||
"running",
|
||||
)
|
||||
self.method_branches[event.method_name] = updated_branch
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionFinishedEvent)
|
||||
def on_method_execution_finished(
|
||||
_: Any, event: MethodExecutionFinishedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_method_status(
|
||||
method_branch = self.method_branches.get(event.method_name)
|
||||
updated_branch = self.formatter.update_method_status(
|
||||
method_branch,
|
||||
self.formatter.current_flow_tree,
|
||||
event.method_name,
|
||||
"completed",
|
||||
)
|
||||
self.method_branches[event.method_name] = updated_branch
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionFailedEvent)
|
||||
def on_method_execution_failed(
|
||||
_: Any, event: MethodExecutionFailedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_method_status(
|
||||
method_branch = self.method_branches.get(event.method_name)
|
||||
updated_branch = self.formatter.update_method_status(
|
||||
method_branch,
|
||||
self.formatter.current_flow_tree,
|
||||
event.method_name,
|
||||
"failed",
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionPausedEvent)
|
||||
def on_method_execution_paused(
|
||||
_: Any, event: MethodExecutionPausedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_method_status(
|
||||
event.method_name,
|
||||
"paused",
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(FlowPausedEvent)
|
||||
def on_flow_paused(_: Any, event: FlowPausedEvent) -> None:
|
||||
self.formatter.handle_flow_status(
|
||||
event.flow_name,
|
||||
event.flow_id,
|
||||
"paused",
|
||||
)
|
||||
self.method_branches[event.method_name] = updated_branch
|
||||
|
||||
# ----------- TOOL USAGE EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(ToolUsageStartedEvent)
|
||||
def on_tool_usage_started(source: Any, event: ToolUsageStartedEvent) -> None:
|
||||
if isinstance(source, LLM):
|
||||
@@ -339,9 +374,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
else:
|
||||
self.formatter.handle_tool_usage_started(
|
||||
self.formatter.current_agent_branch,
|
||||
event.tool_name,
|
||||
event.tool_args,
|
||||
event.run_attempts,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(ToolUsageFinishedEvent)
|
||||
@@ -350,6 +385,12 @@ class EventListener(BaseEventListener):
|
||||
self.formatter.handle_llm_tool_usage_finished(
|
||||
event.tool_name,
|
||||
)
|
||||
else:
|
||||
self.formatter.handle_tool_usage_finished(
|
||||
self.formatter.current_tool_branch,
|
||||
event.tool_name,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(ToolUsageErrorEvent)
|
||||
def on_tool_usage_error(source: Any, event: ToolUsageErrorEvent) -> None:
|
||||
@@ -360,9 +401,10 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
else:
|
||||
self.formatter.handle_tool_usage_error(
|
||||
self.formatter.current_tool_branch,
|
||||
event.tool_name,
|
||||
event.error,
|
||||
event.run_attempts,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
# ----------- LLM EVENTS -----------
|
||||
@@ -371,15 +413,32 @@ class EventListener(BaseEventListener):
|
||||
def on_llm_call_started(_: Any, event: LLMCallStartedEvent) -> None:
|
||||
self.text_stream = StringIO()
|
||||
self.next_chunk = 0
|
||||
# Capture the returned tool branch and update the current_tool_branch reference
|
||||
thinking_branch = self.formatter.handle_llm_call_started(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
# Update the formatter's current_tool_branch to ensure proper cleanup
|
||||
if thinking_branch is not None:
|
||||
self.formatter.current_tool_branch = thinking_branch
|
||||
|
||||
@crewai_event_bus.on(LLMCallCompletedEvent)
|
||||
def on_llm_call_completed(_: Any, event: LLMCallCompletedEvent) -> None:
|
||||
self.formatter.handle_llm_stream_completed()
|
||||
self.formatter.handle_llm_call_completed(
|
||||
self.formatter.current_tool_branch,
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(LLMCallFailedEvent)
|
||||
def on_llm_call_failed(_: Any, event: LLMCallFailedEvent) -> None:
|
||||
self.formatter.handle_llm_stream_completed()
|
||||
self.formatter.handle_llm_call_failed(event.error)
|
||||
self.formatter.handle_llm_call_failed(
|
||||
self.formatter.current_tool_branch,
|
||||
event.error,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||
def on_llm_stream_chunk(_: Any, event: LLMStreamChunkEvent) -> None:
|
||||
@@ -390,7 +449,9 @@ class EventListener(BaseEventListener):
|
||||
|
||||
accumulated_text = self.text_stream.getvalue()
|
||||
self.formatter.handle_llm_stream_chunk(
|
||||
event.chunk,
|
||||
accumulated_text,
|
||||
self.formatter.current_crew_tree,
|
||||
event.call_type,
|
||||
)
|
||||
|
||||
@@ -430,6 +491,7 @@ class EventListener(BaseEventListener):
|
||||
@crewai_event_bus.on(CrewTestCompletedEvent)
|
||||
def on_crew_test_completed(_: Any, event: CrewTestCompletedEvent) -> None:
|
||||
self.formatter.handle_crew_test_completed(
|
||||
self.formatter.current_flow_tree,
|
||||
event.crew_name or "Crew",
|
||||
)
|
||||
|
||||
@@ -446,7 +508,10 @@ class EventListener(BaseEventListener):
|
||||
|
||||
self.knowledge_retrieval_in_progress = True
|
||||
|
||||
self.formatter.handle_knowledge_retrieval_started()
|
||||
self.formatter.handle_knowledge_retrieval_started(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(KnowledgeRetrievalCompletedEvent)
|
||||
def on_knowledge_retrieval_completed(
|
||||
@@ -457,13 +522,24 @@ class EventListener(BaseEventListener):
|
||||
|
||||
self.knowledge_retrieval_in_progress = False
|
||||
self.formatter.handle_knowledge_retrieval_completed(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
event.retrieved_knowledge,
|
||||
event.query,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(KnowledgeQueryStartedEvent)
|
||||
def on_knowledge_query_started(
|
||||
_: Any, event: KnowledgeQueryStartedEvent
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@crewai_event_bus.on(KnowledgeQueryFailedEvent)
|
||||
def on_knowledge_query_failed(_: Any, event: KnowledgeQueryFailedEvent) -> None:
|
||||
self.formatter.handle_knowledge_query_failed(event.error)
|
||||
self.formatter.handle_knowledge_query_failed(
|
||||
self.formatter.current_agent_branch,
|
||||
event.error,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(KnowledgeQueryCompletedEvent)
|
||||
def on_knowledge_query_completed(
|
||||
@@ -475,7 +551,11 @@ class EventListener(BaseEventListener):
|
||||
def on_knowledge_search_query_failed(
|
||||
_: Any, event: KnowledgeSearchQueryFailedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_knowledge_search_query_failed(event.error)
|
||||
self.formatter.handle_knowledge_search_query_failed(
|
||||
self.formatter.current_agent_branch,
|
||||
event.error,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
# ----------- REASONING EVENTS -----------
|
||||
|
||||
@@ -483,7 +563,11 @@ class EventListener(BaseEventListener):
|
||||
def on_agent_reasoning_started(
|
||||
_: Any, event: AgentReasoningStartedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_reasoning_started(event.attempt)
|
||||
self.formatter.handle_reasoning_started(
|
||||
self.formatter.current_agent_branch,
|
||||
event.attempt,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentReasoningCompletedEvent)
|
||||
def on_agent_reasoning_completed(
|
||||
@@ -492,12 +576,14 @@ class EventListener(BaseEventListener):
|
||||
self.formatter.handle_reasoning_completed(
|
||||
event.plan,
|
||||
event.ready,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentReasoningFailedEvent)
|
||||
def on_agent_reasoning_failed(_: Any, event: AgentReasoningFailedEvent) -> None:
|
||||
self.formatter.handle_reasoning_failed(
|
||||
event.error,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
# ----------- AGENT LOGGING EVENTS -----------
|
||||
@@ -624,6 +710,18 @@ class EventListener(BaseEventListener):
|
||||
event.tool_args,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionCompletedEvent)
|
||||
def on_mcp_tool_execution_completed(
|
||||
_: Any, event: MCPToolExecutionCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_mcp_tool_execution_completed(
|
||||
event.server_name,
|
||||
event.tool_name,
|
||||
event.tool_args,
|
||||
event.result,
|
||||
event.execution_duration_ms,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionFailedEvent)
|
||||
def on_mcp_tool_execution_failed(
|
||||
_: Any, event: MCPToolExecutionFailedEvent
|
||||
|
||||
@@ -9,8 +9,6 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from crewai.cli.authentication.token import AuthError, get_auth_token
|
||||
from crewai.cli.config import Settings
|
||||
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai.cli.plus_api import PlusAPI
|
||||
from crewai.cli.version import get_crewai_version
|
||||
from crewai.events.listeners.tracing.types import TraceEvent
|
||||
@@ -18,6 +16,7 @@ from crewai.events.listeners.tracing.utils import (
|
||||
is_tracing_enabled_in_context,
|
||||
should_auto_collect_first_time_traces,
|
||||
)
|
||||
from crewai.utilities.constants import CREWAI_BASE_URL
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
@@ -327,12 +326,10 @@ class TraceBatchManager:
|
||||
if response.status_code == 200:
|
||||
access_code = response.json().get("access_code", None)
|
||||
console = Console()
|
||||
settings = Settings()
|
||||
base_url = settings.enterprise_base_url or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
return_link = (
|
||||
f"{base_url}/crewai_plus/trace_batches/{self.trace_batch_id}"
|
||||
f"{CREWAI_BASE_URL}/crewai_plus/trace_batches/{self.trace_batch_id}"
|
||||
if not self.is_current_batch_ephemeral and access_code is None
|
||||
else f"{base_url}/crewai_plus/ephemeral_trace_batches/{self.trace_batch_id}?access_code={access_code}"
|
||||
else f"{CREWAI_BASE_URL}/crewai_plus/ephemeral_trace_batches/{self.trace_batch_id}?access_code={access_code}"
|
||||
)
|
||||
|
||||
if self.is_current_batch_ephemeral:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Trace collection listener for orchestrating trace collection."""
|
||||
|
||||
import os
|
||||
from typing import Any, ClassVar, cast
|
||||
from typing import Any, ClassVar
|
||||
import uuid
|
||||
|
||||
from typing_extensions import Self
|
||||
@@ -33,6 +33,7 @@ from crewai.events.types.crew_events import (
|
||||
)
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowFailedEvent,
|
||||
FlowFinishedEvent,
|
||||
FlowPlotEvent,
|
||||
FlowStartedEvent,
|
||||
@@ -105,7 +106,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
"""Create or return singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cast(Self, cls._instance)
|
||||
return cls._instance
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -194,6 +195,22 @@ class TraceCollectionListener(BaseEventListener):
|
||||
@event_bus.on(FlowFinishedEvent)
|
||||
def on_flow_finished(source: Any, event: FlowFinishedEvent) -> None:
|
||||
self._handle_trace_event("flow_finished", source, event)
|
||||
if self.batch_manager.batch_owner_type == "flow":
|
||||
if self.first_time_handler.is_first_time:
|
||||
self.first_time_handler.mark_events_collected()
|
||||
self.first_time_handler.handle_execution_completion()
|
||||
else:
|
||||
self.batch_manager.finalize_batch()
|
||||
|
||||
@event_bus.on(FlowFailedEvent)
|
||||
def on_flow_failed(source: Any, event: FlowFailedEvent) -> None:
|
||||
self._handle_trace_event("flow_failed", source, event)
|
||||
if self.batch_manager.batch_owner_type == "flow":
|
||||
if self.first_time_handler.is_first_time:
|
||||
self.first_time_handler.mark_events_collected()
|
||||
self.first_time_handler.handle_execution_completion()
|
||||
else:
|
||||
self.batch_manager.finalize_batch()
|
||||
|
||||
@event_bus.on(FlowPlotEvent)
|
||||
def on_flow_plot(source: Any, event: FlowPlotEvent) -> None:
|
||||
@@ -319,12 +336,21 @@ class TraceCollectionListener(BaseEventListener):
|
||||
source: Any, event: MemoryQueryCompletedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("memory_query_completed", source, event)
|
||||
if self.formatter and self.memory_retrieval_in_progress:
|
||||
self.formatter.handle_memory_query_completed(
|
||||
self.formatter.current_agent_branch,
|
||||
event.source_type or "memory",
|
||||
event.query_time_ms,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@event_bus.on(MemoryQueryFailedEvent)
|
||||
def on_memory_query_failed(source: Any, event: MemoryQueryFailedEvent) -> None:
|
||||
self._handle_action_event("memory_query_failed", source, event)
|
||||
if self.formatter and self.memory_retrieval_in_progress:
|
||||
self.formatter.handle_memory_query_failed(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
event.error,
|
||||
event.source_type or "memory",
|
||||
)
|
||||
@@ -338,7 +364,10 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
self.memory_save_in_progress = True
|
||||
|
||||
self.formatter.handle_memory_save_started()
|
||||
self.formatter.handle_memory_save_started(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@event_bus.on(MemorySaveCompletedEvent)
|
||||
def on_memory_save_completed(
|
||||
@@ -352,6 +381,8 @@ class TraceCollectionListener(BaseEventListener):
|
||||
self.memory_save_in_progress = False
|
||||
|
||||
self.formatter.handle_memory_save_completed(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
event.save_time_ms,
|
||||
event.source_type or "memory",
|
||||
)
|
||||
@@ -361,8 +392,10 @@ class TraceCollectionListener(BaseEventListener):
|
||||
self._handle_action_event("memory_save_failed", source, event)
|
||||
if self.formatter and self.memory_save_in_progress:
|
||||
self.formatter.handle_memory_save_failed(
|
||||
self.formatter.current_agent_branch,
|
||||
event.error,
|
||||
event.source_type or "memory",
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@event_bus.on(MemoryRetrievalStartedEvent)
|
||||
@@ -375,7 +408,10 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
self.memory_retrieval_in_progress = True
|
||||
|
||||
self.formatter.handle_memory_retrieval_started()
|
||||
self.formatter.handle_memory_retrieval_started(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
)
|
||||
|
||||
@event_bus.on(MemoryRetrievalCompletedEvent)
|
||||
def on_memory_retrieval_completed(
|
||||
@@ -387,6 +423,8 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
self.memory_retrieval_in_progress = False
|
||||
self.formatter.handle_memory_retrieval_completed(
|
||||
self.formatter.current_agent_branch,
|
||||
self.formatter.current_crew_tree,
|
||||
event.memory_content,
|
||||
event.retrieval_time_ms,
|
||||
)
|
||||
|
||||
@@ -58,29 +58,6 @@ class MethodExecutionFailedEvent(FlowEvent):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class MethodExecutionPausedEvent(FlowEvent):
|
||||
"""Event emitted when a flow method is paused waiting for human feedback.
|
||||
|
||||
This event is emitted when a @human_feedback decorated method with an
|
||||
async provider raises HumanFeedbackPending to pause execution.
|
||||
|
||||
Attributes:
|
||||
flow_name: Name of the flow that is paused.
|
||||
method_name: Name of the method waiting for feedback.
|
||||
state: Current flow state when paused.
|
||||
flow_id: Unique identifier for this flow execution.
|
||||
message: The message shown when requesting feedback.
|
||||
emit: Optional list of possible outcomes for routing.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
state: dict[str, Any] | BaseModel
|
||||
flow_id: str
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
type: str = "method_execution_paused"
|
||||
|
||||
|
||||
class FlowFinishedEvent(FlowEvent):
|
||||
"""Event emitted when a flow completes execution"""
|
||||
|
||||
@@ -90,27 +67,14 @@ class FlowFinishedEvent(FlowEvent):
|
||||
state: dict[str, Any] | BaseModel
|
||||
|
||||
|
||||
class FlowPausedEvent(FlowEvent):
|
||||
"""Event emitted when a flow is paused waiting for human feedback.
|
||||
class FlowFailedEvent(FlowEvent):
|
||||
"""Event emitted when a flow fails execution"""
|
||||
|
||||
This event is emitted when a flow is paused due to a @human_feedback
|
||||
decorated method with an async provider raising HumanFeedbackPending.
|
||||
flow_name: str
|
||||
error: Exception
|
||||
type: str = "flow_failed"
|
||||
|
||||
Attributes:
|
||||
flow_name: Name of the flow that is paused.
|
||||
flow_id: Unique identifier for this flow execution.
|
||||
method_name: Name of the method waiting for feedback.
|
||||
state: Current flow state when paused.
|
||||
message: The message shown when requesting feedback.
|
||||
emit: Optional list of possible outcomes for routing.
|
||||
"""
|
||||
|
||||
flow_id: str
|
||||
method_name: str
|
||||
state: dict[str, Any] | BaseModel
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
type: str = "flow_paused"
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class FlowPlotEvent(FlowEvent):
|
||||
@@ -118,43 +82,3 @@ class FlowPlotEvent(FlowEvent):
|
||||
|
||||
flow_name: str
|
||||
type: str = "flow_plot"
|
||||
|
||||
|
||||
class HumanFeedbackRequestedEvent(FlowEvent):
|
||||
"""Event emitted when human feedback is requested.
|
||||
|
||||
This event is emitted when a @human_feedback decorated method
|
||||
requires input from a human reviewer.
|
||||
|
||||
Attributes:
|
||||
flow_name: Name of the flow requesting feedback.
|
||||
method_name: Name of the method decorated with @human_feedback.
|
||||
output: The method output shown to the human for review.
|
||||
message: The message displayed when requesting feedback.
|
||||
emit: Optional list of possible outcomes for routing.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
output: Any
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
type: str = "human_feedback_requested"
|
||||
|
||||
|
||||
class HumanFeedbackReceivedEvent(FlowEvent):
|
||||
"""Event emitted when human feedback is received.
|
||||
|
||||
This event is emitted after a human provides feedback in response
|
||||
to a @human_feedback decorated method.
|
||||
|
||||
Attributes:
|
||||
flow_name: Name of the flow that received feedback.
|
||||
method_name: Name of the method that received feedback.
|
||||
feedback: The raw text feedback provided by the human.
|
||||
outcome: The collapsed outcome string (if emit was specified).
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
feedback: str
|
||||
outcome: str | None = None
|
||||
type: str = "human_feedback_received"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
from crewai.experimental.crew_agent_executor_flow import CrewAgentExecutorFlow
|
||||
from crewai.experimental.evaluation import (
|
||||
AgentEvaluationResult,
|
||||
AgentEvaluator,
|
||||
@@ -24,7 +23,6 @@ __all__ = [
|
||||
"AgentEvaluationResult",
|
||||
"AgentEvaluator",
|
||||
"BaseEvaluator",
|
||||
"CrewAgentExecutorFlow",
|
||||
"EvaluationScore",
|
||||
"EvaluationTraceCallback",
|
||||
"ExperimentResult",
|
||||
|
||||
@@ -1,808 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, Field, GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||
from crewai.agents.parser import (
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
OutputParserError,
|
||||
)
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.logging_events import (
|
||||
AgentLogsExecutionEvent,
|
||||
AgentLogsStartedEvent,
|
||||
)
|
||||
from crewai.flow.flow import Flow, listen, or_, router, start
|
||||
from crewai.hooks.llm_hooks import (
|
||||
get_after_llm_call_hooks,
|
||||
get_before_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.agent_utils import (
|
||||
enforce_rpm_limit,
|
||||
format_message_for_llm,
|
||||
get_llm_response,
|
||||
handle_agent_action_core,
|
||||
handle_context_length,
|
||||
handle_max_iterations_exceeded,
|
||||
handle_output_parser_exception,
|
||||
handle_unknown_error,
|
||||
has_reached_max_iterations,
|
||||
is_context_length_exceeded,
|
||||
process_llm_response,
|
||||
)
|
||||
from crewai.utilities.constants import TRAINING_DATA_FILE
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.crew import Crew
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.task import Task
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.tools.tool_types import ToolResult
|
||||
from crewai.utilities.prompts import StandardPromptResult, SystemPromptResult
|
||||
|
||||
|
||||
class AgentReActState(BaseModel):
|
||||
"""Structured state for agent ReAct flow execution.
|
||||
|
||||
Replaces scattered instance variables with validated immutable state.
|
||||
Maps to: self.messages, self.iterations, formatted_answer in current executor.
|
||||
"""
|
||||
|
||||
messages: list[LLMMessage] = Field(default_factory=list)
|
||||
iterations: int = Field(default=0)
|
||||
current_answer: AgentAction | AgentFinish | None = Field(default=None)
|
||||
is_finished: bool = Field(default=False)
|
||||
ask_for_human_input: bool = Field(default=False)
|
||||
|
||||
|
||||
class CrewAgentExecutorFlow(Flow[AgentReActState], CrewAgentExecutorMixin):
|
||||
"""Flow-based executor matching CrewAgentExecutor interface.
|
||||
|
||||
Inherits from:
|
||||
- Flow[AgentReActState]: Provides flow orchestration capabilities
|
||||
- CrewAgentExecutorMixin: Provides memory methods (short/long/external term)
|
||||
|
||||
Note: Multiple instances may be created during agent initialization
|
||||
(cache setup, RPM controller setup, etc.) but only the final instance
|
||||
should execute tasks via invoke().
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: BaseLLM,
|
||||
task: Task,
|
||||
crew: Crew,
|
||||
agent: Agent,
|
||||
prompt: SystemPromptResult | StandardPromptResult,
|
||||
max_iter: int,
|
||||
tools: list[CrewStructuredTool],
|
||||
tools_names: str,
|
||||
stop_words: list[str],
|
||||
tools_description: str,
|
||||
tools_handler: ToolsHandler,
|
||||
step_callback: Any = None,
|
||||
original_tools: list[BaseTool] | None = None,
|
||||
function_calling_llm: BaseLLM | Any | None = None,
|
||||
respect_context_window: bool = False,
|
||||
request_within_rpm_limit: Callable[[], bool] | None = None,
|
||||
callbacks: list[Any] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
i18n: I18N | None = None,
|
||||
) -> None:
|
||||
"""Initialize the flow-based agent executor.
|
||||
|
||||
Args:
|
||||
llm: Language model instance.
|
||||
task: Task to execute.
|
||||
crew: Crew instance.
|
||||
agent: Agent to execute.
|
||||
prompt: Prompt templates.
|
||||
max_iter: Maximum iterations.
|
||||
tools: Available tools.
|
||||
tools_names: Tool names string.
|
||||
stop_words: Stop word list.
|
||||
tools_description: Tool descriptions.
|
||||
tools_handler: Tool handler instance.
|
||||
step_callback: Optional step callback.
|
||||
original_tools: Original tool list.
|
||||
function_calling_llm: Optional function calling LLM.
|
||||
respect_context_window: Respect context limits.
|
||||
request_within_rpm_limit: RPM limit check function.
|
||||
callbacks: Optional callbacks list.
|
||||
response_model: Optional Pydantic model for structured outputs.
|
||||
"""
|
||||
self._i18n: I18N = i18n or get_i18n()
|
||||
self.llm = llm
|
||||
self.task = task
|
||||
self.agent = agent
|
||||
self.crew = crew
|
||||
self.prompt = prompt
|
||||
self.tools = tools
|
||||
self.tools_names = tools_names
|
||||
self.stop = stop_words
|
||||
self.max_iter = max_iter
|
||||
self.callbacks = callbacks or []
|
||||
self._printer: Printer = Printer()
|
||||
self.tools_handler = tools_handler
|
||||
self.original_tools = original_tools or []
|
||||
self.step_callback = step_callback
|
||||
self.tools_description = tools_description
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.respect_context_window = respect_context_window
|
||||
self.request_within_rpm_limit = request_within_rpm_limit
|
||||
self.response_model = response_model
|
||||
self.log_error_after = 3
|
||||
self._console: Console = Console()
|
||||
|
||||
# Error context storage for recovery
|
||||
self._last_parser_error: OutputParserError | None = None
|
||||
self._last_context_error: Exception | None = None
|
||||
|
||||
# Execution guard to prevent concurrent/duplicate executions
|
||||
self._execution_lock = threading.Lock()
|
||||
self._is_executing: bool = False
|
||||
self._has_been_invoked: bool = False
|
||||
self._flow_initialized: bool = False
|
||||
|
||||
self._instance_id = str(uuid4())[:8]
|
||||
|
||||
self.before_llm_call_hooks: list[Callable] = []
|
||||
self.after_llm_call_hooks: list[Callable] = []
|
||||
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
|
||||
self.after_llm_call_hooks.extend(get_after_llm_call_hooks())
|
||||
|
||||
if self.llm:
|
||||
existing_stop = getattr(self.llm, "stop", [])
|
||||
self.llm.stop = list(
|
||||
set(
|
||||
existing_stop + self.stop
|
||||
if isinstance(existing_stop, list)
|
||||
else self.stop
|
||||
)
|
||||
)
|
||||
|
||||
self._state = AgentReActState()
|
||||
|
||||
def _ensure_flow_initialized(self) -> None:
|
||||
"""Ensure Flow.__init__() has been called.
|
||||
|
||||
This is deferred from __init__ to prevent FlowCreatedEvent emission
|
||||
during agent setup when multiple executor instances are created.
|
||||
Only the instance that actually executes via invoke() will emit events.
|
||||
"""
|
||||
if not self._flow_initialized:
|
||||
# Now call Flow's __init__ which will replace self._state
|
||||
# with Flow's managed state. Suppress flow events since this is
|
||||
# an agent executor, not a user-facing flow.
|
||||
super().__init__(
|
||||
suppress_flow_events=True,
|
||||
)
|
||||
self._flow_initialized = True
|
||||
|
||||
@property
|
||||
def use_stop_words(self) -> bool:
|
||||
"""Check to determine if stop words are being used.
|
||||
|
||||
Returns:
|
||||
bool: True if stop words should be used.
|
||||
"""
|
||||
return self.llm.supports_stop_words() if self.llm else False
|
||||
|
||||
@property
|
||||
def state(self) -> AgentReActState:
|
||||
"""Get state - returns temporary state if Flow not yet initialized.
|
||||
|
||||
Flow initialization is deferred to prevent event emission during agent setup.
|
||||
Returns the temporary state until invoke() is called.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def messages(self) -> list[LLMMessage]:
|
||||
"""Compatibility property for mixin - returns state messages."""
|
||||
return self._state.messages
|
||||
|
||||
@property
|
||||
def iterations(self) -> int:
|
||||
"""Compatibility property for mixin - returns state iterations."""
|
||||
return self._state.iterations
|
||||
|
||||
@start()
|
||||
def initialize_reasoning(self) -> Literal["initialized"]:
|
||||
"""Initialize the reasoning flow and emit agent start logs."""
|
||||
self._show_start_logs()
|
||||
return "initialized"
|
||||
|
||||
@listen("force_final_answer")
|
||||
def force_final_answer(self) -> Literal["agent_finished"]:
|
||||
"""Force agent to provide final answer when max iterations exceeded."""
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer=None,
|
||||
printer=self._printer,
|
||||
i18n=self._i18n,
|
||||
messages=list(self.state.messages),
|
||||
llm=self.llm,
|
||||
callbacks=self.callbacks,
|
||||
)
|
||||
|
||||
self.state.current_answer = formatted_answer
|
||||
self.state.is_finished = True
|
||||
|
||||
return "agent_finished"
|
||||
|
||||
@listen("continue_reasoning")
|
||||
def call_llm_and_parse(self) -> Literal["parsed", "parser_error", "context_error"]:
|
||||
"""Execute LLM call with hooks and parse the response.
|
||||
|
||||
Returns routing decision based on parsing result.
|
||||
"""
|
||||
try:
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
|
||||
answer = get_llm_response(
|
||||
llm=self.llm,
|
||||
messages=list(self.state.messages),
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
response_model=self.response_model,
|
||||
executor_context=self,
|
||||
)
|
||||
|
||||
# Parse the LLM response
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words)
|
||||
self.state.current_answer = formatted_answer
|
||||
|
||||
if "Final Answer:" in answer and isinstance(formatted_answer, AgentAction):
|
||||
warning_text = Text()
|
||||
warning_text.append("⚠️ ", style="yellow bold")
|
||||
warning_text.append(
|
||||
f"LLM returned 'Final Answer:' but parsed as AgentAction (tool: {formatted_answer.tool})",
|
||||
style="yellow",
|
||||
)
|
||||
self._console.print(warning_text)
|
||||
preview_text = Text()
|
||||
preview_text.append("Answer preview: ", style="yellow")
|
||||
preview_text.append(f"{answer[:200]}...", style="yellow dim")
|
||||
self._console.print(preview_text)
|
||||
|
||||
return "parsed"
|
||||
|
||||
except OutputParserError as e:
|
||||
# Store error context for recovery
|
||||
self._last_parser_error = e or OutputParserError(
|
||||
error="Unknown parser error"
|
||||
)
|
||||
return "parser_error"
|
||||
|
||||
except Exception as e:
|
||||
if is_context_length_exceeded(e):
|
||||
self._last_context_error = e
|
||||
return "context_error"
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
raise e
|
||||
handle_unknown_error(self._printer, e)
|
||||
raise
|
||||
|
||||
@router(call_llm_and_parse)
|
||||
def route_by_answer_type(self) -> Literal["execute_tool", "agent_finished"]:
|
||||
"""Route based on whether answer is AgentAction or AgentFinish."""
|
||||
if isinstance(self.state.current_answer, AgentAction):
|
||||
return "execute_tool"
|
||||
return "agent_finished"
|
||||
|
||||
@listen("execute_tool")
|
||||
def execute_tool_action(self) -> Literal["tool_completed", "tool_result_is_final"]:
|
||||
"""Execute the tool action and handle the result."""
|
||||
try:
|
||||
action = cast(AgentAction, self.state.current_answer)
|
||||
|
||||
# Extract fingerprint context for tool execution
|
||||
fingerprint_context = {}
|
||||
if (
|
||||
self.agent
|
||||
and hasattr(self.agent, "security_config")
|
||||
and hasattr(self.agent.security_config, "fingerprint")
|
||||
):
|
||||
fingerprint_context = {
|
||||
"agent_fingerprint": str(self.agent.security_config.fingerprint)
|
||||
}
|
||||
|
||||
# Execute the tool
|
||||
tool_result = execute_tool_and_check_finality(
|
||||
agent_action=action,
|
||||
fingerprint_context=fingerprint_context,
|
||||
tools=self.tools,
|
||||
i18n=self._i18n,
|
||||
agent_key=self.agent.key if self.agent else None,
|
||||
agent_role=self.agent.role if self.agent else None,
|
||||
tools_handler=self.tools_handler,
|
||||
task=self.task,
|
||||
agent=self.agent,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
crew=self.crew,
|
||||
)
|
||||
|
||||
# Handle agent action and append observation to messages
|
||||
result = self._handle_agent_action(action, tool_result)
|
||||
self.state.current_answer = result
|
||||
|
||||
# Invoke step callback if configured
|
||||
self._invoke_step_callback(result)
|
||||
|
||||
# Append result message to conversation state
|
||||
if hasattr(result, "text"):
|
||||
self._append_message_to_state(result.text)
|
||||
|
||||
# Check if tool result became a final answer (result_as_answer flag)
|
||||
if isinstance(result, AgentFinish):
|
||||
self.state.is_finished = True
|
||||
return "tool_result_is_final"
|
||||
|
||||
return "tool_completed"
|
||||
|
||||
except Exception as e:
|
||||
error_text = Text()
|
||||
error_text.append("❌ Error in tool execution: ", style="red bold")
|
||||
error_text.append(str(e), style="red")
|
||||
self._console.print(error_text)
|
||||
raise
|
||||
|
||||
@listen("initialized")
|
||||
def continue_iteration(self) -> Literal["check_iteration"]:
|
||||
"""Bridge listener that connects iteration loop back to iteration check."""
|
||||
return "check_iteration"
|
||||
|
||||
@router(or_(initialize_reasoning, continue_iteration))
|
||||
def check_max_iterations(
|
||||
self,
|
||||
) -> Literal["force_final_answer", "continue_reasoning"]:
|
||||
"""Check if max iterations reached before proceeding with reasoning."""
|
||||
if has_reached_max_iterations(self.state.iterations, self.max_iter):
|
||||
return "force_final_answer"
|
||||
return "continue_reasoning"
|
||||
|
||||
@router(execute_tool_action)
|
||||
def increment_and_continue(self) -> Literal["initialized"]:
|
||||
"""Increment iteration counter and loop back for next iteration."""
|
||||
self.state.iterations += 1
|
||||
return "initialized"
|
||||
|
||||
@listen(or_("agent_finished", "tool_result_is_final"))
|
||||
def finalize(self) -> Literal["completed", "skipped"]:
|
||||
"""Finalize execution and emit completion logs."""
|
||||
if self.state.current_answer is None:
|
||||
skip_text = Text()
|
||||
skip_text.append("⚠️ ", style="yellow bold")
|
||||
skip_text.append(
|
||||
"Finalize called but no answer in state - skipping", style="yellow"
|
||||
)
|
||||
self._console.print(skip_text)
|
||||
return "skipped"
|
||||
|
||||
if not isinstance(self.state.current_answer, AgentFinish):
|
||||
skip_text = Text()
|
||||
skip_text.append("⚠️ ", style="yellow bold")
|
||||
skip_text.append(
|
||||
f"Finalize called with {type(self.state.current_answer).__name__} instead of AgentFinish - skipping",
|
||||
style="yellow",
|
||||
)
|
||||
self._console.print(skip_text)
|
||||
return "skipped"
|
||||
|
||||
self.state.is_finished = True
|
||||
|
||||
self._show_logs(self.state.current_answer)
|
||||
|
||||
return "completed"
|
||||
|
||||
@listen("parser_error")
|
||||
def recover_from_parser_error(self) -> Literal["initialized"]:
|
||||
"""Recover from output parser errors and retry."""
|
||||
formatted_answer = handle_output_parser_exception(
|
||||
e=self._last_parser_error,
|
||||
messages=list(self.state.messages),
|
||||
iterations=self.state.iterations,
|
||||
log_error_after=self.log_error_after,
|
||||
printer=self._printer,
|
||||
)
|
||||
|
||||
if formatted_answer:
|
||||
self.state.current_answer = formatted_answer
|
||||
|
||||
self.state.iterations += 1
|
||||
|
||||
return "initialized"
|
||||
|
||||
@listen("context_error")
|
||||
def recover_from_context_length(self) -> Literal["initialized"]:
|
||||
"""Recover from context length errors and retry."""
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
messages=self.state.messages,
|
||||
llm=self.llm,
|
||||
callbacks=self.callbacks,
|
||||
i18n=self._i18n,
|
||||
)
|
||||
|
||||
self.state.iterations += 1
|
||||
|
||||
return "initialized"
|
||||
|
||||
def invoke(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Execute agent with given inputs.
|
||||
|
||||
Args:
|
||||
inputs: Input dictionary containing prompt variables.
|
||||
|
||||
Returns:
|
||||
Dictionary with agent output.
|
||||
"""
|
||||
self._ensure_flow_initialized()
|
||||
|
||||
with self._execution_lock:
|
||||
if self._is_executing:
|
||||
raise RuntimeError(
|
||||
"Executor is already running. "
|
||||
"Cannot invoke the same executor instance concurrently."
|
||||
)
|
||||
self._is_executing = True
|
||||
self._has_been_invoked = True
|
||||
|
||||
try:
|
||||
# Reset state for fresh execution
|
||||
self.state.messages.clear()
|
||||
self.state.iterations = 0
|
||||
self.state.current_answer = None
|
||||
self.state.is_finished = False
|
||||
|
||||
if "system" in self.prompt:
|
||||
prompt = cast("SystemPromptResult", self.prompt)
|
||||
system_prompt = self._format_prompt(prompt["system"], inputs)
|
||||
user_prompt = self._format_prompt(prompt["user"], inputs)
|
||||
self.state.messages.append(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
self.state.messages.append(format_message_for_llm(user_prompt))
|
||||
else:
|
||||
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
|
||||
self.state.messages.append(format_message_for_llm(user_prompt))
|
||||
|
||||
self.state.ask_for_human_input = bool(
|
||||
inputs.get("ask_for_human_input", False)
|
||||
)
|
||||
|
||||
self.kickoff()
|
||||
|
||||
formatted_answer = self.state.current_answer
|
||||
|
||||
if not isinstance(formatted_answer, AgentFinish):
|
||||
raise RuntimeError(
|
||||
"Agent execution ended without reaching a final answer."
|
||||
)
|
||||
|
||||
if self.state.ask_for_human_input:
|
||||
formatted_answer = self._handle_human_feedback(formatted_answer)
|
||||
|
||||
self._create_short_term_memory(formatted_answer)
|
||||
self._create_long_term_memory(formatted_answer)
|
||||
self._create_external_memory(formatted_answer)
|
||||
|
||||
return {"output": formatted_answer.output}
|
||||
|
||||
except AssertionError:
|
||||
fail_text = Text()
|
||||
fail_text.append("❌ ", style="red bold")
|
||||
fail_text.append(
|
||||
"Agent failed to reach a final answer. This is likely a bug - please report it.",
|
||||
style="red",
|
||||
)
|
||||
self._console.print(fail_text)
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_unknown_error(self._printer, e)
|
||||
raise
|
||||
finally:
|
||||
self._is_executing = False
|
||||
|
||||
def _handle_agent_action(
|
||||
self, formatted_answer: AgentAction, tool_result: ToolResult
|
||||
) -> AgentAction | AgentFinish:
|
||||
"""Process agent action and tool execution result.
|
||||
|
||||
Args:
|
||||
formatted_answer: Agent's action to execute.
|
||||
tool_result: Result from tool execution.
|
||||
|
||||
Returns:
|
||||
Updated action or final answer.
|
||||
"""
|
||||
add_image_tool = self._i18n.tools("add_image")
|
||||
if (
|
||||
isinstance(add_image_tool, dict)
|
||||
and formatted_answer.tool.casefold().strip()
|
||||
== add_image_tool.get("name", "").casefold().strip()
|
||||
):
|
||||
self.state.messages.append(
|
||||
{"role": "assistant", "content": tool_result.result}
|
||||
)
|
||||
return formatted_answer
|
||||
|
||||
return handle_agent_action_core(
|
||||
formatted_answer=formatted_answer,
|
||||
tool_result=tool_result,
|
||||
messages=self.state.messages,
|
||||
step_callback=self.step_callback,
|
||||
show_logs=self._show_logs,
|
||||
)
|
||||
|
||||
def _invoke_step_callback(
|
||||
self, formatted_answer: AgentAction | AgentFinish
|
||||
) -> None:
|
||||
"""Invoke step callback if configured.
|
||||
|
||||
Args:
|
||||
formatted_answer: Current agent response.
|
||||
"""
|
||||
if self.step_callback:
|
||||
self.step_callback(formatted_answer)
|
||||
|
||||
def _append_message_to_state(
|
||||
self, text: str, role: Literal["user", "assistant", "system"] = "assistant"
|
||||
) -> None:
|
||||
"""Add message to state conversation history.
|
||||
|
||||
Args:
|
||||
text: Message content.
|
||||
role: Message role (default: assistant).
|
||||
"""
|
||||
self.state.messages.append(format_message_for_llm(text, role=role))
|
||||
|
||||
def _show_start_logs(self) -> None:
|
||||
"""Emit agent start event."""
|
||||
if self.agent is None:
|
||||
raise ValueError("Agent cannot be None")
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self.agent,
|
||||
AgentLogsStartedEvent(
|
||||
agent_role=self.agent.role,
|
||||
task_description=(self.task.description if self.task else "Not Found"),
|
||||
verbose=self.agent.verbose
|
||||
or (hasattr(self, "crew") and getattr(self.crew, "verbose", False)),
|
||||
),
|
||||
)
|
||||
|
||||
def _show_logs(self, formatted_answer: AgentAction | AgentFinish) -> None:
|
||||
"""Emit agent execution event.
|
||||
|
||||
Args:
|
||||
formatted_answer: Agent's response to log.
|
||||
"""
|
||||
if self.agent is None:
|
||||
raise ValueError("Agent cannot be None")
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self.agent,
|
||||
AgentLogsExecutionEvent(
|
||||
agent_role=self.agent.role,
|
||||
formatted_answer=formatted_answer,
|
||||
verbose=self.agent.verbose
|
||||
or (hasattr(self, "crew") and getattr(self.crew, "verbose", False)),
|
||||
),
|
||||
)
|
||||
|
||||
def _handle_crew_training_output(
|
||||
self, result: AgentFinish, human_feedback: str | None = None
|
||||
) -> None:
|
||||
"""Save training data for crew training mode.
|
||||
|
||||
Args:
|
||||
result: Agent's final output.
|
||||
human_feedback: Optional feedback from human.
|
||||
"""
|
||||
agent_id = str(self.agent.id)
|
||||
train_iteration = (
|
||||
getattr(self.crew, "_train_iteration", None) if self.crew else None
|
||||
)
|
||||
|
||||
if train_iteration is None or not isinstance(train_iteration, int):
|
||||
train_error = Text()
|
||||
train_error.append("❌ ", style="red bold")
|
||||
train_error.append(
|
||||
"Invalid or missing train iteration. Cannot save training data.",
|
||||
style="red",
|
||||
)
|
||||
self._console.print(train_error)
|
||||
return
|
||||
|
||||
training_handler = CrewTrainingHandler(TRAINING_DATA_FILE)
|
||||
training_data = training_handler.load() or {}
|
||||
|
||||
# Initialize or retrieve agent's training data
|
||||
agent_training_data = training_data.get(agent_id, {})
|
||||
|
||||
if human_feedback is not None:
|
||||
# Save initial output and human feedback
|
||||
agent_training_data[train_iteration] = {
|
||||
"initial_output": result.output,
|
||||
"human_feedback": human_feedback,
|
||||
}
|
||||
else:
|
||||
# Save improved output
|
||||
if train_iteration in agent_training_data:
|
||||
agent_training_data[train_iteration]["improved_output"] = result.output
|
||||
else:
|
||||
train_error = Text()
|
||||
train_error.append("❌ ", style="red bold")
|
||||
train_error.append(
|
||||
f"No existing training data for agent {agent_id} and iteration "
|
||||
f"{train_iteration}. Cannot save improved output.",
|
||||
style="red",
|
||||
)
|
||||
self._console.print(train_error)
|
||||
return
|
||||
|
||||
# Update the training data and save
|
||||
training_data[agent_id] = agent_training_data
|
||||
training_handler.save(training_data)
|
||||
|
||||
@staticmethod
|
||||
def _format_prompt(prompt: str, inputs: dict[str, str]) -> str:
|
||||
"""Format prompt template with input values.
|
||||
|
||||
Args:
|
||||
prompt: Template string.
|
||||
inputs: Values to substitute.
|
||||
|
||||
Returns:
|
||||
Formatted prompt.
|
||||
"""
|
||||
prompt = prompt.replace("{input}", inputs["input"])
|
||||
prompt = prompt.replace("{tool_names}", inputs["tool_names"])
|
||||
return prompt.replace("{tools}", inputs["tools"])
|
||||
|
||||
def _handle_human_feedback(self, formatted_answer: AgentFinish) -> AgentFinish:
|
||||
"""Process human feedback and refine answer.
|
||||
|
||||
Args:
|
||||
formatted_answer: Initial agent result.
|
||||
|
||||
Returns:
|
||||
Final answer after feedback.
|
||||
"""
|
||||
human_feedback = self._ask_human_input(formatted_answer.output)
|
||||
|
||||
if self._is_training_mode():
|
||||
return self._handle_training_feedback(formatted_answer, human_feedback)
|
||||
|
||||
return self._handle_regular_feedback(formatted_answer, human_feedback)
|
||||
|
||||
def _is_training_mode(self) -> bool:
|
||||
"""Check if training mode is active.
|
||||
|
||||
Returns:
|
||||
True if in training mode.
|
||||
"""
|
||||
return bool(self.crew and self.crew._train)
|
||||
|
||||
def _handle_training_feedback(
|
||||
self, initial_answer: AgentFinish, feedback: str
|
||||
) -> AgentFinish:
|
||||
"""Process training feedback and generate improved answer.
|
||||
|
||||
Args:
|
||||
initial_answer: Initial agent output.
|
||||
feedback: Training feedback.
|
||||
|
||||
Returns:
|
||||
Improved answer.
|
||||
"""
|
||||
self._handle_crew_training_output(initial_answer, feedback)
|
||||
self.state.messages.append(
|
||||
format_message_for_llm(
|
||||
self._i18n.slice("feedback_instructions").format(feedback=feedback)
|
||||
)
|
||||
)
|
||||
|
||||
# Re-run flow for improved answer
|
||||
self.state.iterations = 0
|
||||
self.state.is_finished = False
|
||||
self.state.current_answer = None
|
||||
|
||||
self.kickoff()
|
||||
|
||||
# Get improved answer from state
|
||||
improved_answer = self.state.current_answer
|
||||
if not isinstance(improved_answer, AgentFinish):
|
||||
raise RuntimeError(
|
||||
"Training feedback iteration did not produce final answer"
|
||||
)
|
||||
|
||||
self._handle_crew_training_output(improved_answer)
|
||||
self.state.ask_for_human_input = False
|
||||
return improved_answer
|
||||
|
||||
def _handle_regular_feedback(
|
||||
self, current_answer: AgentFinish, initial_feedback: str
|
||||
) -> AgentFinish:
|
||||
"""Process regular feedback iteratively until user is satisfied.
|
||||
|
||||
Args:
|
||||
current_answer: Current agent output.
|
||||
initial_feedback: Initial user feedback.
|
||||
|
||||
Returns:
|
||||
Final answer after iterations.
|
||||
"""
|
||||
feedback = initial_feedback
|
||||
answer = current_answer
|
||||
|
||||
while self.state.ask_for_human_input:
|
||||
if feedback.strip() == "":
|
||||
self.state.ask_for_human_input = False
|
||||
else:
|
||||
answer = self._process_feedback_iteration(feedback)
|
||||
feedback = self._ask_human_input(answer.output)
|
||||
|
||||
return answer
|
||||
|
||||
def _process_feedback_iteration(self, feedback: str) -> AgentFinish:
|
||||
"""Process a single feedback iteration and generate updated response.
|
||||
|
||||
Args:
|
||||
feedback: User feedback.
|
||||
|
||||
Returns:
|
||||
Updated agent response.
|
||||
"""
|
||||
self.state.messages.append(
|
||||
format_message_for_llm(
|
||||
self._i18n.slice("feedback_instructions").format(feedback=feedback)
|
||||
)
|
||||
)
|
||||
|
||||
# Re-run flow
|
||||
self.state.iterations = 0
|
||||
self.state.is_finished = False
|
||||
self.state.current_answer = None
|
||||
|
||||
self.kickoff()
|
||||
|
||||
# Get answer from state
|
||||
answer = self.state.current_answer
|
||||
if not isinstance(answer, AgentFinish):
|
||||
raise RuntimeError("Feedback iteration did not produce final answer")
|
||||
|
||||
return answer
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, _source_type: Any, _handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
"""Generate Pydantic core schema for Protocol compatibility.
|
||||
|
||||
Allows the executor to be used in Pydantic models without
|
||||
requiring arbitrary_types_allowed=True.
|
||||
"""
|
||||
return core_schema.any_schema()
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.agent_events import (
|
||||
@@ -29,10 +28,6 @@ from crewai.experimental.evaluation.evaluation_listener import (
|
||||
from crewai.task import Task
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
class ExecutionState:
|
||||
current_agent_id: str | None = None
|
||||
current_task_id: str | None = None
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import enum
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.llm import BaseLLM
|
||||
from crewai.task import Task
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
class MetricCategory(enum.Enum):
|
||||
GOAL_ALIGNMENT = "goal_alignment"
|
||||
SEMANTIC_QUALITY = "semantic_quality"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from hashlib import md5
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from crewai import Agent, Crew
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.experimental.evaluation import AgentEvaluator, create_default_evaluator
|
||||
from crewai.experimental.evaluation.evaluation_display import (
|
||||
@@ -18,11 +17,6 @@ from crewai.experimental.evaluation.experiment.result_display import (
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.crew import Crew
|
||||
|
||||
|
||||
class ExperimentRunner:
|
||||
def __init__(self, dataset: list[dict[str, Any]]):
|
||||
self.dataset = dataset or []
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.experimental.evaluation.base_evaluator import (
|
||||
BaseEvaluator,
|
||||
@@ -13,10 +12,6 @@ from crewai.task import Task
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
class GoalAlignmentEvaluator(BaseEvaluator):
|
||||
@property
|
||||
def metric_category(self) -> MetricCategory:
|
||||
|
||||
@@ -6,16 +6,15 @@ This module provides evaluator implementations for:
|
||||
- Thinking-to-action ratio
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from enum import Enum
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.experimental.evaluation.base_evaluator import (
|
||||
BaseEvaluator,
|
||||
@@ -28,10 +27,6 @@ from crewai.tasks.task_output import TaskOutput
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
class ReasoningPatternType(Enum):
|
||||
EFFICIENT = "efficient" # Good reasoning flow
|
||||
LOOP = "loop" # Agent is stuck in a loop
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.experimental.evaluation.base_evaluator import (
|
||||
BaseEvaluator,
|
||||
@@ -13,10 +12,6 @@ from crewai.task import Task
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
class SemanticQualityEvaluator(BaseEvaluator):
|
||||
@property
|
||||
def metric_category(self) -> MetricCategory:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.experimental.evaluation.base_evaluator import (
|
||||
BaseEvaluator,
|
||||
@@ -14,10 +13,6 @@ from crewai.task import Task
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
class ToolSelectionEvaluator(BaseEvaluator):
|
||||
@property
|
||||
def metric_category(self) -> MetricCategory:
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
from crewai.flow.async_feedback import (
|
||||
ConsoleProvider,
|
||||
HumanFeedbackPending,
|
||||
HumanFeedbackProvider,
|
||||
PendingFeedbackContext,
|
||||
)
|
||||
from crewai.flow.flow import Flow, and_, listen, or_, router, start
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult, human_feedback
|
||||
from crewai.flow.persistence import persist
|
||||
from crewai.flow.visualization import (
|
||||
FlowStructure,
|
||||
@@ -15,16 +8,10 @@ from crewai.flow.visualization import (
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ConsoleProvider",
|
||||
"Flow",
|
||||
"FlowStructure",
|
||||
"HumanFeedbackPending",
|
||||
"HumanFeedbackProvider",
|
||||
"HumanFeedbackResult",
|
||||
"PendingFeedbackContext",
|
||||
"and_",
|
||||
"build_flow_structure",
|
||||
"human_feedback",
|
||||
"listen",
|
||||
"or_",
|
||||
"persist",
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Async human feedback support for CrewAI Flows.
|
||||
|
||||
This module provides abstractions for non-blocking human-in-the-loop workflows,
|
||||
allowing integration with external systems like Slack, Teams, webhooks, or APIs.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from crewai.flow import Flow, start, human_feedback
|
||||
from crewai.flow.async_feedback import HumanFeedbackProvider, HumanFeedbackPending
|
||||
|
||||
class SlackProvider(HumanFeedbackProvider):
|
||||
def request_feedback(self, context, flow):
|
||||
self.send_slack_notification(context)
|
||||
raise HumanFeedbackPending(context=context)
|
||||
|
||||
class MyFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
provider=SlackProvider(),
|
||||
)
|
||||
def review(self):
|
||||
return "Content to review"
|
||||
```
|
||||
"""
|
||||
|
||||
from crewai.flow.async_feedback.types import (
|
||||
HumanFeedbackPending,
|
||||
HumanFeedbackProvider,
|
||||
PendingFeedbackContext,
|
||||
)
|
||||
from crewai.flow.async_feedback.providers import ConsoleProvider
|
||||
|
||||
__all__ = [
|
||||
"ConsoleProvider",
|
||||
"HumanFeedbackPending",
|
||||
"HumanFeedbackProvider",
|
||||
"PendingFeedbackContext",
|
||||
]
|
||||
@@ -1,124 +0,0 @@
|
||||
"""Default provider implementations for human feedback.
|
||||
|
||||
This module provides the ConsoleProvider, which is the default synchronous
|
||||
provider that collects feedback via console input.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.flow import Flow
|
||||
|
||||
|
||||
class ConsoleProvider:
|
||||
"""Default synchronous console-based feedback provider.
|
||||
|
||||
This provider blocks execution and waits for console input from the user.
|
||||
It displays the method output with formatting and prompts for feedback.
|
||||
|
||||
This is the default provider used when no custom provider is specified
|
||||
in the @human_feedback decorator.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from crewai.flow.async_feedback import ConsoleProvider
|
||||
|
||||
# Explicitly use console provider
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
provider=ConsoleProvider(),
|
||||
)
|
||||
def my_method(self):
|
||||
return "Content to review"
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = True):
|
||||
"""Initialize the console provider.
|
||||
|
||||
Args:
|
||||
verbose: Whether to display formatted output. If False, only
|
||||
shows the prompt message.
|
||||
"""
|
||||
self.verbose = verbose
|
||||
|
||||
def request_feedback(
|
||||
self,
|
||||
context: PendingFeedbackContext,
|
||||
flow: Flow,
|
||||
) -> str:
|
||||
"""Request feedback via console input (blocking).
|
||||
|
||||
Displays the method output with formatting and waits for the user
|
||||
to type their feedback. Press Enter to skip (returns empty string).
|
||||
|
||||
Args:
|
||||
context: The pending feedback context with output and message.
|
||||
flow: The Flow instance (used for event emission).
|
||||
|
||||
Returns:
|
||||
The user's feedback as a string, or empty string if skipped.
|
||||
"""
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.event_listener import event_listener
|
||||
from crewai.events.types.flow_events import (
|
||||
HumanFeedbackReceivedEvent,
|
||||
HumanFeedbackRequestedEvent,
|
||||
)
|
||||
|
||||
# Emit feedback requested event
|
||||
crewai_event_bus.emit(
|
||||
flow,
|
||||
HumanFeedbackRequestedEvent(
|
||||
type="human_feedback_requested",
|
||||
flow_name=flow.name or flow.__class__.__name__,
|
||||
method_name=context.method_name,
|
||||
output=context.method_output,
|
||||
message=context.message,
|
||||
emit=context.emit,
|
||||
),
|
||||
)
|
||||
|
||||
# Pause live updates during human input
|
||||
formatter = event_listener.formatter
|
||||
formatter.pause_live_updates()
|
||||
|
||||
try:
|
||||
console = formatter.console
|
||||
|
||||
if self.verbose:
|
||||
# Display output with formatting using Rich console
|
||||
console.print("\n" + "═" * 50, style="bold cyan")
|
||||
console.print(" OUTPUT FOR REVIEW", style="bold cyan")
|
||||
console.print("═" * 50 + "\n", style="bold cyan")
|
||||
console.print(context.method_output)
|
||||
console.print("\n" + "═" * 50 + "\n", style="bold cyan")
|
||||
|
||||
# Show message and prompt for feedback
|
||||
console.print(context.message, style="yellow")
|
||||
console.print(
|
||||
"(Press Enter to skip, or type your feedback)\n", style="cyan"
|
||||
)
|
||||
|
||||
feedback = input("Your feedback: ").strip()
|
||||
|
||||
# Emit feedback received event
|
||||
crewai_event_bus.emit(
|
||||
flow,
|
||||
HumanFeedbackReceivedEvent(
|
||||
type="human_feedback_received",
|
||||
flow_name=flow.name or flow.__class__.__name__,
|
||||
method_name=context.method_name,
|
||||
feedback=feedback,
|
||||
outcome=None, # Will be determined after collapsing
|
||||
),
|
||||
)
|
||||
|
||||
return feedback
|
||||
finally:
|
||||
# Resume live updates
|
||||
formatter.resume_live_updates()
|
||||
@@ -1,264 +0,0 @@
|
||||
"""Core types for async human feedback in Flows.
|
||||
|
||||
This module defines the protocol, exception, and context types used for
|
||||
non-blocking human-in-the-loop workflows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.flow import Flow
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingFeedbackContext:
|
||||
"""Context capturing everything needed to resume a paused flow.
|
||||
|
||||
When a flow is paused waiting for async human feedback, this dataclass
|
||||
stores all the information needed to:
|
||||
1. Identify which flow execution is waiting
|
||||
2. What method triggered the feedback request
|
||||
3. What was shown to the human
|
||||
4. How to route the response when it arrives
|
||||
|
||||
Attributes:
|
||||
flow_id: Unique identifier for the flow instance (from state.id)
|
||||
flow_class: Fully qualified class name (e.g., "myapp.flows.ReviewFlow")
|
||||
method_name: Name of the method that triggered feedback request
|
||||
method_output: The output that was shown to the human for review
|
||||
message: The message displayed when requesting feedback
|
||||
emit: Optional list of outcome strings for routing
|
||||
default_outcome: Outcome to use when no feedback is provided
|
||||
metadata: Optional metadata for external system integration
|
||||
llm: LLM model string for outcome collapsing
|
||||
requested_at: When the feedback was requested
|
||||
|
||||
Example:
|
||||
```python
|
||||
context = PendingFeedbackContext(
|
||||
flow_id="abc-123",
|
||||
flow_class="myapp.ReviewFlow",
|
||||
method_name="review_content",
|
||||
method_output={"title": "Draft", "body": "..."},
|
||||
message="Please review and approve or reject:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
flow_id: str
|
||||
flow_class: str
|
||||
method_name: str
|
||||
method_output: Any
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
default_outcome: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
llm: str | None = None
|
||||
requested_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Serialize context to a dictionary for persistence.
|
||||
|
||||
Returns:
|
||||
Dictionary representation suitable for JSON serialization.
|
||||
"""
|
||||
return {
|
||||
"flow_id": self.flow_id,
|
||||
"flow_class": self.flow_class,
|
||||
"method_name": self.method_name,
|
||||
"method_output": self.method_output,
|
||||
"message": self.message,
|
||||
"emit": self.emit,
|
||||
"default_outcome": self.default_outcome,
|
||||
"metadata": self.metadata,
|
||||
"llm": self.llm,
|
||||
"requested_at": self.requested_at.isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> PendingFeedbackContext:
|
||||
"""Deserialize context from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary representation of the context.
|
||||
|
||||
Returns:
|
||||
Reconstructed PendingFeedbackContext instance.
|
||||
"""
|
||||
requested_at = data.get("requested_at")
|
||||
if isinstance(requested_at, str):
|
||||
requested_at = datetime.fromisoformat(requested_at)
|
||||
elif requested_at is None:
|
||||
requested_at = datetime.now()
|
||||
|
||||
return cls(
|
||||
flow_id=data["flow_id"],
|
||||
flow_class=data["flow_class"],
|
||||
method_name=data["method_name"],
|
||||
method_output=data.get("method_output"),
|
||||
message=data.get("message", ""),
|
||||
emit=data.get("emit"),
|
||||
default_outcome=data.get("default_outcome"),
|
||||
metadata=data.get("metadata", {}),
|
||||
llm=data.get("llm"),
|
||||
requested_at=requested_at,
|
||||
)
|
||||
|
||||
|
||||
class HumanFeedbackPending(Exception): # noqa: N818 - Not an error, a control flow signal
|
||||
"""Signal that flow execution should pause for async human feedback.
|
||||
|
||||
When raised by a provider, the flow framework will:
|
||||
1. Stop execution at the current method
|
||||
2. Automatically persist state and context (if persistence is configured)
|
||||
3. Return this object to the caller (not re-raise it)
|
||||
|
||||
The caller receives this as a return value from `flow.kickoff()`, enabling
|
||||
graceful handling of the paused state without try/except blocks:
|
||||
|
||||
```python
|
||||
result = flow.kickoff()
|
||||
if isinstance(result, HumanFeedbackPending):
|
||||
# Flow is paused, handle async feedback
|
||||
print(f"Waiting for feedback: {result.context.flow_id}")
|
||||
else:
|
||||
# Normal completion
|
||||
print(f"Flow completed: {result}")
|
||||
```
|
||||
|
||||
Note:
|
||||
The flow framework automatically saves pending feedback when this
|
||||
exception is raised. Providers do NOT need to call `save_pending_feedback`
|
||||
manually - just raise this exception and the framework handles persistence.
|
||||
|
||||
Attributes:
|
||||
context: The PendingFeedbackContext with all details needed to resume
|
||||
callback_info: Optional dict with information for external systems
|
||||
(e.g., webhook URL, ticket ID, Slack thread ID)
|
||||
|
||||
Example:
|
||||
```python
|
||||
class SlackProvider(HumanFeedbackProvider):
|
||||
def request_feedback(self, context, flow):
|
||||
# Send notification to external system
|
||||
ticket_id = self.create_slack_thread(context)
|
||||
|
||||
# Raise to pause - framework handles persistence automatically
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={
|
||||
"slack_channel": "#reviews",
|
||||
"thread_id": ticket_id,
|
||||
}
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: PendingFeedbackContext,
|
||||
callback_info: dict[str, Any] | None = None,
|
||||
message: str | None = None,
|
||||
):
|
||||
"""Initialize the pending feedback exception.
|
||||
|
||||
Args:
|
||||
context: The pending feedback context with flow details
|
||||
callback_info: Optional information for external system callbacks
|
||||
message: Optional custom message (defaults to descriptive message)
|
||||
"""
|
||||
self.context = context
|
||||
self.callback_info = callback_info or {}
|
||||
|
||||
if message is None:
|
||||
message = (
|
||||
f"Human feedback pending for flow '{context.flow_id}' "
|
||||
f"at method '{context.method_name}'"
|
||||
)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class HumanFeedbackProvider(Protocol):
|
||||
"""Protocol for human feedback collection strategies.
|
||||
|
||||
Implement this protocol to create custom feedback providers that integrate
|
||||
with external systems like Slack, Teams, email, or custom APIs.
|
||||
|
||||
Providers can be either:
|
||||
- **Synchronous (blocking)**: Return feedback string directly
|
||||
- **Asynchronous (non-blocking)**: Raise HumanFeedbackPending to pause
|
||||
|
||||
The default ConsoleProvider is synchronous and blocks waiting for input.
|
||||
For async workflows, implement a provider that raises HumanFeedbackPending.
|
||||
|
||||
Note:
|
||||
The flow framework automatically handles state persistence when
|
||||
HumanFeedbackPending is raised. Providers only need to:
|
||||
1. Notify the external system (Slack, email, webhook, etc.)
|
||||
2. Raise HumanFeedbackPending with the context and callback info
|
||||
|
||||
Example synchronous provider:
|
||||
```python
|
||||
class ConsoleProvider(HumanFeedbackProvider):
|
||||
def request_feedback(self, context, flow):
|
||||
print(context.method_output)
|
||||
return input("Your feedback: ")
|
||||
```
|
||||
|
||||
Example async provider:
|
||||
```python
|
||||
class SlackProvider(HumanFeedbackProvider):
|
||||
def __init__(self, channel: str):
|
||||
self.channel = channel
|
||||
|
||||
def request_feedback(self, context, flow):
|
||||
# Send notification to Slack
|
||||
thread_id = self.post_to_slack(
|
||||
channel=self.channel,
|
||||
message=context.message,
|
||||
content=context.method_output,
|
||||
)
|
||||
|
||||
# Raise to pause - framework handles persistence automatically
|
||||
raise HumanFeedbackPending(
|
||||
context=context,
|
||||
callback_info={
|
||||
"channel": self.channel,
|
||||
"thread_id": thread_id,
|
||||
}
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
def request_feedback(
|
||||
self,
|
||||
context: PendingFeedbackContext,
|
||||
flow: Flow,
|
||||
) -> str:
|
||||
"""Request feedback from a human.
|
||||
|
||||
For synchronous providers, block and return the feedback string.
|
||||
For async providers, notify the external system and raise
|
||||
HumanFeedbackPending to pause the flow.
|
||||
|
||||
Args:
|
||||
context: The pending feedback context containing all details
|
||||
about what feedback is needed and how to route the response.
|
||||
flow: The Flow instance, providing access to state and name.
|
||||
|
||||
Returns:
|
||||
The human's feedback as a string (synchronous providers only).
|
||||
|
||||
Raises:
|
||||
HumanFeedbackPending: To signal that the flow should pause and
|
||||
wait for external feedback. The framework will automatically
|
||||
persist state when this is raised.
|
||||
"""
|
||||
...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,15 +70,6 @@ class FlowMethod(Generic[P, R]):
|
||||
|
||||
self._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined]
|
||||
|
||||
# Preserve flow-related attributes from wrapped method (e.g., from @human_feedback)
|
||||
for attr in [
|
||||
"__is_router__",
|
||||
"__router_paths__",
|
||||
"__human_feedback_config__",
|
||||
]:
|
||||
if hasattr(meth, attr):
|
||||
setattr(self, attr, getattr(meth, attr))
|
||||
|
||||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
||||
"""Call the wrapped method.
|
||||
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
"""Human feedback decorator for Flow methods.
|
||||
|
||||
This module provides the @human_feedback decorator that enables human-in-the-loop
|
||||
workflows within CrewAI Flows. It allows collecting human feedback on method outputs
|
||||
and optionally routing to different listeners based on the feedback.
|
||||
|
||||
Supports both synchronous (blocking) and asynchronous (non-blocking) feedback
|
||||
collection through the provider parameter.
|
||||
|
||||
Example (synchronous, default):
|
||||
```python
|
||||
from crewai.flow import Flow, start, listen, human_feedback
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Please review this content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def generate_content(self):
|
||||
return {"title": "Article", "body": "Content..."}
|
||||
|
||||
@listen("approved")
|
||||
def publish(self):
|
||||
result = self.human_feedback
|
||||
print(f"Publishing: {result.output}")
|
||||
```
|
||||
|
||||
Example (asynchronous with custom provider):
|
||||
```python
|
||||
from crewai.flow import Flow, start, human_feedback
|
||||
from crewai.flow.async_feedback import HumanFeedbackProvider, HumanFeedbackPending
|
||||
|
||||
class SlackProvider(HumanFeedbackProvider):
|
||||
def request_feedback(self, context, flow):
|
||||
self.send_notification(context)
|
||||
raise HumanFeedbackPending(context=context)
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
provider=SlackProvider(),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "Content..."
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from crewai.flow.flow_wrappers import FlowMethod
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackProvider
|
||||
from crewai.flow.flow import Flow
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanFeedbackResult:
|
||||
"""Result from a @human_feedback decorated method.
|
||||
|
||||
This dataclass captures all information about a human feedback interaction,
|
||||
including the original method output, the human's feedback, and any
|
||||
collapsed outcome for routing purposes.
|
||||
|
||||
Attributes:
|
||||
output: The original return value from the decorated method that was
|
||||
shown to the human for review.
|
||||
feedback: The raw text feedback provided by the human. Empty string
|
||||
if no feedback was provided.
|
||||
outcome: The collapsed outcome string when emit is specified.
|
||||
This is determined by the LLM based on the human's feedback.
|
||||
None if emit was not specified.
|
||||
timestamp: When the feedback was received.
|
||||
method_name: The name of the decorated method that triggered feedback.
|
||||
metadata: Optional metadata for enterprise integrations. Can be used
|
||||
to pass additional context like channel, assignee, etc.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@listen("approved")
|
||||
def handle_approval(self):
|
||||
result = self.human_feedback
|
||||
print(f"Output: {result.output}")
|
||||
print(f"Feedback: {result.feedback}")
|
||||
print(f"Outcome: {result.outcome}") # "approved"
|
||||
```
|
||||
"""
|
||||
|
||||
output: Any
|
||||
feedback: str
|
||||
outcome: str | None = None
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
method_name: str = ""
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanFeedbackConfig:
|
||||
"""Configuration for the @human_feedback decorator.
|
||||
|
||||
Stores the parameters passed to the decorator for later use during
|
||||
method execution and for introspection by visualization tools.
|
||||
|
||||
Attributes:
|
||||
message: The message shown to the human when requesting feedback.
|
||||
emit: Optional sequence of outcome strings for routing.
|
||||
llm: The LLM model to use for collapsing feedback to outcomes.
|
||||
default_outcome: The outcome to use when no feedback is provided.
|
||||
metadata: Optional metadata for enterprise integrations.
|
||||
provider: Optional custom feedback provider for async workflows.
|
||||
"""
|
||||
|
||||
message: str
|
||||
emit: Sequence[str] | None = None
|
||||
llm: str | BaseLLM | None = None
|
||||
default_outcome: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
provider: HumanFeedbackProvider | None = None
|
||||
|
||||
|
||||
class HumanFeedbackMethod(FlowMethod[Any, Any]):
|
||||
"""Wrapper for methods decorated with @human_feedback.
|
||||
|
||||
This wrapper extends FlowMethod to add human feedback specific attributes
|
||||
that are used by FlowMeta for routing and by visualization tools.
|
||||
|
||||
Attributes:
|
||||
__is_router__: True when emit is specified, enabling router behavior.
|
||||
__router_paths__: List of possible outcomes when acting as a router.
|
||||
__human_feedback_config__: The HumanFeedbackConfig for this method.
|
||||
"""
|
||||
|
||||
__is_router__: bool = False
|
||||
__router_paths__: list[str] | None = None
|
||||
__human_feedback_config__: HumanFeedbackConfig | None = None
|
||||
|
||||
|
||||
def human_feedback(
|
||||
message: str,
|
||||
emit: Sequence[str] | None = None,
|
||||
llm: str | BaseLLM | None = None,
|
||||
default_outcome: str | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
provider: HumanFeedbackProvider | None = None,
|
||||
) -> Callable[[F], F]:
|
||||
"""Decorator for Flow methods that require human feedback.
|
||||
|
||||
This decorator wraps a Flow method to:
|
||||
1. Execute the method and capture its output
|
||||
2. Display the output to the human with a feedback request
|
||||
3. Collect the human's free-form feedback
|
||||
4. Optionally collapse the feedback to a predefined outcome using an LLM
|
||||
5. Store the result for access by downstream methods
|
||||
|
||||
When `emit` is specified, the decorator acts as a router, and the
|
||||
collapsed outcome triggers the appropriate @listen decorated method.
|
||||
|
||||
Supports both synchronous (blocking) and asynchronous (non-blocking)
|
||||
feedback collection through the `provider` parameter. If no provider
|
||||
is specified, defaults to synchronous console input.
|
||||
|
||||
Args:
|
||||
message: The message shown to the human when requesting feedback.
|
||||
This should clearly explain what kind of feedback is expected.
|
||||
emit: Optional sequence of outcome strings. When provided, the
|
||||
human's feedback will be collapsed to one of these outcomes
|
||||
using the specified LLM. The outcome then triggers @listen
|
||||
methods that match.
|
||||
llm: The LLM model to use for collapsing feedback to outcomes.
|
||||
Required when emit is specified. Can be a model string
|
||||
like "gpt-4o-mini" or a BaseLLM instance.
|
||||
default_outcome: The outcome to use when the human provides no
|
||||
feedback (empty input). Must be one of the emit values
|
||||
if emit is specified.
|
||||
metadata: Optional metadata for enterprise integrations. This is
|
||||
passed through to the HumanFeedbackResult and can be used
|
||||
by enterprise forks for features like Slack/Teams integration.
|
||||
provider: Optional HumanFeedbackProvider for custom feedback
|
||||
collection. Use this for async workflows that integrate with
|
||||
external systems like Slack, Teams, or webhooks. When the
|
||||
provider raises HumanFeedbackPending, the flow pauses and
|
||||
can be resumed later with Flow.resume().
|
||||
|
||||
Returns:
|
||||
A decorator function that wraps the method with human feedback
|
||||
collection logic.
|
||||
|
||||
Raises:
|
||||
ValueError: If emit is specified but llm is not provided.
|
||||
ValueError: If default_outcome is specified but emit is not.
|
||||
ValueError: If default_outcome is not in the emit list.
|
||||
HumanFeedbackPending: When an async provider pauses execution.
|
||||
|
||||
Example:
|
||||
Basic feedback without routing:
|
||||
```python
|
||||
@start()
|
||||
@human_feedback(message="Please review this output:")
|
||||
def generate_content(self):
|
||||
return "Generated content..."
|
||||
```
|
||||
|
||||
With routing based on feedback:
|
||||
```python
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review and approve or reject:",
|
||||
emit=["approved", "rejected", "needs_revision"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_revision",
|
||||
)
|
||||
def review_document(self):
|
||||
return document_content
|
||||
|
||||
@listen("approved")
|
||||
def publish(self):
|
||||
print(f"Publishing: {self.last_human_feedback.output}")
|
||||
```
|
||||
|
||||
Async feedback with custom provider:
|
||||
```python
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review this content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
provider=SlackProvider(channel="#reviews"),
|
||||
)
|
||||
def generate_content(self):
|
||||
return "Content to review..."
|
||||
```
|
||||
"""
|
||||
# Validation at decoration time
|
||||
if emit is not None:
|
||||
if not llm:
|
||||
raise ValueError(
|
||||
"llm is required when emit is specified. "
|
||||
"Provide an LLM model string (e.g., 'gpt-4o-mini') or a BaseLLM instance."
|
||||
)
|
||||
if default_outcome is not None and default_outcome not in emit:
|
||||
raise ValueError(
|
||||
f"default_outcome '{default_outcome}' must be one of the "
|
||||
f"emit options: {list(emit)}"
|
||||
)
|
||||
elif default_outcome is not None:
|
||||
raise ValueError("default_outcome requires emit to be specified.")
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
"""Inner decorator that wraps the function."""
|
||||
|
||||
def _request_feedback(flow_instance: Flow, method_output: Any) -> str:
|
||||
"""Request feedback using provider or default console."""
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
# Build context for provider
|
||||
# Use flow_id property which handles both dict and BaseModel states
|
||||
context = PendingFeedbackContext(
|
||||
flow_id=flow_instance.flow_id or "unknown",
|
||||
flow_class=f"{flow_instance.__class__.__module__}.{flow_instance.__class__.__name__}",
|
||||
method_name=func.__name__,
|
||||
method_output=method_output,
|
||||
message=message,
|
||||
emit=list(emit) if emit else None,
|
||||
default_outcome=default_outcome,
|
||||
metadata=metadata or {},
|
||||
llm=llm if isinstance(llm, str) else None,
|
||||
)
|
||||
|
||||
if provider is not None:
|
||||
# Use custom provider (may raise HumanFeedbackPending)
|
||||
return provider.request_feedback(context, flow_instance)
|
||||
else:
|
||||
# Use default console input
|
||||
return flow_instance._request_human_feedback(
|
||||
message=message,
|
||||
output=method_output,
|
||||
metadata=metadata,
|
||||
emit=emit,
|
||||
)
|
||||
|
||||
def _process_feedback(
|
||||
flow_instance: Flow,
|
||||
method_output: Any,
|
||||
raw_feedback: str,
|
||||
) -> HumanFeedbackResult | str:
|
||||
"""Process feedback and return result or outcome."""
|
||||
# Determine outcome
|
||||
collapsed_outcome: str | None = None
|
||||
|
||||
if not raw_feedback.strip():
|
||||
# Empty feedback
|
||||
if default_outcome:
|
||||
collapsed_outcome = default_outcome
|
||||
elif emit:
|
||||
# No default and no feedback - use first outcome
|
||||
collapsed_outcome = emit[0]
|
||||
elif emit:
|
||||
# Collapse feedback to outcome using LLM
|
||||
collapsed_outcome = flow_instance._collapse_to_outcome(
|
||||
feedback=raw_feedback,
|
||||
outcomes=emit,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
# Create result
|
||||
result = HumanFeedbackResult(
|
||||
output=method_output,
|
||||
feedback=raw_feedback,
|
||||
outcome=collapsed_outcome,
|
||||
timestamp=datetime.now(),
|
||||
method_name=func.__name__,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
# Store in flow instance
|
||||
flow_instance.human_feedback_history.append(result)
|
||||
flow_instance.last_human_feedback = result
|
||||
|
||||
# Return based on mode
|
||||
if emit:
|
||||
# Return outcome for routing
|
||||
return collapsed_outcome # type: ignore[return-value]
|
||||
return result
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
# Async wrapper
|
||||
@wraps(func)
|
||||
async def async_wrapper(self: Flow, *args: Any, **kwargs: Any) -> Any:
|
||||
# Execute the original method
|
||||
method_output = await func(self, *args, **kwargs)
|
||||
|
||||
# Request human feedback (may raise HumanFeedbackPending)
|
||||
raw_feedback = _request_feedback(self, method_output)
|
||||
|
||||
# Process and return
|
||||
return _process_feedback(self, method_output, raw_feedback)
|
||||
|
||||
wrapper: Any = async_wrapper
|
||||
else:
|
||||
# Sync wrapper
|
||||
@wraps(func)
|
||||
def sync_wrapper(self: Flow, *args: Any, **kwargs: Any) -> Any:
|
||||
# Execute the original method
|
||||
method_output = func(self, *args, **kwargs)
|
||||
|
||||
# Request human feedback (may raise HumanFeedbackPending)
|
||||
raw_feedback = _request_feedback(self, method_output)
|
||||
|
||||
# Process and return
|
||||
return _process_feedback(self, method_output, raw_feedback)
|
||||
|
||||
wrapper = sync_wrapper
|
||||
|
||||
# Preserve existing Flow decorator attributes
|
||||
for attr in [
|
||||
"__is_start_method__",
|
||||
"__trigger_methods__",
|
||||
"__condition_type__",
|
||||
"__trigger_condition__",
|
||||
"__is_flow_method__",
|
||||
]:
|
||||
if hasattr(func, attr):
|
||||
setattr(wrapper, attr, getattr(func, attr))
|
||||
|
||||
# Add human feedback specific attributes (create config inline to avoid race conditions)
|
||||
wrapper.__human_feedback_config__ = HumanFeedbackConfig(
|
||||
message=message,
|
||||
emit=emit,
|
||||
llm=llm,
|
||||
default_outcome=default_outcome,
|
||||
metadata=metadata,
|
||||
provider=provider,
|
||||
)
|
||||
wrapper.__is_flow_method__ = True
|
||||
|
||||
# Make it a router if emit specified
|
||||
if emit:
|
||||
wrapper.__is_router__ = True
|
||||
wrapper.__router_paths__ = list(emit)
|
||||
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
return decorator
|
||||
@@ -1,26 +1,16 @@
|
||||
"""Base class for flow state persistence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
|
||||
class FlowPersistence(ABC):
|
||||
"""Abstract base class for flow state persistence.
|
||||
|
||||
This class defines the interface that all persistence implementations must follow.
|
||||
It supports both structured (Pydantic BaseModel) and unstructured (dict) states.
|
||||
|
||||
For async human feedback support, implementations can optionally override:
|
||||
- save_pending_feedback(): Saves state with pending feedback context
|
||||
- load_pending_feedback(): Loads state and pending feedback context
|
||||
- clear_pending_feedback(): Clears pending feedback after resume
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -55,52 +45,3 @@ class FlowPersistence(ABC):
|
||||
Returns:
|
||||
The most recent state as a dictionary, or None if no state exists
|
||||
"""
|
||||
|
||||
def save_pending_feedback(
|
||||
self,
|
||||
flow_uuid: str,
|
||||
context: PendingFeedbackContext,
|
||||
state_data: dict[str, Any] | BaseModel,
|
||||
) -> None:
|
||||
"""Save state with a pending feedback marker.
|
||||
|
||||
This method is called when a flow is paused waiting for async human
|
||||
feedback. The default implementation just saves the state without
|
||||
the pending feedback context. Override to store the context.
|
||||
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
context: The pending feedback context with all resume information
|
||||
state_data: Current state data
|
||||
"""
|
||||
# Default: just save the state without pending context
|
||||
self.save_state(flow_uuid, context.method_name, state_data)
|
||||
|
||||
def load_pending_feedback(
|
||||
self,
|
||||
flow_uuid: str,
|
||||
) -> tuple[dict[str, Any], PendingFeedbackContext] | None:
|
||||
"""Load state and pending feedback context.
|
||||
|
||||
This method is called when resuming a paused flow. Override to
|
||||
load both the state and the pending feedback context.
|
||||
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
|
||||
Returns:
|
||||
Tuple of (state_data, pending_context) if pending feedback exists,
|
||||
None otherwise.
|
||||
"""
|
||||
return None
|
||||
|
||||
def clear_pending_feedback(self, flow_uuid: str) -> None: # noqa: B027
|
||||
"""Clear the pending feedback marker after successful resume.
|
||||
|
||||
This is called after feedback is received and the flow resumes.
|
||||
Optional override to remove the pending feedback marker.
|
||||
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -2,22 +2,17 @@
|
||||
SQLite-based implementation of flow state persistence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.flow.persistence.base import FlowPersistence
|
||||
from crewai.utilities.paths import db_storage_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
|
||||
class SQLiteFlowPersistence(FlowPersistence):
|
||||
"""SQLite-based implementation of flow state persistence.
|
||||
@@ -25,28 +20,6 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
This class provides a simple, file-based persistence implementation using SQLite.
|
||||
It's suitable for development and testing, or for production use cases with
|
||||
moderate performance requirements.
|
||||
|
||||
This implementation supports async human feedback by storing pending feedback
|
||||
context in a separate table. When a flow is paused waiting for feedback,
|
||||
use save_pending_feedback() to persist the context. Later, use
|
||||
load_pending_feedback() to retrieve it when resuming.
|
||||
|
||||
Example:
|
||||
```python
|
||||
persistence = SQLiteFlowPersistence("flows.db")
|
||||
|
||||
# Start a flow with async feedback
|
||||
try:
|
||||
flow = MyFlow(persistence=persistence)
|
||||
result = flow.kickoff()
|
||||
except HumanFeedbackPending as e:
|
||||
# Flow is paused, state is already persisted
|
||||
print(f"Waiting for feedback: {e.context.flow_id}")
|
||||
|
||||
# Later, resume with feedback
|
||||
flow = MyFlow.from_pending("abc-123", persistence)
|
||||
result = flow.resume("looks good!")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str | None = None) -> None:
|
||||
@@ -72,7 +45,6 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
def init_db(self) -> None:
|
||||
"""Create the necessary tables if they don't exist."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
# Main state table
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS flow_states (
|
||||
@@ -92,26 +64,6 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
"""
|
||||
)
|
||||
|
||||
# Pending feedback table for async HITL
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pending_feedback (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
flow_uuid TEXT NOT NULL UNIQUE,
|
||||
context_json TEXT NOT NULL,
|
||||
state_json TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
# Add index for faster UUID lookups on pending feedback
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_pending_feedback_uuid
|
||||
ON pending_feedback(flow_uuid)
|
||||
"""
|
||||
)
|
||||
|
||||
def save_state(
|
||||
self,
|
||||
flow_uuid: str,
|
||||
@@ -178,104 +130,3 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
if row:
|
||||
return json.loads(row[0])
|
||||
return None
|
||||
|
||||
def save_pending_feedback(
|
||||
self,
|
||||
flow_uuid: str,
|
||||
context: PendingFeedbackContext,
|
||||
state_data: dict[str, Any] | BaseModel,
|
||||
) -> None:
|
||||
"""Save state with a pending feedback marker.
|
||||
|
||||
This method stores both the flow state and the pending feedback context,
|
||||
allowing the flow to be resumed later when feedback is received.
|
||||
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
context: The pending feedback context with all resume information
|
||||
state_data: Current state data
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
# Convert state_data to dict
|
||||
if isinstance(state_data, BaseModel):
|
||||
state_dict = state_data.model_dump()
|
||||
elif isinstance(state_data, dict):
|
||||
state_dict = state_data
|
||||
else:
|
||||
raise ValueError(
|
||||
f"state_data must be either a Pydantic BaseModel or dict, got {type(state_data)}"
|
||||
)
|
||||
|
||||
# Also save to regular state table for consistency
|
||||
self.save_state(flow_uuid, context.method_name, state_data)
|
||||
|
||||
# Save pending feedback context
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
# Use INSERT OR REPLACE to handle re-triggering feedback on same flow
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO pending_feedback (
|
||||
flow_uuid,
|
||||
context_json,
|
||||
state_json,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
flow_uuid,
|
||||
json.dumps(context.to_dict()),
|
||||
json.dumps(state_dict),
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
),
|
||||
)
|
||||
|
||||
def load_pending_feedback(
|
||||
self,
|
||||
flow_uuid: str,
|
||||
) -> tuple[dict[str, Any], PendingFeedbackContext] | None:
|
||||
"""Load state and pending feedback context.
|
||||
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
|
||||
Returns:
|
||||
Tuple of (state_data, pending_context) if pending feedback exists,
|
||||
None otherwise.
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT state_json, context_json
|
||||
FROM pending_feedback
|
||||
WHERE flow_uuid = ?
|
||||
""",
|
||||
(flow_uuid,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
state_dict = json.loads(row[0])
|
||||
context_dict = json.loads(row[1])
|
||||
context = PendingFeedbackContext.from_dict(context_dict)
|
||||
return (state_dict, context)
|
||||
return None
|
||||
|
||||
def clear_pending_feedback(self, flow_uuid: str) -> None:
|
||||
"""Clear the pending feedback marker after successful resume.
|
||||
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM pending_feedback
|
||||
WHERE flow_uuid = ?
|
||||
""",
|
||||
(flow_uuid,),
|
||||
)
|
||||
|
||||
@@ -249,7 +249,6 @@ class ToolUsage:
|
||||
"tool_args": self.action.tool_input,
|
||||
"tool_class": self.action.tool,
|
||||
"agent": self.agent,
|
||||
"run_attempts": self._run_attempts,
|
||||
}
|
||||
|
||||
if self.agent.fingerprint: # type: ignore
|
||||
@@ -436,7 +435,6 @@ class ToolUsage:
|
||||
"tool_args": self.action.tool_input,
|
||||
"tool_class": self.action.tool,
|
||||
"agent": self.agent,
|
||||
"run_attempts": self._run_attempts,
|
||||
}
|
||||
|
||||
# TODO: Investigate fingerprint attribute availability on BaseAgent/LiteAgent
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
"lite_agent_system_prompt_without_tools": "You are {role}. {backstory}\nYour personal goal is: {goal}\n\nTo give my best complete final answer to the task respond using the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!",
|
||||
"lite_agent_response_format": "Ensure your final answer strictly adheres to the following OpenAPI schema: {response_format}\n\nDo not include the OpenAPI schema in the final output. Ensure the final output does not include any code block markers like ```json or ```python.",
|
||||
"knowledge_search_query": "The original query is: {task_prompt}.",
|
||||
"knowledge_search_query_system_prompt": "Your goal is to rewrite the user query so that it is optimized for retrieval from a vector database. Consider how the query will be used to find relevant documents, and aim to make it more specific and context-aware. \n\n Do not include any other text than the rewritten query, especially any preamble or postamble and only add expected output format if its relevant to the rewritten query. \n\n Focus on the key words of the intended task and to retrieve the most relevant information. \n\n There will be some extra context provided that might need to be removed such as expected_output formats structured_outputs and other instructions.",
|
||||
"human_feedback_collapse": "Based on the following human feedback, determine which outcome best matches their intent.\n\nFeedback: {feedback}\n\nPossible outcomes: {outcomes}\n\nRespond with ONLY one of the exact outcome values listed above, nothing else."
|
||||
"knowledge_search_query_system_prompt": "Your goal is to rewrite the user query so that it is optimized for retrieval from a vector database. Consider how the query will be used to find relevant documents, and aim to make it more specific and context-aware. \n\n Do not include any other text than the rewritten query, especially any preamble or postamble and only add expected output format if its relevant to the rewritten query. \n\n Focus on the key words of the intended task and to retrieve the most relevant information. \n\n There will be some extra context provided that might need to be removed such as expected_output formats structured_outputs and other instructions."
|
||||
},
|
||||
"errors": {
|
||||
"force_final_answer_error": "You can't keep going, here is the best final answer you generated:\n\n {formatted_answer}",
|
||||
|
||||
@@ -30,3 +30,4 @@ NOT_SPECIFIED: Final[
|
||||
"allows us to distinguish between 'not passed at all' and 'explicitly passed None' or '[]'.",
|
||||
]
|
||||
] = _NotSpecified()
|
||||
CREWAI_BASE_URL: Final[str] = "https://app.crewai.com"
|
||||
|
||||
@@ -1178,7 +1178,6 @@ def test_system_and_prompt_template():
|
||||
|
||||
{{ .Response }}<|eot_id|>""",
|
||||
)
|
||||
agent.create_agent_executor()
|
||||
|
||||
expected_prompt = """<|start_header_id|>system<|end_header_id|>
|
||||
|
||||
@@ -1443,8 +1442,6 @@ def test_agent_max_retry_limit():
|
||||
human_input=True,
|
||||
)
|
||||
|
||||
agent.create_agent_executor(task=task)
|
||||
|
||||
error_message = "Error happening while sending prompt to model."
|
||||
with patch.object(
|
||||
CrewAgentExecutor, "invoke", wraps=agent.agent_executor.invoke
|
||||
@@ -1506,8 +1503,9 @@ def test_agent_with_custom_stop_words():
|
||||
)
|
||||
|
||||
assert isinstance(agent.llm, BaseLLM)
|
||||
assert set(agent.llm.stop) == set(stop_words)
|
||||
assert set(agent.llm.stop) == set([*stop_words, "\nObservation:"])
|
||||
assert all(word in agent.llm.stop for word in stop_words)
|
||||
assert "\nObservation:" in agent.llm.stop
|
||||
|
||||
|
||||
def test_agent_with_callbacks():
|
||||
@@ -1631,8 +1629,6 @@ def test_handle_context_length_exceeds_limit_cli_no():
|
||||
)
|
||||
task = Task(description="test task", agent=agent, expected_output="test output")
|
||||
|
||||
agent.create_agent_executor(task=task)
|
||||
|
||||
with patch.object(
|
||||
CrewAgentExecutor, "invoke", wraps=agent.agent_executor.invoke
|
||||
) as private_mock:
|
||||
@@ -1683,8 +1679,8 @@ def test_agent_with_all_llm_attributes():
|
||||
assert agent.llm.temperature == 0.7
|
||||
assert agent.llm.top_p == 0.9
|
||||
# assert agent.llm.n == 1
|
||||
assert set(agent.llm.stop) == set(["STOP", "END"])
|
||||
assert all(word in agent.llm.stop for word in ["STOP", "END"])
|
||||
assert set(agent.llm.stop) == set(["STOP", "END", "\nObservation:"])
|
||||
assert all(word in agent.llm.stop for word in ["STOP", "END", "\nObservation:"])
|
||||
assert agent.llm.max_tokens == 100
|
||||
assert agent.llm.presence_penalty == 0.1
|
||||
assert agent.llm.frequency_penalty == 0.1
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
"""Unit tests for CrewAgentExecutorFlow.
|
||||
|
||||
Tests the Flow-based agent executor implementation including state management,
|
||||
flow methods, routing logic, and error handling.
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.experimental.crew_agent_executor_flow import (
|
||||
AgentReActState,
|
||||
CrewAgentExecutorFlow,
|
||||
)
|
||||
from crewai.agents.parser import AgentAction, AgentFinish
|
||||
|
||||
class TestAgentReActState:
|
||||
"""Test AgentReActState Pydantic model."""
|
||||
|
||||
def test_state_initialization(self):
|
||||
"""Test AgentReActState initialization with defaults."""
|
||||
state = AgentReActState()
|
||||
assert state.iterations == 0
|
||||
assert state.messages == []
|
||||
assert state.current_answer is None
|
||||
assert state.is_finished is False
|
||||
assert state.ask_for_human_input is False
|
||||
|
||||
def test_state_with_values(self):
|
||||
"""Test AgentReActState initialization with values."""
|
||||
messages = [{"role": "user", "content": "test"}]
|
||||
state = AgentReActState(
|
||||
messages=messages,
|
||||
iterations=5,
|
||||
current_answer=AgentFinish(thought="thinking", output="done", text="final"),
|
||||
is_finished=True,
|
||||
ask_for_human_input=True,
|
||||
)
|
||||
assert state.messages == messages
|
||||
assert state.iterations == 5
|
||||
assert isinstance(state.current_answer, AgentFinish)
|
||||
assert state.is_finished is True
|
||||
assert state.ask_for_human_input is True
|
||||
|
||||
|
||||
class TestCrewAgentExecutorFlow:
|
||||
"""Test CrewAgentExecutorFlow class."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dependencies(self):
|
||||
"""Create mock dependencies for executor."""
|
||||
llm = Mock()
|
||||
llm.supports_stop_words.return_value = True
|
||||
|
||||
task = Mock()
|
||||
task.description = "Test task"
|
||||
task.human_input = False
|
||||
task.response_model = None
|
||||
|
||||
crew = Mock()
|
||||
crew.verbose = False
|
||||
crew._train = False
|
||||
|
||||
agent = Mock()
|
||||
agent.id = "test-agent-id"
|
||||
agent.role = "Test Agent"
|
||||
agent.verbose = False
|
||||
agent.key = "test-key"
|
||||
|
||||
prompt = {"prompt": "Test prompt with {input}, {tool_names}, {tools}"}
|
||||
|
||||
tools = []
|
||||
tools_handler = Mock()
|
||||
|
||||
return {
|
||||
"llm": llm,
|
||||
"task": task,
|
||||
"crew": crew,
|
||||
"agent": agent,
|
||||
"prompt": prompt,
|
||||
"max_iter": 10,
|
||||
"tools": tools,
|
||||
"tools_names": "",
|
||||
"stop_words": ["Observation"],
|
||||
"tools_description": "",
|
||||
"tools_handler": tools_handler,
|
||||
}
|
||||
|
||||
def test_executor_initialization(self, mock_dependencies):
|
||||
"""Test CrewAgentExecutorFlow initialization."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
|
||||
assert executor.llm == mock_dependencies["llm"]
|
||||
assert executor.task == mock_dependencies["task"]
|
||||
assert executor.agent == mock_dependencies["agent"]
|
||||
assert executor.crew == mock_dependencies["crew"]
|
||||
assert executor.max_iter == 10
|
||||
assert executor.use_stop_words is True
|
||||
|
||||
def test_initialize_reasoning(self, mock_dependencies):
|
||||
"""Test flow entry point."""
|
||||
with patch.object(
|
||||
CrewAgentExecutorFlow, "_show_start_logs"
|
||||
) as mock_show_start:
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
result = executor.initialize_reasoning()
|
||||
|
||||
assert result == "initialized"
|
||||
mock_show_start.assert_called_once()
|
||||
|
||||
def test_check_max_iterations_not_reached(self, mock_dependencies):
|
||||
"""Test routing when iterations < max."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.iterations = 5
|
||||
|
||||
result = executor.check_max_iterations()
|
||||
assert result == "continue_reasoning"
|
||||
|
||||
def test_check_max_iterations_reached(self, mock_dependencies):
|
||||
"""Test routing when iterations >= max."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.iterations = 10
|
||||
|
||||
result = executor.check_max_iterations()
|
||||
assert result == "force_final_answer"
|
||||
|
||||
def test_route_by_answer_type_action(self, mock_dependencies):
|
||||
"""Test routing for AgentAction."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.current_answer = AgentAction(
|
||||
thought="thinking", tool="search", tool_input="query", text="action text"
|
||||
)
|
||||
|
||||
result = executor.route_by_answer_type()
|
||||
assert result == "execute_tool"
|
||||
|
||||
def test_route_by_answer_type_finish(self, mock_dependencies):
|
||||
"""Test routing for AgentFinish."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="final thoughts", output="Final answer", text="complete"
|
||||
)
|
||||
|
||||
result = executor.route_by_answer_type()
|
||||
assert result == "agent_finished"
|
||||
|
||||
def test_continue_iteration(self, mock_dependencies):
|
||||
"""Test iteration continuation."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
|
||||
result = executor.continue_iteration()
|
||||
|
||||
assert result == "check_iteration"
|
||||
|
||||
def test_finalize_success(self, mock_dependencies):
|
||||
"""Test finalize with valid AgentFinish."""
|
||||
with patch.object(CrewAgentExecutorFlow, "_show_logs") as mock_show_logs:
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="final thinking", output="Done", text="complete"
|
||||
)
|
||||
|
||||
result = executor.finalize()
|
||||
|
||||
assert result == "completed"
|
||||
assert executor.state.is_finished is True
|
||||
mock_show_logs.assert_called_once()
|
||||
|
||||
def test_finalize_failure(self, mock_dependencies):
|
||||
"""Test finalize skips when given AgentAction instead of AgentFinish."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.current_answer = AgentAction(
|
||||
thought="thinking", tool="search", tool_input="query", text="action text"
|
||||
)
|
||||
|
||||
result = executor.finalize()
|
||||
|
||||
# Should return "skipped" and not set is_finished
|
||||
assert result == "skipped"
|
||||
assert executor.state.is_finished is False
|
||||
|
||||
def test_format_prompt(self, mock_dependencies):
|
||||
"""Test prompt formatting."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
inputs = {"input": "test input", "tool_names": "tool1, tool2", "tools": "desc"}
|
||||
|
||||
result = executor._format_prompt("Prompt {input} {tool_names} {tools}", inputs)
|
||||
|
||||
assert "test input" in result
|
||||
assert "tool1, tool2" in result
|
||||
assert "desc" in result
|
||||
|
||||
def test_is_training_mode_false(self, mock_dependencies):
|
||||
"""Test training mode detection when not in training."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
assert executor._is_training_mode() is False
|
||||
|
||||
def test_is_training_mode_true(self, mock_dependencies):
|
||||
"""Test training mode detection when in training."""
|
||||
mock_dependencies["crew"]._train = True
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
assert executor._is_training_mode() is True
|
||||
|
||||
def test_append_message_to_state(self, mock_dependencies):
|
||||
"""Test message appending to state."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
initial_count = len(executor.state.messages)
|
||||
|
||||
executor._append_message_to_state("test message")
|
||||
|
||||
assert len(executor.state.messages) == initial_count + 1
|
||||
assert executor.state.messages[-1]["content"] == "test message"
|
||||
|
||||
def test_invoke_step_callback(self, mock_dependencies):
|
||||
"""Test step callback invocation."""
|
||||
callback = Mock()
|
||||
mock_dependencies["step_callback"] = callback
|
||||
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
answer = AgentFinish(thought="thinking", output="test", text="final")
|
||||
|
||||
executor._invoke_step_callback(answer)
|
||||
|
||||
callback.assert_called_once_with(answer)
|
||||
|
||||
def test_invoke_step_callback_none(self, mock_dependencies):
|
||||
"""Test step callback when none provided."""
|
||||
mock_dependencies["step_callback"] = None
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
|
||||
# Should not raise error
|
||||
executor._invoke_step_callback(
|
||||
AgentFinish(thought="thinking", output="test", text="final")
|
||||
)
|
||||
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.handle_output_parser_exception")
|
||||
def test_recover_from_parser_error(
|
||||
self, mock_handle_exception, mock_dependencies
|
||||
):
|
||||
"""Test recovery from OutputParserError."""
|
||||
from crewai.agents.parser import OutputParserError
|
||||
|
||||
mock_handle_exception.return_value = None
|
||||
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor._last_parser_error = OutputParserError("test error")
|
||||
initial_iterations = executor.state.iterations
|
||||
|
||||
result = executor.recover_from_parser_error()
|
||||
|
||||
assert result == "initialized"
|
||||
assert executor.state.iterations == initial_iterations + 1
|
||||
mock_handle_exception.assert_called_once()
|
||||
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.handle_context_length")
|
||||
def test_recover_from_context_length(
|
||||
self, mock_handle_context, mock_dependencies
|
||||
):
|
||||
"""Test recovery from context length error."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor._last_context_error = Exception("context too long")
|
||||
initial_iterations = executor.state.iterations
|
||||
|
||||
result = executor.recover_from_context_length()
|
||||
|
||||
assert result == "initialized"
|
||||
assert executor.state.iterations == initial_iterations + 1
|
||||
mock_handle_context.assert_called_once()
|
||||
|
||||
def test_use_stop_words_property(self, mock_dependencies):
|
||||
"""Test use_stop_words property."""
|
||||
mock_dependencies["llm"].supports_stop_words.return_value = True
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
assert executor.use_stop_words is True
|
||||
|
||||
mock_dependencies["llm"].supports_stop_words.return_value = False
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
assert executor.use_stop_words is False
|
||||
|
||||
def test_compatibility_properties(self, mock_dependencies):
|
||||
"""Test compatibility properties for mixin."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.messages = [{"role": "user", "content": "test"}]
|
||||
executor.state.iterations = 5
|
||||
|
||||
# Test that compatibility properties return state values
|
||||
assert executor.messages == executor.state.messages
|
||||
assert executor.iterations == executor.state.iterations
|
||||
|
||||
|
||||
class TestFlowErrorHandling:
|
||||
"""Test error handling in flow methods."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dependencies(self):
|
||||
"""Create mock dependencies."""
|
||||
llm = Mock()
|
||||
llm.supports_stop_words.return_value = True
|
||||
|
||||
task = Mock()
|
||||
task.description = "Test task"
|
||||
|
||||
crew = Mock()
|
||||
agent = Mock()
|
||||
agent.role = "Test Agent"
|
||||
agent.verbose = False
|
||||
|
||||
prompt = {"prompt": "Test {input}"}
|
||||
|
||||
return {
|
||||
"llm": llm,
|
||||
"task": task,
|
||||
"crew": crew,
|
||||
"agent": agent,
|
||||
"prompt": prompt,
|
||||
"max_iter": 10,
|
||||
"tools": [],
|
||||
"tools_names": "",
|
||||
"stop_words": [],
|
||||
"tools_description": "",
|
||||
"tools_handler": Mock(),
|
||||
}
|
||||
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.get_llm_response")
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.enforce_rpm_limit")
|
||||
def test_call_llm_parser_error(
|
||||
self, mock_enforce_rpm, mock_get_llm, mock_dependencies
|
||||
):
|
||||
"""Test call_llm_and_parse handles OutputParserError."""
|
||||
from crewai.agents.parser import OutputParserError
|
||||
|
||||
mock_enforce_rpm.return_value = None
|
||||
mock_get_llm.side_effect = OutputParserError("parse failed")
|
||||
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
result = executor.call_llm_and_parse()
|
||||
|
||||
assert result == "parser_error"
|
||||
assert executor._last_parser_error is not None
|
||||
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.get_llm_response")
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.enforce_rpm_limit")
|
||||
@patch("crewai.experimental.crew_agent_executor_flow.is_context_length_exceeded")
|
||||
def test_call_llm_context_error(
|
||||
self,
|
||||
mock_is_context_exceeded,
|
||||
mock_enforce_rpm,
|
||||
mock_get_llm,
|
||||
mock_dependencies,
|
||||
):
|
||||
"""Test call_llm_and_parse handles context length error."""
|
||||
mock_enforce_rpm.return_value = None
|
||||
mock_get_llm.side_effect = Exception("context length")
|
||||
mock_is_context_exceeded.return_value = True
|
||||
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
result = executor.call_llm_and_parse()
|
||||
|
||||
assert result == "context_error"
|
||||
assert executor._last_context_error is not None
|
||||
|
||||
|
||||
class TestFlowInvoke:
|
||||
"""Test the invoke method that maintains backward compatibility."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dependencies(self):
|
||||
"""Create mock dependencies."""
|
||||
llm = Mock()
|
||||
task = Mock()
|
||||
task.description = "Test"
|
||||
task.human_input = False
|
||||
|
||||
crew = Mock()
|
||||
crew._short_term_memory = None
|
||||
crew._long_term_memory = None
|
||||
crew._entity_memory = None
|
||||
crew._external_memory = None
|
||||
|
||||
agent = Mock()
|
||||
agent.role = "Test"
|
||||
agent.verbose = False
|
||||
|
||||
prompt = {"prompt": "Test {input} {tool_names} {tools}"}
|
||||
|
||||
return {
|
||||
"llm": llm,
|
||||
"task": task,
|
||||
"crew": crew,
|
||||
"agent": agent,
|
||||
"prompt": prompt,
|
||||
"max_iter": 10,
|
||||
"tools": [],
|
||||
"tools_names": "",
|
||||
"stop_words": [],
|
||||
"tools_description": "",
|
||||
"tools_handler": Mock(),
|
||||
}
|
||||
|
||||
@patch.object(CrewAgentExecutorFlow, "kickoff")
|
||||
@patch.object(CrewAgentExecutorFlow, "_create_short_term_memory")
|
||||
@patch.object(CrewAgentExecutorFlow, "_create_long_term_memory")
|
||||
@patch.object(CrewAgentExecutorFlow, "_create_external_memory")
|
||||
def test_invoke_success(
|
||||
self,
|
||||
mock_external_memory,
|
||||
mock_long_term_memory,
|
||||
mock_short_term_memory,
|
||||
mock_kickoff,
|
||||
mock_dependencies,
|
||||
):
|
||||
"""Test successful invoke without human feedback."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
|
||||
# Mock kickoff to set the final answer in state
|
||||
def mock_kickoff_side_effect():
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="final thinking", output="Final result", text="complete"
|
||||
)
|
||||
|
||||
mock_kickoff.side_effect = mock_kickoff_side_effect
|
||||
|
||||
inputs = {"input": "test", "tool_names": "", "tools": ""}
|
||||
result = executor.invoke(inputs)
|
||||
|
||||
assert result == {"output": "Final result"}
|
||||
mock_kickoff.assert_called_once()
|
||||
mock_short_term_memory.assert_called_once()
|
||||
mock_long_term_memory.assert_called_once()
|
||||
mock_external_memory.assert_called_once()
|
||||
|
||||
@patch.object(CrewAgentExecutorFlow, "kickoff")
|
||||
def test_invoke_failure_no_agent_finish(self, mock_kickoff, mock_dependencies):
|
||||
"""Test invoke fails without AgentFinish."""
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
executor.state.current_answer = AgentAction(
|
||||
thought="thinking", tool="test", tool_input="test", text="action text"
|
||||
)
|
||||
|
||||
inputs = {"input": "test", "tool_names": "", "tools": ""}
|
||||
|
||||
with pytest.raises(RuntimeError, match="without reaching a final answer"):
|
||||
executor.invoke(inputs)
|
||||
|
||||
@patch.object(CrewAgentExecutorFlow, "kickoff")
|
||||
@patch.object(CrewAgentExecutorFlow, "_create_short_term_memory")
|
||||
@patch.object(CrewAgentExecutorFlow, "_create_long_term_memory")
|
||||
@patch.object(CrewAgentExecutorFlow, "_create_external_memory")
|
||||
def test_invoke_with_system_prompt(
|
||||
self,
|
||||
mock_external_memory,
|
||||
mock_long_term_memory,
|
||||
mock_short_term_memory,
|
||||
mock_kickoff,
|
||||
mock_dependencies,
|
||||
):
|
||||
"""Test invoke with system prompt configuration."""
|
||||
mock_dependencies["prompt"] = {
|
||||
"system": "System: {input}",
|
||||
"user": "User: {input} {tool_names} {tools}",
|
||||
}
|
||||
executor = CrewAgentExecutorFlow(**mock_dependencies)
|
||||
|
||||
def mock_kickoff_side_effect():
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="final thoughts", output="Done", text="complete"
|
||||
)
|
||||
|
||||
mock_kickoff.side_effect = mock_kickoff_side_effect
|
||||
|
||||
inputs = {"input": "test", "tool_names": "", "tools": ""}
|
||||
result = executor.invoke(inputs)
|
||||
mock_short_term_memory.assert_called_once()
|
||||
mock_long_term_memory.assert_called_once()
|
||||
mock_external_memory.assert_called_once()
|
||||
mock_kickoff.assert_called_once()
|
||||
|
||||
assert result == {"output": "Done"}
|
||||
assert len(executor.state.messages) >= 2
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai.cli.plus_api import PlusAPI
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
):
|
||||
mock_make_request.assert_called_once_with(
|
||||
method,
|
||||
f"{os.getenv('CREWAI_PLUS_URL')}{endpoint}",
|
||||
f"{DEFAULT_CREWAI_ENTERPRISE_URL}{endpoint}",
|
||||
headers={
|
||||
"Authorization": ANY,
|
||||
"Content-Type": ANY,
|
||||
@@ -53,7 +53,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
@@ -84,7 +84,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
def test_get_agent_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
@@ -115,7 +115,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
def test_get_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
@@ -163,7 +163,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
def test_publish_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
@@ -320,7 +320,6 @@ class TestPlusAPI(unittest.TestCase):
|
||||
)
|
||||
|
||||
@patch("crewai.cli.plus_api.Settings")
|
||||
@patch.dict(os.environ, {"CREWAI_PLUS_URL": ""})
|
||||
def test_custom_base_url(self, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.enterprise_base_url = "https://custom-url.com/api"
|
||||
@@ -330,11 +329,3 @@ class TestPlusAPI(unittest.TestCase):
|
||||
custom_api.base_url,
|
||||
"https://custom-url.com/api",
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {"CREWAI_PLUS_URL": "https://custom-url-from-env.com"})
|
||||
def test_custom_base_url_from_env(self):
|
||||
custom_api = PlusAPI("test_key")
|
||||
self.assertEqual(
|
||||
custom_api.base_url,
|
||||
"https://custom-url-from-env.com",
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,19 +7,22 @@ from crewai.events.event_listener import event_listener
|
||||
class TestFlowHumanInputIntegration:
|
||||
"""Test integration between Flow execution and human input functionality."""
|
||||
|
||||
def test_console_formatter_pause_resume_methods_exist(self):
|
||||
"""Test that ConsoleFormatter pause/resume methods exist and are callable."""
|
||||
def test_console_formatter_pause_resume_methods(self):
|
||||
"""Test that ConsoleFormatter pause/resume methods work correctly."""
|
||||
formatter = event_listener.formatter
|
||||
|
||||
# Methods should exist and be callable
|
||||
assert hasattr(formatter, "pause_live_updates")
|
||||
assert hasattr(formatter, "resume_live_updates")
|
||||
assert callable(formatter.pause_live_updates)
|
||||
assert callable(formatter.resume_live_updates)
|
||||
original_paused_state = formatter._live_paused
|
||||
|
||||
# Should not raise
|
||||
formatter.pause_live_updates()
|
||||
formatter.resume_live_updates()
|
||||
try:
|
||||
formatter._live_paused = False
|
||||
|
||||
formatter.pause_live_updates()
|
||||
assert formatter._live_paused
|
||||
|
||||
formatter.resume_live_updates()
|
||||
assert not formatter._live_paused
|
||||
finally:
|
||||
formatter._live_paused = original_paused_state
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
def test_human_input_pauses_flow_updates(self, mock_input):
|
||||
@@ -35,16 +38,23 @@ class TestFlowHumanInputIntegration:
|
||||
|
||||
formatter = event_listener.formatter
|
||||
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates") as mock_pause,
|
||||
patch.object(formatter, "resume_live_updates") as mock_resume,
|
||||
):
|
||||
result = executor._ask_human_input("Test result")
|
||||
original_paused_state = formatter._live_paused
|
||||
|
||||
mock_pause.assert_called_once()
|
||||
mock_resume.assert_called_once()
|
||||
mock_input.assert_called_once()
|
||||
assert result == ""
|
||||
try:
|
||||
formatter._live_paused = False
|
||||
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates") as mock_pause,
|
||||
patch.object(formatter, "resume_live_updates") as mock_resume,
|
||||
):
|
||||
result = executor._ask_human_input("Test result")
|
||||
|
||||
mock_pause.assert_called_once()
|
||||
mock_resume.assert_called_once()
|
||||
mock_input.assert_called_once()
|
||||
assert result == ""
|
||||
finally:
|
||||
formatter._live_paused = original_paused_state
|
||||
|
||||
@patch("builtins.input", side_effect=["feedback", ""])
|
||||
def test_multiple_human_input_rounds(self, mock_input):
|
||||
@@ -60,46 +70,53 @@ class TestFlowHumanInputIntegration:
|
||||
|
||||
formatter = event_listener.formatter
|
||||
|
||||
pause_calls = []
|
||||
resume_calls = []
|
||||
original_paused_state = formatter._live_paused
|
||||
|
||||
def track_pause():
|
||||
pause_calls.append(True)
|
||||
try:
|
||||
pause_calls = []
|
||||
resume_calls = []
|
||||
|
||||
def track_resume():
|
||||
resume_calls.append(True)
|
||||
def track_pause():
|
||||
pause_calls.append(True)
|
||||
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates", side_effect=track_pause),
|
||||
patch.object(
|
||||
formatter, "resume_live_updates", side_effect=track_resume
|
||||
),
|
||||
):
|
||||
result1 = executor._ask_human_input("Test result 1")
|
||||
assert result1 == "feedback"
|
||||
def track_resume():
|
||||
resume_calls.append(True)
|
||||
|
||||
result2 = executor._ask_human_input("Test result 2")
|
||||
assert result2 == ""
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates", side_effect=track_pause),
|
||||
patch.object(
|
||||
formatter, "resume_live_updates", side_effect=track_resume
|
||||
),
|
||||
):
|
||||
result1 = executor._ask_human_input("Test result 1")
|
||||
assert result1 == "feedback"
|
||||
|
||||
assert len(pause_calls) == 2
|
||||
assert len(resume_calls) == 2
|
||||
result2 = executor._ask_human_input("Test result 2")
|
||||
assert result2 == ""
|
||||
|
||||
assert len(pause_calls) == 2
|
||||
assert len(resume_calls) == 2
|
||||
finally:
|
||||
formatter._live_paused = original_paused_state
|
||||
|
||||
def test_pause_resume_with_no_live_session(self):
|
||||
"""Test pause/resume methods handle case when no Live session exists."""
|
||||
formatter = event_listener.formatter
|
||||
|
||||
original_streaming_live = formatter._streaming_live
|
||||
original_live = formatter._live
|
||||
original_paused_state = formatter._live_paused
|
||||
|
||||
try:
|
||||
formatter._streaming_live = None
|
||||
formatter._live = None
|
||||
formatter._live_paused = False
|
||||
|
||||
# Should not raise when no session exists
|
||||
formatter.pause_live_updates()
|
||||
formatter.resume_live_updates()
|
||||
|
||||
assert formatter._streaming_live is None
|
||||
assert not formatter._live_paused
|
||||
finally:
|
||||
formatter._streaming_live = original_streaming_live
|
||||
formatter._live = original_live
|
||||
formatter._live_paused = original_paused_state
|
||||
|
||||
def test_pause_resume_exception_handling(self):
|
||||
"""Test that resume is called even if exception occurs during human input."""
|
||||
@@ -114,18 +131,23 @@ class TestFlowHumanInputIntegration:
|
||||
|
||||
formatter = event_listener.formatter
|
||||
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates") as mock_pause,
|
||||
patch.object(formatter, "resume_live_updates") as mock_resume,
|
||||
patch(
|
||||
"builtins.input", side_effect=KeyboardInterrupt("Test exception")
|
||||
),
|
||||
):
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
executor._ask_human_input("Test result")
|
||||
original_paused_state = formatter._live_paused
|
||||
|
||||
mock_pause.assert_called_once()
|
||||
mock_resume.assert_called_once()
|
||||
try:
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates") as mock_pause,
|
||||
patch.object(formatter, "resume_live_updates") as mock_resume,
|
||||
patch(
|
||||
"builtins.input", side_effect=KeyboardInterrupt("Test exception")
|
||||
),
|
||||
):
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
executor._ask_human_input("Test result")
|
||||
|
||||
mock_pause.assert_called_once()
|
||||
mock_resume.assert_called_once()
|
||||
finally:
|
||||
formatter._live_paused = original_paused_state
|
||||
|
||||
def test_training_mode_human_input(self):
|
||||
"""Test human input in training mode."""
|
||||
@@ -140,25 +162,28 @@ class TestFlowHumanInputIntegration:
|
||||
|
||||
formatter = event_listener.formatter
|
||||
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates") as mock_pause,
|
||||
patch.object(formatter, "resume_live_updates") as mock_resume,
|
||||
patch.object(formatter.console, "print") as mock_console_print,
|
||||
patch("builtins.input", return_value="training feedback"),
|
||||
):
|
||||
result = executor._ask_human_input("Test result")
|
||||
original_paused_state = formatter._live_paused
|
||||
|
||||
mock_pause.assert_called_once()
|
||||
mock_resume.assert_called_once()
|
||||
assert result == "training feedback"
|
||||
try:
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates") as mock_pause,
|
||||
patch.object(formatter, "resume_live_updates") as mock_resume,
|
||||
patch("builtins.input", return_value="training feedback"),
|
||||
):
|
||||
result = executor._ask_human_input("Test result")
|
||||
|
||||
# Verify the training panel was printed via formatter's console
|
||||
mock_console_print.assert_called()
|
||||
# Check that a Panel with training title was printed
|
||||
call_args = mock_console_print.call_args_list
|
||||
training_panel_found = any(
|
||||
hasattr(call[0][0], "title") and "Training" in str(call[0][0].title)
|
||||
for call in call_args
|
||||
if call[0]
|
||||
)
|
||||
assert training_panel_found
|
||||
mock_pause.assert_called_once()
|
||||
mock_resume.assert_called_once()
|
||||
assert result == "training feedback"
|
||||
|
||||
executor._printer.print.assert_called()
|
||||
call_args = [
|
||||
call[1]["content"]
|
||||
for call in executor._printer.print.call_args_list
|
||||
]
|
||||
training_prompt_found = any(
|
||||
"TRAINING MODE" in content for content in call_args
|
||||
)
|
||||
assert training_prompt_found
|
||||
finally:
|
||||
formatter._live_paused = original_paused_state
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
"""Unit tests for the @human_feedback decorator.
|
||||
|
||||
This module tests the @human_feedback decorator's validation logic,
|
||||
async support, and attribute preservation functionality.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.flow import Flow, human_feedback, listen, start
|
||||
from crewai.flow.human_feedback import (
|
||||
HumanFeedbackConfig,
|
||||
HumanFeedbackResult,
|
||||
)
|
||||
|
||||
|
||||
class TestHumanFeedbackValidation:
|
||||
"""Tests for decorator parameter validation."""
|
||||
|
||||
def test_emit_requires_llm(self):
|
||||
"""Test that specifying emit without llm raises ValueError."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
emit=["approve", "reject"],
|
||||
# llm not provided
|
||||
)
|
||||
def test_method(self):
|
||||
return "output"
|
||||
|
||||
assert "llm is required" in str(exc_info.value)
|
||||
|
||||
def test_default_outcome_requires_emit(self):
|
||||
"""Test that specifying default_outcome without emit raises ValueError."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
default_outcome="approve",
|
||||
# emit not provided
|
||||
)
|
||||
def test_method(self):
|
||||
return "output"
|
||||
|
||||
assert "requires emit" in str(exc_info.value)
|
||||
|
||||
def test_default_outcome_must_be_in_emit(self):
|
||||
"""Test that default_outcome must be one of the emit values."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
emit=["approve", "reject"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="invalid_outcome",
|
||||
)
|
||||
def test_method(self):
|
||||
return "output"
|
||||
|
||||
assert "must be one of" in str(exc_info.value)
|
||||
|
||||
def test_valid_configuration_with_routing(self):
|
||||
"""Test that valid configuration with routing doesn't raise."""
|
||||
|
||||
@human_feedback(
|
||||
message="Review this:",
|
||||
emit=["approve", "reject"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="reject",
|
||||
)
|
||||
def test_method(self):
|
||||
return "output"
|
||||
|
||||
# Should not raise
|
||||
assert hasattr(test_method, "__human_feedback_config__")
|
||||
assert test_method.__is_router__ is True
|
||||
assert test_method.__router_paths__ == ["approve", "reject"]
|
||||
|
||||
def test_valid_configuration_without_routing(self):
|
||||
"""Test that valid configuration without routing doesn't raise."""
|
||||
|
||||
@human_feedback(message="Review this:")
|
||||
def test_method(self):
|
||||
return "output"
|
||||
|
||||
# Should not raise
|
||||
assert hasattr(test_method, "__human_feedback_config__")
|
||||
assert not hasattr(test_method, "__is_router__") or not test_method.__is_router__
|
||||
|
||||
|
||||
class TestHumanFeedbackConfig:
|
||||
"""Tests for HumanFeedbackConfig dataclass."""
|
||||
|
||||
def test_config_creation(self):
|
||||
"""Test HumanFeedbackConfig can be created with all parameters."""
|
||||
config = HumanFeedbackConfig(
|
||||
message="Test message",
|
||||
emit=["a", "b"],
|
||||
llm="gpt-4",
|
||||
default_outcome="a",
|
||||
metadata={"key": "value"},
|
||||
)
|
||||
|
||||
assert config.message == "Test message"
|
||||
assert config.emit == ["a", "b"]
|
||||
assert config.llm == "gpt-4"
|
||||
assert config.default_outcome == "a"
|
||||
assert config.metadata == {"key": "value"}
|
||||
|
||||
|
||||
class TestHumanFeedbackResult:
|
||||
"""Tests for HumanFeedbackResult dataclass."""
|
||||
|
||||
def test_result_creation(self):
|
||||
"""Test HumanFeedbackResult can be created with all fields."""
|
||||
result = HumanFeedbackResult(
|
||||
output={"title": "Test"},
|
||||
feedback="Looks good",
|
||||
outcome="approved",
|
||||
method_name="test_method",
|
||||
)
|
||||
|
||||
assert result.output == {"title": "Test"}
|
||||
assert result.feedback == "Looks good"
|
||||
assert result.outcome == "approved"
|
||||
assert result.method_name == "test_method"
|
||||
assert isinstance(result.timestamp, datetime)
|
||||
assert result.metadata == {}
|
||||
|
||||
def test_result_with_metadata(self):
|
||||
"""Test HumanFeedbackResult with custom metadata."""
|
||||
result = HumanFeedbackResult(
|
||||
output="test",
|
||||
feedback="feedback",
|
||||
metadata={"channel": "slack", "user": "test_user"},
|
||||
)
|
||||
|
||||
assert result.metadata == {"channel": "slack", "user": "test_user"}
|
||||
|
||||
|
||||
class TestDecoratorAttributePreservation:
|
||||
"""Tests for preserving Flow decorator attributes."""
|
||||
|
||||
def test_preserves_start_method_attributes(self):
|
||||
"""Test that @human_feedback preserves @start decorator attributes."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review:")
|
||||
def my_start_method(self):
|
||||
return "output"
|
||||
|
||||
# Check that start method attributes are preserved
|
||||
flow = TestFlow()
|
||||
method = flow._methods.get("my_start_method")
|
||||
assert method is not None
|
||||
assert hasattr(method, "__is_start_method__") or "my_start_method" in flow._start_methods
|
||||
|
||||
def test_preserves_listen_method_attributes(self):
|
||||
"""Test that @human_feedback preserves @listen decorator attributes."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
return "start"
|
||||
|
||||
@listen("begin")
|
||||
@human_feedback(message="Review:")
|
||||
def review(self):
|
||||
return "review output"
|
||||
|
||||
flow = TestFlow()
|
||||
# The method should be registered as a listener
|
||||
assert "review" in flow._listeners or any(
|
||||
"review" in str(v) for v in flow._listeners.values()
|
||||
)
|
||||
|
||||
def test_sets_router_attributes_when_emit_specified(self):
|
||||
"""Test that router attributes are set when emit is specified."""
|
||||
|
||||
# Test the decorator directly without @start wrapping
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def review_method(self):
|
||||
return "output"
|
||||
|
||||
assert review_method.__is_router__ is True
|
||||
assert review_method.__router_paths__ == ["approved", "rejected"]
|
||||
|
||||
|
||||
class TestAsyncSupport:
|
||||
"""Tests for async method support."""
|
||||
|
||||
def test_async_method_detection(self):
|
||||
"""Test that async methods are properly detected and wrapped."""
|
||||
|
||||
@human_feedback(message="Review:")
|
||||
async def async_method(self):
|
||||
return "async output"
|
||||
|
||||
assert asyncio.iscoroutinefunction(async_method)
|
||||
|
||||
def test_sync_method_remains_sync(self):
|
||||
"""Test that sync methods remain synchronous."""
|
||||
|
||||
@human_feedback(message="Review:")
|
||||
def sync_method(self):
|
||||
return "sync output"
|
||||
|
||||
assert not asyncio.iscoroutinefunction(sync_method)
|
||||
|
||||
|
||||
class TestHumanFeedbackExecution:
|
||||
"""Tests for actual human feedback execution."""
|
||||
|
||||
@patch("builtins.input", return_value="This looks great!")
|
||||
@patch("builtins.print")
|
||||
def test_basic_feedback_collection(self, mock_print, mock_input):
|
||||
"""Test basic feedback collection without routing."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Please review:")
|
||||
def generate(self):
|
||||
return "Generated content"
|
||||
|
||||
flow = TestFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="Great job!"):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.output == "Generated content"
|
||||
assert flow.last_human_feedback.feedback == "Great job!"
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_empty_feedback_with_default_outcome(self, mock_print, mock_input):
|
||||
"""Test empty feedback uses default_outcome."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "needs_work"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_work",
|
||||
)
|
||||
def review(self):
|
||||
return "Content"
|
||||
|
||||
flow = TestFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "needs_work"
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.outcome == "needs_work"
|
||||
|
||||
@patch("builtins.input", return_value="Approved!")
|
||||
@patch("builtins.print")
|
||||
def test_feedback_collapsing(self, mock_print, mock_input):
|
||||
"""Test that feedback is collapsed to an outcome."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def review(self):
|
||||
return "Content"
|
||||
|
||||
flow = TestFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="Looks great, approved!"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "approved"
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
|
||||
class TestHumanFeedbackHistory:
|
||||
"""Tests for human feedback history tracking."""
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
def test_history_accumulates(self, mock_print, mock_input):
|
||||
"""Test that multiple feedbacks are stored in history."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review step 1:")
|
||||
def step1(self):
|
||||
return "Step 1 output"
|
||||
|
||||
@listen(step1)
|
||||
@human_feedback(message="Review step 2:")
|
||||
def step2(self, prev):
|
||||
return "Step 2 output"
|
||||
|
||||
flow = TestFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="feedback"):
|
||||
flow.kickoff()
|
||||
|
||||
# Both feedbacks should be in history
|
||||
assert len(flow.human_feedback_history) == 2
|
||||
assert flow.human_feedback_history[0].method_name == "step1"
|
||||
assert flow.human_feedback_history[1].method_name == "step2"
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_human_feedback_property_returns_last(self, mock_print, mock_input):
|
||||
"""Test that human_feedback property returns the last result."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review:")
|
||||
def generate(self):
|
||||
return "output"
|
||||
|
||||
flow = TestFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="last feedback"):
|
||||
flow.kickoff()
|
||||
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.feedback == "last feedback"
|
||||
assert flow.last_human_feedback is flow.last_human_feedback
|
||||
|
||||
|
||||
class TestCollapseToOutcome:
|
||||
"""Tests for the _collapse_to_outcome method."""
|
||||
|
||||
def test_exact_match(self):
|
||||
"""Test exact match returns the correct outcome."""
|
||||
flow = Flow()
|
||||
|
||||
with patch("crewai.llm.LLM") as MockLLM:
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.call.return_value = "approved"
|
||||
MockLLM.return_value = mock_llm
|
||||
|
||||
result = flow._collapse_to_outcome(
|
||||
feedback="I approve this",
|
||||
outcomes=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
assert result == "approved"
|
||||
|
||||
def test_partial_match(self):
|
||||
"""Test partial match finds the outcome in the response."""
|
||||
flow = Flow()
|
||||
|
||||
with patch("crewai.llm.LLM") as MockLLM:
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.call.return_value = "The outcome is approved based on the feedback"
|
||||
MockLLM.return_value = mock_llm
|
||||
|
||||
result = flow._collapse_to_outcome(
|
||||
feedback="Looks good",
|
||||
outcomes=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
assert result == "approved"
|
||||
|
||||
def test_fallback_to_first(self):
|
||||
"""Test that unmatched response falls back to first outcome."""
|
||||
flow = Flow()
|
||||
|
||||
with patch("crewai.llm.LLM") as MockLLM:
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.call.return_value = "something completely different"
|
||||
MockLLM.return_value = mock_llm
|
||||
|
||||
result = flow._collapse_to_outcome(
|
||||
feedback="Unclear feedback",
|
||||
outcomes=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
assert result == "approved" # First in list
|
||||
@@ -1,428 +0,0 @@
|
||||
"""Integration tests for the @human_feedback decorator with Flow.
|
||||
|
||||
This module tests the integration of @human_feedback with @listen,
|
||||
routing behavior, multi-step flows, and state management.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.flow import Flow, HumanFeedbackResult, human_feedback, listen, start
|
||||
from crewai.flow.flow import FlowState
|
||||
|
||||
|
||||
class TestRoutingIntegration:
|
||||
"""Tests for routing integration with @listen decorators."""
|
||||
|
||||
@patch("builtins.input", return_value="I approve")
|
||||
@patch("builtins.print")
|
||||
def test_routes_to_matching_listener(self, mock_print, mock_input):
|
||||
"""Test that collapsed outcome routes to the matching @listen method."""
|
||||
execution_order = []
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def generate(self):
|
||||
execution_order.append("generate")
|
||||
return "content"
|
||||
|
||||
@listen("approved")
|
||||
def on_approved(self):
|
||||
execution_order.append("on_approved")
|
||||
return "published"
|
||||
|
||||
@listen("rejected")
|
||||
def on_rejected(self):
|
||||
execution_order.append("on_rejected")
|
||||
return "discarded"
|
||||
|
||||
flow = ReviewFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="Approved!"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert "generate" in execution_order
|
||||
assert "on_approved" in execution_order
|
||||
assert "on_rejected" not in execution_order
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_default_outcome_routes_correctly(self, mock_print, mock_input):
|
||||
"""Test that default_outcome routes when no feedback provided."""
|
||||
executed_listener = []
|
||||
|
||||
class ReviewFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "needs_work"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="needs_work",
|
||||
)
|
||||
def generate(self):
|
||||
return "content"
|
||||
|
||||
@listen("approved")
|
||||
def on_approved(self):
|
||||
executed_listener.append("approved")
|
||||
|
||||
@listen("needs_work")
|
||||
def on_needs_work(self):
|
||||
executed_listener.append("needs_work")
|
||||
|
||||
flow = ReviewFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
flow.kickoff()
|
||||
|
||||
assert "needs_work" in executed_listener
|
||||
assert "approved" not in executed_listener
|
||||
|
||||
|
||||
class TestMultiStepFlows:
|
||||
"""Tests for multi-step flows with multiple @human_feedback decorators."""
|
||||
|
||||
@patch("builtins.input", side_effect=["Good draft", "Final approved"])
|
||||
@patch("builtins.print")
|
||||
def test_multiple_feedback_steps(self, mock_print, mock_input):
|
||||
"""Test a flow with multiple human feedback steps."""
|
||||
|
||||
class MultiStepFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review draft:")
|
||||
def draft(self):
|
||||
return "Draft content"
|
||||
|
||||
@listen(draft)
|
||||
@human_feedback(message="Final review:")
|
||||
def final_review(self, prev_result: HumanFeedbackResult):
|
||||
return f"Final content based on: {prev_result.feedback}"
|
||||
|
||||
flow = MultiStepFlow()
|
||||
|
||||
with patch.object(
|
||||
flow, "_request_human_feedback", side_effect=["Good draft", "Approved"]
|
||||
):
|
||||
flow.kickoff()
|
||||
|
||||
# Both feedbacks should be recorded
|
||||
assert len(flow.human_feedback_history) == 2
|
||||
assert flow.human_feedback_history[0].method_name == "draft"
|
||||
assert flow.human_feedback_history[0].feedback == "Good draft"
|
||||
assert flow.human_feedback_history[1].method_name == "final_review"
|
||||
assert flow.human_feedback_history[1].feedback == "Approved"
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
def test_mixed_feedback_and_regular_methods(self, mock_print, mock_input):
|
||||
"""Test flow with both @human_feedback and regular methods."""
|
||||
execution_order = []
|
||||
|
||||
class MixedFlow(Flow):
|
||||
@start()
|
||||
def generate(self):
|
||||
execution_order.append("generate")
|
||||
return "generated"
|
||||
|
||||
@listen(generate)
|
||||
@human_feedback(message="Review:")
|
||||
def review(self):
|
||||
execution_order.append("review")
|
||||
return "reviewed"
|
||||
|
||||
@listen(review)
|
||||
def finalize(self, result):
|
||||
execution_order.append("finalize")
|
||||
return "finalized"
|
||||
|
||||
flow = MixedFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="feedback"):
|
||||
flow.kickoff()
|
||||
|
||||
assert execution_order == ["generate", "review", "finalize"]
|
||||
|
||||
|
||||
class TestStateManagement:
|
||||
"""Tests for state management with human feedback."""
|
||||
|
||||
@patch("builtins.input", return_value="approved")
|
||||
@patch("builtins.print")
|
||||
def test_feedback_available_in_listener(self, mock_print, mock_input):
|
||||
"""Test that feedback is accessible in downstream listeners."""
|
||||
captured_feedback = []
|
||||
|
||||
class StateFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def review(self):
|
||||
return "Content to review"
|
||||
|
||||
@listen("approved")
|
||||
def on_approved(self):
|
||||
# Access the feedback via property
|
||||
captured_feedback.append(self.last_human_feedback)
|
||||
return "done"
|
||||
|
||||
flow = StateFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="Great content!"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
flow.kickoff()
|
||||
|
||||
assert len(captured_feedback) == 1
|
||||
result = captured_feedback[0]
|
||||
assert isinstance(result, HumanFeedbackResult)
|
||||
assert result.output == "Content to review"
|
||||
assert result.feedback == "Great content!"
|
||||
assert result.outcome == "approved"
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_history_preserved_across_steps(self, mock_print, mock_input):
|
||||
"""Test that feedback history is preserved across flow execution."""
|
||||
|
||||
class HistoryFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Step 1:")
|
||||
def step1(self):
|
||||
return "Step 1"
|
||||
|
||||
@listen(step1)
|
||||
@human_feedback(message="Step 2:")
|
||||
def step2(self, result):
|
||||
return "Step 2"
|
||||
|
||||
@listen(step2)
|
||||
def final(self, result):
|
||||
# Access history
|
||||
return len(self.human_feedback_history)
|
||||
|
||||
flow = HistoryFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="feedback"):
|
||||
result = flow.kickoff()
|
||||
|
||||
# Final method should see 2 feedback entries
|
||||
assert result == 2
|
||||
|
||||
|
||||
class TestAsyncFlowIntegration:
|
||||
"""Tests for async flow integration."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_flow_with_human_feedback(self):
|
||||
"""Test that @human_feedback works with async flows."""
|
||||
executed = []
|
||||
|
||||
class AsyncFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review:")
|
||||
async def async_review(self):
|
||||
executed.append("async_review")
|
||||
await asyncio.sleep(0.01) # Simulate async work
|
||||
return "async content"
|
||||
|
||||
flow = AsyncFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="feedback"):
|
||||
await flow.kickoff_async()
|
||||
|
||||
assert "async_review" in executed
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.output == "async content"
|
||||
|
||||
|
||||
class TestWithStructuredState:
|
||||
"""Tests for flows with structured (Pydantic) state."""
|
||||
|
||||
@patch("builtins.input", return_value="approved")
|
||||
@patch("builtins.print")
|
||||
def test_with_pydantic_state(self, mock_print, mock_input):
|
||||
"""Test human feedback with structured Pydantic state."""
|
||||
|
||||
class ReviewState(FlowState):
|
||||
content: str = ""
|
||||
review_count: int = 0
|
||||
|
||||
class StructuredFlow(Flow[ReviewState]):
|
||||
initial_state = ReviewState
|
||||
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def review(self):
|
||||
self.state.content = "Generated content"
|
||||
self.state.review_count += 1
|
||||
return self.state.content
|
||||
|
||||
@listen("approved")
|
||||
def on_approved(self):
|
||||
return f"Approved: {self.state.content}"
|
||||
|
||||
flow = StructuredFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="LGTM"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert flow.state.review_count == 1
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.feedback == "LGTM"
|
||||
|
||||
|
||||
class TestMetadataPassthrough:
|
||||
"""Tests for metadata passthrough functionality."""
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_metadata_included_in_result(self, mock_print, mock_input):
|
||||
"""Test that metadata is passed through to HumanFeedbackResult."""
|
||||
|
||||
class MetadataFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
metadata={"channel": "slack", "priority": "high"},
|
||||
)
|
||||
def review(self):
|
||||
return "content"
|
||||
|
||||
flow = MetadataFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="feedback"):
|
||||
flow.kickoff()
|
||||
|
||||
result = flow.last_human_feedback
|
||||
assert result is not None
|
||||
assert result.metadata == {"channel": "slack", "priority": "high"}
|
||||
|
||||
|
||||
class TestEventEmission:
|
||||
"""Tests for event emission during human feedback."""
|
||||
|
||||
@patch("builtins.input", return_value="test feedback")
|
||||
@patch("builtins.print")
|
||||
def test_events_emitted_on_feedback_request(self, mock_print, mock_input):
|
||||
"""Test that events are emitted when feedback is requested."""
|
||||
from crewai.events.event_listener import event_listener
|
||||
|
||||
class EventFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review:")
|
||||
def review(self):
|
||||
return "content"
|
||||
|
||||
flow = EventFlow()
|
||||
|
||||
# We can't easily capture events in tests, but we can verify
|
||||
# the flow executes without errors
|
||||
with (
|
||||
patch.object(
|
||||
event_listener.formatter, "pause_live_updates", return_value=None
|
||||
),
|
||||
patch.object(
|
||||
event_listener.formatter, "resume_live_updates", return_value=None
|
||||
),
|
||||
):
|
||||
flow.kickoff()
|
||||
|
||||
assert flow.last_human_feedback is not None
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Tests for edge cases and error handling."""
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_empty_feedback_first_outcome_fallback(self, mock_print, mock_input):
|
||||
"""Test that empty feedback without default uses first outcome."""
|
||||
|
||||
class FallbackFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["first", "second", "third"],
|
||||
llm="gpt-4o-mini",
|
||||
# No default_outcome specified
|
||||
)
|
||||
def review(self):
|
||||
return "content"
|
||||
|
||||
flow = FallbackFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "first" # Falls back to first outcome
|
||||
|
||||
@patch("builtins.input", return_value="whitespace only ")
|
||||
@patch("builtins.print")
|
||||
def test_whitespace_only_feedback_treated_as_empty(self, mock_print, mock_input):
|
||||
"""Test that whitespace-only feedback is treated as empty."""
|
||||
|
||||
class WhitespaceFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approve", "reject"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="reject",
|
||||
)
|
||||
def review(self):
|
||||
return "content"
|
||||
|
||||
flow = WhitespaceFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value=" "):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "reject" # Uses default because feedback is empty after strip
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
def test_feedback_result_without_routing(self, mock_print, mock_input):
|
||||
"""Test that HumanFeedbackResult is returned when not routing."""
|
||||
|
||||
class NoRoutingFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(message="Review:")
|
||||
def review(self):
|
||||
return "content"
|
||||
|
||||
flow = NoRoutingFlow()
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="feedback"):
|
||||
result = flow.kickoff()
|
||||
|
||||
# Result should be HumanFeedbackResult when not routing
|
||||
assert isinstance(result, HumanFeedbackResult)
|
||||
assert result.output == "content"
|
||||
assert result.feedback == "feedback"
|
||||
assert result.outcome is None # No routing, no outcome
|
||||
@@ -1,107 +1,116 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
from rich.tree import Tree
|
||||
from rich.live import Live
|
||||
from crewai.events.utils.console_formatter import ConsoleFormatter
|
||||
|
||||
|
||||
class TestConsoleFormatterPauseResume:
|
||||
"""Test ConsoleFormatter pause/resume functionality for HITL features."""
|
||||
"""Test ConsoleFormatter pause/resume functionality."""
|
||||
|
||||
def test_pause_stops_active_streaming_session(self):
|
||||
"""Test pausing stops an active streaming Live session."""
|
||||
def test_pause_live_updates_with_active_session(self):
|
||||
"""Test pausing when Live session is active."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
mock_live = MagicMock(spec=Live)
|
||||
formatter._streaming_live = mock_live
|
||||
formatter._live = mock_live
|
||||
formatter._live_paused = False
|
||||
|
||||
formatter.pause_live_updates()
|
||||
|
||||
mock_live.stop.assert_called_once()
|
||||
assert formatter._streaming_live is None
|
||||
assert formatter._live_paused
|
||||
|
||||
def test_pause_is_safe_when_no_session(self):
|
||||
"""Test pausing when no streaming session exists doesn't error."""
|
||||
formatter = ConsoleFormatter()
|
||||
formatter._streaming_live = None
|
||||
|
||||
# Should not raise
|
||||
formatter.pause_live_updates()
|
||||
|
||||
assert formatter._streaming_live is None
|
||||
|
||||
def test_multiple_pauses_are_safe(self):
|
||||
"""Test calling pause multiple times is safe."""
|
||||
def test_pause_live_updates_when_already_paused(self):
|
||||
"""Test pausing when already paused does nothing."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
mock_live = MagicMock(spec=Live)
|
||||
formatter._streaming_live = mock_live
|
||||
formatter._live = mock_live
|
||||
formatter._live_paused = True
|
||||
|
||||
formatter.pause_live_updates()
|
||||
mock_live.stop.assert_called_once()
|
||||
assert formatter._streaming_live is None
|
||||
|
||||
# Second pause should not error (no session to stop)
|
||||
formatter.pause_live_updates()
|
||||
mock_live.stop.assert_not_called()
|
||||
assert formatter._live_paused
|
||||
|
||||
def test_resume_is_safe(self):
|
||||
"""Test resume method exists and doesn't error."""
|
||||
def test_pause_live_updates_with_no_session(self):
|
||||
"""Test pausing when no Live session exists."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
# Should not raise
|
||||
formatter.resume_live_updates()
|
||||
formatter._live = None
|
||||
formatter._live_paused = False
|
||||
|
||||
def test_streaming_after_pause_resume_creates_new_session(self):
|
||||
"""Test that streaming after pause/resume creates new Live session."""
|
||||
formatter = ConsoleFormatter()
|
||||
formatter.verbose = True
|
||||
|
||||
# Simulate having an active session
|
||||
mock_live = MagicMock(spec=Live)
|
||||
formatter._streaming_live = mock_live
|
||||
|
||||
# Pause stops the session
|
||||
formatter.pause_live_updates()
|
||||
assert formatter._streaming_live is None
|
||||
|
||||
# Resume (no-op, sessions created on demand)
|
||||
assert formatter._live_paused
|
||||
|
||||
def test_resume_live_updates_when_paused(self):
|
||||
"""Test resuming when paused."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
formatter._live_paused = True
|
||||
|
||||
formatter.resume_live_updates()
|
||||
|
||||
# After resume, streaming should be able to start a new session
|
||||
assert not formatter._live_paused
|
||||
|
||||
def test_resume_live_updates_when_not_paused(self):
|
||||
"""Test resuming when not paused does nothing."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
formatter._live_paused = False
|
||||
|
||||
formatter.resume_live_updates()
|
||||
|
||||
assert not formatter._live_paused
|
||||
|
||||
def test_print_after_resume_restarts_live_session(self):
|
||||
"""Test that printing a Tree after resume creates new Live session."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
formatter._live_paused = True
|
||||
formatter._live = None
|
||||
|
||||
formatter.resume_live_updates()
|
||||
assert not formatter._live_paused
|
||||
|
||||
tree = Tree("Test")
|
||||
|
||||
with patch("crewai.events.utils.console_formatter.Live") as mock_live_class:
|
||||
mock_live_instance = MagicMock()
|
||||
mock_live_class.return_value = mock_live_instance
|
||||
|
||||
# Simulate streaming chunk (this creates a new Live session)
|
||||
formatter.handle_llm_stream_chunk("test chunk", call_type=None)
|
||||
formatter.print(tree)
|
||||
|
||||
mock_live_class.assert_called_once()
|
||||
mock_live_instance.start.assert_called_once()
|
||||
assert formatter._streaming_live == mock_live_instance
|
||||
assert formatter._live == mock_live_instance
|
||||
|
||||
def test_pause_resume_cycle_with_streaming(self):
|
||||
"""Test full pause/resume cycle during streaming."""
|
||||
def test_multiple_pause_resume_cycles(self):
|
||||
"""Test multiple pause/resume cycles work correctly."""
|
||||
formatter = ConsoleFormatter()
|
||||
formatter.verbose = True
|
||||
|
||||
with patch("crewai.events.utils.console_formatter.Live") as mock_live_class:
|
||||
mock_live_instance = MagicMock()
|
||||
mock_live_class.return_value = mock_live_instance
|
||||
mock_live = MagicMock(spec=Live)
|
||||
formatter._live = mock_live
|
||||
formatter._live_paused = False
|
||||
|
||||
# Start streaming
|
||||
formatter.handle_llm_stream_chunk("chunk 1", call_type=None)
|
||||
assert formatter._streaming_live == mock_live_instance
|
||||
formatter.pause_live_updates()
|
||||
assert formatter._live_paused
|
||||
mock_live.stop.assert_called_once()
|
||||
assert formatter._live is None # Live session should be cleared
|
||||
|
||||
# Pause should stop the session
|
||||
formatter.pause_live_updates()
|
||||
mock_live_instance.stop.assert_called_once()
|
||||
assert formatter._streaming_live is None
|
||||
formatter.resume_live_updates()
|
||||
assert not formatter._live_paused
|
||||
|
||||
# Resume (no-op)
|
||||
formatter.resume_live_updates()
|
||||
formatter.pause_live_updates()
|
||||
assert formatter._live_paused
|
||||
|
||||
# Create a new mock for the next session
|
||||
mock_live_instance_2 = MagicMock()
|
||||
mock_live_class.return_value = mock_live_instance_2
|
||||
formatter.resume_live_updates()
|
||||
assert not formatter._live_paused
|
||||
|
||||
# Streaming again creates new session
|
||||
formatter.handle_llm_stream_chunk("chunk 2", call_type=None)
|
||||
assert formatter._streaming_live == mock_live_instance_2
|
||||
def test_pause_resume_state_initialization(self):
|
||||
"""Test that _live_paused is properly initialized."""
|
||||
formatter = ConsoleFormatter()
|
||||
|
||||
assert hasattr(formatter, "_live_paused")
|
||||
assert not formatter._live_paused
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.7.2"
|
||||
__version__ = "1.7.1"
|
||||
|
||||
Reference in New Issue
Block a user