mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-17 12:58:31 +00:00
Compare commits
48 Commits
git-temapl
...
feat/cli-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a13741b15 | ||
|
|
97b5d6b809 | ||
|
|
4f266980ef | ||
|
|
b87bcd25bb | ||
|
|
4736604b4d | ||
|
|
2ff7dad61e | ||
|
|
33ace9bfe2 | ||
|
|
bcdea6849f | ||
|
|
92ca4ce841 | ||
|
|
27f2682301 | ||
|
|
0c3c4fbe17 | ||
|
|
5f3fa857cc | ||
|
|
8eeca55354 | ||
|
|
b5db79da12 | ||
|
|
156f59760c | ||
|
|
09cba0135e | ||
|
|
50746d5e8b | ||
|
|
bcc050b793 | ||
|
|
d4d7712164 | ||
|
|
414a9ba07e | ||
|
|
3b0286f592 | ||
|
|
5d1f655229 | ||
|
|
f44d3902a4 | ||
|
|
8119edb495 | ||
|
|
17bffb0803 | ||
|
|
cbe139fced | ||
|
|
946d8567fe | ||
|
|
bec9a4941c | ||
|
|
7b5d5bdeef | ||
|
|
a1551bcf2b | ||
|
|
f0e28cd88a | ||
|
|
ff5c55fd54 | ||
|
|
5495825b1d | ||
|
|
6e36f84cc6 | ||
|
|
cddf2d8f7c | ||
|
|
5f17e35c5a | ||
|
|
231a833ad0 | ||
|
|
a870295d42 | ||
|
|
ddda8f6bda | ||
|
|
bf7372fefa | ||
|
|
3451b6fc7a | ||
|
|
dbf2570353 | ||
|
|
d0707fac91 | ||
|
|
35ebdd6022 | ||
|
|
92a77e5cac | ||
|
|
a2922c9ad5 | ||
|
|
9f9b52dd26 | ||
|
|
2482c7ab68 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve CrewAI
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Description**
|
|
||||||
Provide a clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**Steps to Reproduce**
|
|
||||||
Provide a step-by-step process to reproduce the behavior:
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots/Code snippets**
|
|
||||||
If applicable, add screenshots or code snippets to help explain your problem.
|
|
||||||
|
|
||||||
**Environment Details:**
|
|
||||||
- **Operating System**: [e.g., Ubuntu 20.04, macOS Catalina, Windows 10]
|
|
||||||
- **Python Version**: [e.g., 3.8, 3.9, 3.10]
|
|
||||||
- **crewAI Version**: [e.g., 0.30.11]
|
|
||||||
- **crewAI Tools Version**: [e.g., 0.2.6]
|
|
||||||
|
|
||||||
**Logs**
|
|
||||||
Include relevant logs or error messages if applicable.
|
|
||||||
|
|
||||||
**Possible Solution**
|
|
||||||
Have a solution in mind? Please suggest it here, or write "None".
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
24
.github/ISSUE_TEMPLATE/custom.md
vendored
24
.github/ISSUE_TEMPLATE/custom.md
vendored
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
name: Custom issue template
|
|
||||||
about: Describe this issue template's purpose here.
|
|
||||||
title: "[DOCS]"
|
|
||||||
labels: documentation
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Page
|
|
||||||
<!-- Provide a link to the documentation page that needs improvement -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
<!-- Describe what needs to be changed or improved in the documentation -->
|
|
||||||
|
|
||||||
## Suggested Changes
|
|
||||||
<!-- If possible, provide specific suggestions for how to improve the documentation -->
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
<!-- Add any other context about the documentation issue here -->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] I have searched the existing issues to make sure this is not a duplicate
|
|
||||||
- [ ] I have checked the latest version of the documentation to ensure this hasn't been addressed
|
|
||||||
65
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
65
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest a new feature for CrewAI
|
||||||
|
title: "[FEATURE]"
|
||||||
|
labels: ["feature-request"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request!
|
||||||
|
- type: dropdown
|
||||||
|
id: feature-area
|
||||||
|
attributes:
|
||||||
|
label: Feature Area
|
||||||
|
description: Which area of CrewAI does this feature primarily relate to?
|
||||||
|
options:
|
||||||
|
- Core functionality
|
||||||
|
- Agent capabilities
|
||||||
|
- Task management
|
||||||
|
- Integration with external tools
|
||||||
|
- Performance optimization
|
||||||
|
- Documentation
|
||||||
|
- Other (please specify in additional context)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a an existing bug? Please link it here.
|
||||||
|
description: A link to the bug or NA if not related to an existing bug.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context, screenshots, or examples about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: willingness-to-contribute
|
||||||
|
attributes:
|
||||||
|
label: Willingness to Contribute
|
||||||
|
description: Would you be willing to contribute to the implementation of this feature?
|
||||||
|
options:
|
||||||
|
- Yes, I'd be happy to submit a pull request
|
||||||
|
- I could provide more detailed specifications
|
||||||
|
- I can test the feature once it's implemented
|
||||||
|
- No, I'm just suggesting the idea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
6
.github/workflows/mkdocs.yml
vendored
6
.github/workflows/mkdocs.yml
vendored
@@ -1,10 +1,8 @@
|
|||||||
name: Deploy MkDocs
|
name: Deploy MkDocs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
push:
|
types: [published]
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
23
.github/workflows/security-checker.yml
vendored
Normal file
23
.github/workflows/security-checker.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Security Checker
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.11.9"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install bandit
|
||||||
|
|
||||||
|
- name: Run Bandit
|
||||||
|
run: bandit -c pyproject.toml -r src/ -lll
|
||||||
|
|
||||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -24,3 +24,4 @@ jobs:
|
|||||||
stale-pr-message: 'This PR is stale because it has been open for 45 days with no activity.'
|
stale-pr-message: 'This PR is stale because it has been open for 45 days with no activity.'
|
||||||
days-before-pr-stale: 45
|
days-before-pr-stale: 45
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
|
operations-per-run: 1200
|
||||||
|
|||||||
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@@ -11,6 +11,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
142
docs/core-concepts/Cli.md
Normal file
142
docs/core-concepts/Cli.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# CrewAI CLI Documentation
|
||||||
|
|
||||||
|
The CrewAI CLI provides a set of commands to interact with CrewAI, allowing you to create, train, run, and manage crews and pipelines.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To use the CrewAI CLI, make sure you have CrewAI & Poetry installed:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install crewai poetry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
The basic structure of a CrewAI CLI command is:
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai [COMMAND] [OPTIONS] [ARGUMENTS]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### 1. create
|
||||||
|
|
||||||
|
Create a new crew or pipeline.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai create [OPTIONS] TYPE NAME
|
||||||
|
```
|
||||||
|
|
||||||
|
- `TYPE`: Choose between "crew" or "pipeline"
|
||||||
|
- `NAME`: Name of the crew or pipeline
|
||||||
|
- `--router`: (Optional) Create a pipeline with router functionality
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
crewai create crew my_new_crew
|
||||||
|
crewai create pipeline my_new_pipeline --router
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. version
|
||||||
|
|
||||||
|
Show the installed version of CrewAI.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai version [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--tools`: (Optional) Show the installed version of CrewAI tools
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
crewai version
|
||||||
|
crewai version --tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. train
|
||||||
|
|
||||||
|
Train the crew for a specified number of iterations.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai train [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-n, --n_iterations INTEGER`: Number of iterations to train the crew (default: 5)
|
||||||
|
- `-f, --filename TEXT`: Path to a custom file for training (default: "trained_agents_data.pkl")
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
crewai train -n 10 -f my_training_data.pkl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. replay
|
||||||
|
|
||||||
|
Replay the crew execution from a specific task.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai replay [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-t, --task_id TEXT`: Replay the crew from this task ID, including all subsequent tasks
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
crewai replay -t task_123456
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. log_tasks_outputs
|
||||||
|
|
||||||
|
Retrieve your latest crew.kickoff() task outputs.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai log_tasks_outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. reset_memories
|
||||||
|
|
||||||
|
Reset the crew memories (long, short, entity, latest_crew_kickoff_outputs).
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai reset_memories [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-l, --long`: Reset LONG TERM memory
|
||||||
|
- `-s, --short`: Reset SHORT TERM memory
|
||||||
|
- `-e, --entities`: Reset ENTITIES memory
|
||||||
|
- `-k, --kickoff-outputs`: Reset LATEST KICKOFF TASK OUTPUTS
|
||||||
|
- `-a, --all`: Reset ALL memories
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
crewai reset_memories --long --short
|
||||||
|
crewai reset_memories --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. test
|
||||||
|
|
||||||
|
Test the crew and evaluate the results.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai test [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-n, --n_iterations INTEGER`: Number of iterations to test the crew (default: 3)
|
||||||
|
- `-m, --model TEXT`: LLM Model to run the tests on the Crew (default: "gpt-4o-mini")
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
crewai test -n 5 -m gpt-3.5-turbo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. run
|
||||||
|
|
||||||
|
Run the crew.
|
||||||
|
|
||||||
|
```
|
||||||
|
crewai run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
Make sure to run these commands from the directory where your CrewAI project is set up. Some commands may require additional configuration or setup within your project structure.
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Creating a CrewAI Pipeline Project
|
||||||
|
|
||||||
|
Welcome to the comprehensive guide for creating a new CrewAI pipeline project. This document will walk you through the steps to create, customize, and run your CrewAI pipeline project, ensuring you have everything you need to get started.
|
||||||
|
|
||||||
|
To learn more about CrewAI pipelines, visit the [CrewAI documentation](https://docs.crewai.com/core-concepts/Pipeline/).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before getting started with CrewAI pipelines, make sure that you have installed CrewAI via pip:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ pip install crewai crewai-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
The same prerequisites for virtual environments and Code IDEs apply as in regular CrewAI projects.
|
||||||
|
|
||||||
|
## Creating a New Pipeline Project
|
||||||
|
|
||||||
|
To create a new CrewAI pipeline project, you have two options:
|
||||||
|
|
||||||
|
1. For a basic pipeline template:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ crewai create pipeline <project_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. For a pipeline example that includes a router:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ crewai create pipeline --router <project_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
These commands will create a new project folder with the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
<project_name>/
|
||||||
|
├── README.md
|
||||||
|
├── poetry.lock
|
||||||
|
├── pyproject.toml
|
||||||
|
├── src/
|
||||||
|
│ └── <project_name>/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── crews/
|
||||||
|
│ │ ├── crew1/
|
||||||
|
│ │ │ ├── crew1.py
|
||||||
|
│ │ │ └── config/
|
||||||
|
│ │ │ ├── agents.yaml
|
||||||
|
│ │ │ └── tasks.yaml
|
||||||
|
│ │ ├── crew2/
|
||||||
|
│ │ │ ├── crew2.py
|
||||||
|
│ │ │ └── config/
|
||||||
|
│ │ │ ├── agents.yaml
|
||||||
|
│ │ │ └── tasks.yaml
|
||||||
|
│ ├── pipelines/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── pipeline1.py
|
||||||
|
│ │ └── pipeline2.py
|
||||||
|
│ └── tools/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── custom_tool.py
|
||||||
|
└── tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customizing Your Pipeline Project
|
||||||
|
|
||||||
|
To customize your pipeline project, you can:
|
||||||
|
|
||||||
|
1. Modify the crew files in `src/<project_name>/crews/` to define your agents and tasks for each crew.
|
||||||
|
2. Modify the pipeline files in `src/<project_name>/pipelines/` to define your pipeline structure.
|
||||||
|
3. Modify `src/<project_name>/main.py` to set up and run your pipelines.
|
||||||
|
4. Add your environment variables into the `.env` file.
|
||||||
|
|
||||||
|
### Example: Defining a Pipeline
|
||||||
|
|
||||||
|
Here's an example of how to define a pipeline in `src/<project_name>/pipelines/normal_pipeline.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai import Pipeline
|
||||||
|
from crewai.project import PipelineBase
|
||||||
|
from ..crews.normal_crew import NormalCrew
|
||||||
|
|
||||||
|
@PipelineBase
|
||||||
|
class NormalPipeline:
|
||||||
|
def __init__(self):
|
||||||
|
# Initialize crews
|
||||||
|
self.normal_crew = NormalCrew().crew()
|
||||||
|
|
||||||
|
def create_pipeline(self):
|
||||||
|
return Pipeline(
|
||||||
|
stages=[
|
||||||
|
self.normal_crew
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def kickoff(self, inputs):
|
||||||
|
pipeline = self.create_pipeline()
|
||||||
|
results = await pipeline.kickoff(inputs)
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
### Annotations
|
||||||
|
|
||||||
|
The main annotation you'll use for pipelines is `@PipelineBase`. This annotation is used to decorate your pipeline classes, similar to how `@CrewBase` is used for crews.
|
||||||
|
|
||||||
|
## Installing Dependencies
|
||||||
|
|
||||||
|
To install the dependencies for your project, use Poetry:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cd <project_name>
|
||||||
|
$ crewai install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Your Pipeline Project
|
||||||
|
|
||||||
|
To run your pipeline project, use the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ crewai run
|
||||||
|
```
|
||||||
|
|
||||||
|
This will initialize your pipeline and begin task execution as defined in your `main.py` file.
|
||||||
|
|
||||||
|
## Deploying Your Pipeline Project
|
||||||
|
|
||||||
|
Pipelines can be deployed in the same way as regular CrewAI projects. The easiest way is through [CrewAI+](https://www.crewai.com/crewaiplus), where you can deploy your pipeline in a few clicks.
|
||||||
|
|
||||||
|
Remember, when working with pipelines, you're orchestrating multiple crews to work together in a sequence or parallel fashion. This allows for more complex workflows and information processing tasks.
|
||||||
@@ -191,8 +191,7 @@ To install the dependencies for your project, you can use Poetry. First, navigat
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ cd my_project
|
$ cd my_project
|
||||||
$ poetry lock
|
$ crewai install
|
||||||
$ poetry install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will install the dependencies specified in the `pyproject.toml` file.
|
This will install the dependencies specified in the `pyproject.toml` file.
|
||||||
@@ -233,11 +232,6 @@ To run your project, use the following command:
|
|||||||
```shell
|
```shell
|
||||||
$ crewai run
|
$ crewai run
|
||||||
```
|
```
|
||||||
or
|
|
||||||
```shell
|
|
||||||
$ poetry run my_project
|
|
||||||
```
|
|
||||||
|
|
||||||
This will initialize your crew of AI agents and begin task execution as defined in your configuration in the `main.py` file.
|
This will initialize your crew of AI agents and begin task execution as defined in your configuration in the `main.py` file.
|
||||||
|
|
||||||
### Replay Tasks from Latest Crew Kickoff
|
### Replay Tasks from Latest Crew Kickoff
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ There are a couple of different ways you can use HuggingFace to host your LLM.
|
|||||||
|
|
||||||
### Your own HuggingFace endpoint
|
### Your own HuggingFace endpoint
|
||||||
```python
|
```python
|
||||||
from langchain_huggingface import HuggingFaceEndpoint,
|
from langchain_huggingface import HuggingFaceEndpoint
|
||||||
|
|
||||||
llm = HuggingFaceEndpoint(
|
llm = HuggingFaceEndpoint(
|
||||||
repo_id="microsoft/Phi-3-mini-4k-instruct",
|
repo_id="microsoft/Phi-3-mini-4k-instruct",
|
||||||
|
|||||||
@@ -8,13 +8,20 @@ Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By
|
|||||||
<div style="width:25%">
|
<div style="width:25%">
|
||||||
<h2>Getting Started</h2>
|
<h2>Getting Started</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href='./getting-started/Installing-CrewAI'>
|
<li>
|
||||||
|
<a href='./getting-started/Installing-CrewAI'>
|
||||||
Installing CrewAI
|
Installing CrewAI
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href='./getting-started/Start-a-New-CrewAI-Project-Template-Method'>
|
<li>
|
||||||
|
<a href='./getting-started/Start-a-New-CrewAI-Project-Template-Method'>
|
||||||
Start a New CrewAI Project: Template Method
|
Start a New CrewAI Project: Template Method
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='./getting-started/Create-a-New-CrewAI-Pipeline-Template-Method'>
|
||||||
|
Create a New CrewAI Pipeline: Template Method
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
81
docs/tools/SpiderTool.md
Normal file
81
docs/tools/SpiderTool.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# SpiderTool
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Spider](https://spider.cloud/?ref=crewai) is the [fastest](https://github.com/spider-rs/spider/blob/main/benches/BENCHMARKS.md#benchmark-results) open source scraper and crawler that returns LLM-ready data. It converts any website into pure HTML, markdown, metadata or text while enabling you to crawl with custom actions using AI.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To use the Spider API you need to download the [Spider SDK](https://pypi.org/project/spider-client/) and the crewai[tools] SDK too:
|
||||||
|
|
||||||
|
```python
|
||||||
|
pip install spider-client 'crewai[tools]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
This example shows you how you can use the Spider tool to enable your agent to scrape and crawl websites. The data returned from the Spider API is already LLM-ready, so no need to do any cleaning there.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai_tools import SpiderTool
|
||||||
|
|
||||||
|
def main():
|
||||||
|
spider_tool = SpiderTool()
|
||||||
|
|
||||||
|
searcher = Agent(
|
||||||
|
role="Web Research Expert",
|
||||||
|
goal="Find related information from specific URL's",
|
||||||
|
backstory="An expert web researcher that uses the web extremely well",
|
||||||
|
tools=[spider_tool],
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return_metadata = Task(
|
||||||
|
description="Scrape https://spider.cloud with a limit of 1 and enable metadata",
|
||||||
|
expected_output="Metadata and 10 word summary of spider.cloud",
|
||||||
|
agent=searcher
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(
|
||||||
|
agents=[searcher],
|
||||||
|
tasks=[
|
||||||
|
return_metadata,
|
||||||
|
],
|
||||||
|
verbose=2
|
||||||
|
)
|
||||||
|
|
||||||
|
crew.kickoff()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
- `api_key` (string, optional): Specifies Spider API key. If not specified, it looks for `SPIDER_API_KEY` in environment variables.
|
||||||
|
- `params` (object, optional): Optional parameters for the request. Defaults to `{"return_format": "markdown"}` to return the website's content in a format that fits LLMs better.
|
||||||
|
- `request` (string): The request type to perform. Possible values are `http`, `chrome`, and `smart`. Use `smart` to perform an HTTP request by default until JavaScript rendering is needed for the HTML.
|
||||||
|
- `limit` (int): The maximum number of pages allowed to crawl per website. Remove the value or set it to `0` to crawl all pages.
|
||||||
|
- `depth` (int): The crawl limit for maximum depth. If `0`, no limit will be applied.
|
||||||
|
- `cache` (bool): Use HTTP caching for the crawl to speed up repeated runs. Default is `true`.
|
||||||
|
- `budget` (object): Object that has paths with a counter for limiting the amount of pages example `{"*":1}` for only crawling the root page.
|
||||||
|
- `locale` (string): The locale to use for request, example `en-US`.
|
||||||
|
- `cookies` (string): Add HTTP cookies to use for request.
|
||||||
|
- `stealth` (bool): Use stealth mode for headless chrome request to help prevent being blocked. The default is `true` on chrome.
|
||||||
|
- `headers` (object): Forward HTTP headers to use for all request. The object is expected to be a map of key value pairs.
|
||||||
|
- `metadata` (bool): Boolean to store metadata about the pages and content found. This could help improve AI interopt. Defaults to `false` unless you have the website already stored with the configuration enabled.
|
||||||
|
- `viewport` (object): Configure the viewport for chrome. Defaults to `800x600`.
|
||||||
|
- `encoding` (string): The type of encoding to use like `UTF-8`, `SHIFT_JIS`, or etc.
|
||||||
|
- `subdomains` (bool): Allow subdomains to be included. Default is `false`.
|
||||||
|
- `user_agent` (string): Add a custom HTTP user agent to the request. By default this is set to a random agent.
|
||||||
|
- `store_data` (bool): Boolean to determine if storage should be used. If set this takes precedence over `storageless`. Defaults to `false`.
|
||||||
|
- `gpt_config` (object): Use AI to generate actions to perform during the crawl. You can pass an array for the `"prompt"` to chain steps.
|
||||||
|
- `fingerprint` (bool): Use advanced fingerprint for chrome.
|
||||||
|
- `storageless` (bool): Boolean to prevent storing any type of data for the request including storage and AI vectors embedding. Defaults to `false` unless you have the website already stored.
|
||||||
|
- `readability` (bool): Use [readability](https://github.com/mozilla/readability) to pre-process the content for reading. This may drastically improve the content for LLM usage.
|
||||||
|
`return_format` (string): The format to return the data in. Possible values are `markdown`, `raw`, `text`, and `html2text`. Use `raw` to return the default format of the page like HTML etc.
|
||||||
|
- `proxy_enabled` (bool): Enable high performance premium proxies for the request to prevent being blocked at the network level.
|
||||||
|
- `query_selector` (string): The CSS query selector to use when extracting content from the markup.
|
||||||
|
- `full_resources` (bool): Crawl and download all the resources for a website.
|
||||||
|
- `request_timeout` (int): The timeout to use for request. Timeouts can be from `5-60`. The default is `30` seconds.
|
||||||
|
- `run_in_background` (bool): Run the request in the background. Useful if storing data and wanting to trigger crawls to the dashboard. This has no effect if storageless is set.
|
||||||
@@ -129,6 +129,7 @@ nav:
|
|||||||
- Processes: 'core-concepts/Processes.md'
|
- Processes: 'core-concepts/Processes.md'
|
||||||
- Crews: 'core-concepts/Crews.md'
|
- Crews: 'core-concepts/Crews.md'
|
||||||
- Collaboration: 'core-concepts/Collaboration.md'
|
- Collaboration: 'core-concepts/Collaboration.md'
|
||||||
|
- Pipeline: 'core-concepts/Pipeline.md'
|
||||||
- Training: 'core-concepts/Training-Crew.md'
|
- Training: 'core-concepts/Training-Crew.md'
|
||||||
- Memory: 'core-concepts/Memory.md'
|
- Memory: 'core-concepts/Memory.md'
|
||||||
- Planning: 'core-concepts/Planning.md'
|
- Planning: 'core-concepts/Planning.md'
|
||||||
@@ -177,6 +178,7 @@ nav:
|
|||||||
- PG RAG Search: 'tools/PGSearchTool.md'
|
- PG RAG Search: 'tools/PGSearchTool.md'
|
||||||
- Scrape Website: 'tools/ScrapeWebsiteTool.md'
|
- Scrape Website: 'tools/ScrapeWebsiteTool.md'
|
||||||
- Selenium Scraper: 'tools/SeleniumScrapingTool.md'
|
- Selenium Scraper: 'tools/SeleniumScrapingTool.md'
|
||||||
|
- Spider Scraper: 'tools/SpiderTool.md'
|
||||||
- TXT RAG Search: 'tools/TXTSearchTool.md'
|
- TXT RAG Search: 'tools/TXTSearchTool.md'
|
||||||
- Vision Tool: 'tools/VisionTool.md'
|
- Vision Tool: 'tools/VisionTool.md'
|
||||||
- Website RAG Search: 'tools/WebsiteSearchTool.md'
|
- Website RAG Search: 'tools/WebsiteSearchTool.md'
|
||||||
|
|||||||
174
poetry.lock
generated
174
poetry.lock
generated
@@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agentops"
|
name = "agentops"
|
||||||
@@ -253,6 +253,24 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
|
|||||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "auth0-python"
|
||||||
|
version = "4.7.1"
|
||||||
|
description = ""
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "auth0_python-4.7.1-py3-none-any.whl", hash = "sha256:5bdbefd582171f398c2b686a19fb5e241a2fa267929519a0c02e33e5932fa7b8"},
|
||||||
|
{file = "auth0_python-4.7.1.tar.gz", hash = "sha256:5cf8be11aa807d54e19271a990eb92bea1863824e4863c7fc8493c6f15a597f1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiohttp = ">=3.8.5,<4.0.0"
|
||||||
|
cryptography = ">=42.0.4,<43.0.0"
|
||||||
|
pyjwt = ">=2.8.0,<3.0.0"
|
||||||
|
requests = ">=2.31.0,<3.0.0"
|
||||||
|
urllib3 = ">=2.0.7,<3.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autoflake"
|
name = "autoflake"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -829,29 +847,81 @@ name = "crewai-tools"
|
|||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
description = "Set of tools for the crewAI framework"
|
description = "Set of tools for the crewAI framework"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10,<=3.13"
|
python-versions = "<=3.13,>=3.10"
|
||||||
files = []
|
files = [
|
||||||
develop = false
|
{file = "crewai_tools-0.8.3-py3-none-any.whl", hash = "sha256:a54a10c36b8403250e13d6594bd37db7e7deb3f9fabc77e8720c081864ae6189"},
|
||||||
|
{file = "crewai_tools-0.8.3.tar.gz", hash = "sha256:f0317ea1d926221b22fcf4b816d71916fe870aa66ed7ee2a0067dba42b5634eb"},
|
||||||
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
beautifulsoup4 = "^4.12.3"
|
beautifulsoup4 = ">=4.12.3,<5.0.0"
|
||||||
chromadb = "^0.4.22"
|
chromadb = ">=0.4.22,<0.5.0"
|
||||||
docker = "^7.1.0"
|
docker = ">=7.1.0,<8.0.0"
|
||||||
docx2txt = "^0.8"
|
docx2txt = ">=0.8,<0.9"
|
||||||
embedchain = "^0.1.114"
|
embedchain = ">=0.1.114,<0.2.0"
|
||||||
lancedb = "^0.5.4"
|
lancedb = ">=0.5.4,<0.6.0"
|
||||||
langchain = ">0.2,<=0.3"
|
langchain = ">0.2,<=0.3"
|
||||||
openai = "^1.12.0"
|
openai = ">=1.12.0,<2.0.0"
|
||||||
pydantic = "^2.6.1"
|
pydantic = ">=2.6.1,<3.0.0"
|
||||||
pyright = "^1.1.350"
|
pyright = ">=1.1.350,<2.0.0"
|
||||||
pytest = "^8.0.0"
|
pytest = ">=8.0.0,<9.0.0"
|
||||||
pytube = "^15.0.0"
|
pytube = ">=15.0.0,<16.0.0"
|
||||||
requests = "^2.31.0"
|
requests = ">=2.31.0,<3.0.0"
|
||||||
selenium = "^4.18.1"
|
selenium = ">=4.18.1,<5.0.0"
|
||||||
|
|
||||||
[package.source]
|
[[package]]
|
||||||
type = "directory"
|
name = "cryptography"
|
||||||
url = "../crewai-tools"
|
version = "42.0.8"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
|
||||||
|
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
|
||||||
|
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
|
||||||
|
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
|
||||||
|
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
|
||||||
|
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
|
||||||
|
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
|
||||||
|
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
|
||||||
|
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
|
||||||
|
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
|
||||||
|
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
|
||||||
|
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
||||||
|
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||||
|
nox = ["nox"]
|
||||||
|
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||||
|
sdist = ["build"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cssselect2"
|
name = "cssselect2"
|
||||||
@@ -1321,12 +1391,12 @@ files = [
|
|||||||
google-auth = ">=2.14.1,<3.0.dev0"
|
google-auth = ">=2.14.1,<3.0.dev0"
|
||||||
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
|
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
|
||||||
grpcio = [
|
grpcio = [
|
||||||
{version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
|
||||||
{version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
|
{version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
|
||||||
|
{version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||||
]
|
]
|
||||||
grpcio-status = [
|
grpcio-status = [
|
||||||
{version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
|
||||||
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
|
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
|
||||||
|
{version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||||
]
|
]
|
||||||
proto-plus = ">=1.22.3,<2.0.0dev"
|
proto-plus = ">=1.22.3,<2.0.0dev"
|
||||||
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
|
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
|
||||||
@@ -3628,8 +3698,8 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
numpy = [
|
numpy = [
|
||||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
|
||||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||||
|
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||||
]
|
]
|
||||||
python-dateutil = ">=2.8.2"
|
python-dateutil = ">=2.8.2"
|
||||||
@@ -4027,6 +4097,19 @@ files = [
|
|||||||
{file = "pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7"},
|
{file = "pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7"},
|
||||||
{file = "pyarrow-17.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204"},
|
{file = "pyarrow-17.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204"},
|
||||||
{file = "pyarrow-17.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8"},
|
{file = "pyarrow-17.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8"},
|
||||||
|
{file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155"},
|
||||||
|
{file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145"},
|
||||||
|
{file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c"},
|
||||||
|
{file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c"},
|
||||||
|
{file = "pyarrow-17.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda"},
|
||||||
|
{file = "pyarrow-17.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204"},
|
||||||
|
{file = "pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -4219,6 +4302,23 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
windows-terminal = ["colorama (>=0.4.6)"]
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.9.0"
|
||||||
|
description = "JSON Web Token implementation in Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
|
||||||
|
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
crypto = ["cryptography (>=3.4.0)"]
|
||||||
|
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||||
|
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||||
|
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pylance"
|
name = "pylance"
|
||||||
version = "0.9.18"
|
version = "0.9.18"
|
||||||
@@ -5467,22 +5567,23 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "1.26.19"
|
version = "2.2.2"
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"},
|
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||||
{file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"},
|
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""}
|
pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
|
h2 = ["h2 (>=4,<5)"]
|
||||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
@@ -5556,23 +5657,20 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcrpy"
|
name = "vcrpy"
|
||||||
version = "6.0.1"
|
version = "5.1.0"
|
||||||
description = "Automatically mock your HTTP interactions to simplify and speed up testing"
|
description = "Automatically mock your HTTP interactions to simplify and speed up testing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278"},
|
{file = "vcrpy-5.1.0-py2.py3-none-any.whl", hash = "sha256:605e7b7a63dcd940db1df3ab2697ca7faf0e835c0852882142bafb19649d599e"},
|
||||||
|
{file = "vcrpy-5.1.0.tar.gz", hash = "sha256:bbf1532f2618a04f11bce2a99af3a9647a32c880957293ff91e0a5f187b6b3d2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
PyYAML = "*"
|
PyYAML = "*"
|
||||||
urllib3 = {version = "<2", markers = "platform_python_implementation == \"PyPy\""}
|
|
||||||
wrapt = "*"
|
wrapt = "*"
|
||||||
yarl = "*"
|
yarl = "*"
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
tests = ["Werkzeug (==2.0.3)", "aiohttp", "boto3", "httplib2", "httpx", "pytest", "pytest-aiohttp", "pytest-asyncio", "pytest-cov", "pytest-httpbin", "requests (>=2.22.0)", "tornado", "urllib3"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "20.26.3"
|
version = "20.26.3"
|
||||||
@@ -6062,4 +6160,4 @@ tools = ["crewai-tools"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<=3.13"
|
python-versions = ">=3.10,<=3.13"
|
||||||
content-hash = "fc1b510ea9c814db67ac69d2454071b718cb7f6846bd845f7f48561cb0397ce1"
|
content-hash = "8327a37f807d35d0851e9cc46960e8df0d06924938b2c5354b09951fa54f15e3"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ jsonref = "^1.1.0"
|
|||||||
agentops = { version = "^0.3.0", optional = true }
|
agentops = { version = "^0.3.0", optional = true }
|
||||||
embedchain = "^0.1.114"
|
embedchain = "^0.1.114"
|
||||||
json-repair = "^0.25.2"
|
json-repair = "^0.25.2"
|
||||||
|
auth0-python = "^4.7.1"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
tools = ["crewai-tools"]
|
tools = ["crewai-tools"]
|
||||||
@@ -62,6 +63,9 @@ ignore_missing_imports = true
|
|||||||
disable_error_code = 'import-untyped'
|
disable_error_code = 'import-untyped'
|
||||||
exclude = ["cli/templates"]
|
exclude = ["cli/templates"]
|
||||||
|
|
||||||
|
[tool.bandit]
|
||||||
|
exclude_dirs = ["src/crewai/cli/templates"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@@ -113,40 +113,41 @@ class Agent(BaseAgent):
|
|||||||
description="Maximum number of retries for an agent to execute a task when an error occurs.",
|
description="Maximum number of retries for an agent to execute a task when an error occurs.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(__pydantic_self__, **data):
|
|
||||||
config = data.pop("config", {})
|
|
||||||
super().__init__(**config, **data)
|
|
||||||
__pydantic_self__.agent_ops_agent_name = __pydantic_self__.role
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def set_agent_executor(self) -> "Agent":
|
def post_init_setup(self):
|
||||||
"""Ensure agent executor and token process are set."""
|
self.agent_ops_agent_name = self.role
|
||||||
|
|
||||||
if hasattr(self.llm, "model_name"):
|
if hasattr(self.llm, "model_name"):
|
||||||
token_handler = TokenCalcHandler(self.llm.model_name, self._token_process)
|
self._setup_llm_callbacks()
|
||||||
|
|
||||||
# Ensure self.llm.callbacks is a list
|
|
||||||
if not isinstance(self.llm.callbacks, list):
|
|
||||||
self.llm.callbacks = []
|
|
||||||
|
|
||||||
# Check if an instance of TokenCalcHandler already exists in the list
|
|
||||||
if not any(
|
|
||||||
isinstance(handler, TokenCalcHandler) for handler in self.llm.callbacks
|
|
||||||
):
|
|
||||||
self.llm.callbacks.append(token_handler)
|
|
||||||
|
|
||||||
if agentops and not any(
|
|
||||||
isinstance(handler, agentops.LangchainCallbackHandler)
|
|
||||||
for handler in self.llm.callbacks
|
|
||||||
):
|
|
||||||
agentops.stop_instrumenting()
|
|
||||||
self.llm.callbacks.append(agentops.LangchainCallbackHandler())
|
|
||||||
|
|
||||||
if not self.agent_executor:
|
if not self.agent_executor:
|
||||||
if not self.cache_handler:
|
self._setup_agent_executor()
|
||||||
self.cache_handler = CacheHandler()
|
|
||||||
self.set_cache_handler(self.cache_handler)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def _setup_llm_callbacks(self):
|
||||||
|
token_handler = TokenCalcHandler(self.llm.model_name, self._token_process)
|
||||||
|
|
||||||
|
if not isinstance(self.llm.callbacks, list):
|
||||||
|
self.llm.callbacks = []
|
||||||
|
|
||||||
|
if not any(
|
||||||
|
isinstance(handler, TokenCalcHandler) for handler in self.llm.callbacks
|
||||||
|
):
|
||||||
|
self.llm.callbacks.append(token_handler)
|
||||||
|
|
||||||
|
if agentops and not any(
|
||||||
|
isinstance(handler, agentops.LangchainCallbackHandler)
|
||||||
|
for handler in self.llm.callbacks
|
||||||
|
):
|
||||||
|
agentops.stop_instrumenting()
|
||||||
|
self.llm.callbacks.append(agentops.LangchainCallbackHandler())
|
||||||
|
|
||||||
|
def _setup_agent_executor(self):
|
||||||
|
if not self.cache_handler:
|
||||||
|
self.cache_handler = CacheHandler()
|
||||||
|
self.set_cache_handler(self.cache_handler)
|
||||||
|
|
||||||
def execute_task(
|
def execute_task(
|
||||||
self,
|
self,
|
||||||
task: Any,
|
task: Any,
|
||||||
@@ -213,7 +214,7 @@ class Agent(BaseAgent):
|
|||||||
raise e
|
raise e
|
||||||
result = self.execute_task(task, context, tools)
|
result = self.execute_task(task, context, tools)
|
||||||
|
|
||||||
if self.max_rpm:
|
if self.max_rpm and self._rpm_controller:
|
||||||
self._rpm_controller.stop_rpm_counter()
|
self._rpm_controller.stop_rpm_counter()
|
||||||
|
|
||||||
# If there was any tool in self.tools_results that had result_as_answer
|
# If there was any tool in self.tools_results that had result_as_answer
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import Any, Dict, List, Optional, TypeVar
|
|||||||
from pydantic import (
|
from pydantic import (
|
||||||
UUID4,
|
UUID4,
|
||||||
BaseModel,
|
BaseModel,
|
||||||
ConfigDict,
|
|
||||||
Field,
|
Field,
|
||||||
InstanceOf,
|
InstanceOf,
|
||||||
PrivateAttr,
|
PrivateAttr,
|
||||||
@@ -20,6 +19,7 @@ from crewai.agents.agent_builder.utilities.base_token_process import TokenProces
|
|||||||
from crewai.agents.cache.cache_handler import CacheHandler
|
from crewai.agents.cache.cache_handler import CacheHandler
|
||||||
from crewai.agents.tools_handler import ToolsHandler
|
from crewai.agents.tools_handler import ToolsHandler
|
||||||
from crewai.utilities import I18N, Logger, RPMController
|
from crewai.utilities import I18N, Logger, RPMController
|
||||||
|
from crewai.utilities.config import process_config
|
||||||
|
|
||||||
T = TypeVar("T", bound="BaseAgent")
|
T = TypeVar("T", bound="BaseAgent")
|
||||||
|
|
||||||
@@ -74,21 +74,26 @@ class BaseAgent(ABC, BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__hash__ = object.__hash__ # type: ignore
|
__hash__ = object.__hash__ # type: ignore
|
||||||
_logger: Logger = PrivateAttr()
|
_logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=False))
|
||||||
_rpm_controller: RPMController = PrivateAttr(default=None)
|
_rpm_controller: Optional[RPMController] = PrivateAttr(default=None)
|
||||||
_request_within_rpm_limit: Any = PrivateAttr(default=None)
|
_request_within_rpm_limit: Any = PrivateAttr(default=None)
|
||||||
formatting_errors: int = 0
|
_original_role: Optional[str] = PrivateAttr(default=None)
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
_original_goal: Optional[str] = PrivateAttr(default=None)
|
||||||
|
_original_backstory: Optional[str] = PrivateAttr(default=None)
|
||||||
|
_token_process: TokenProcess = PrivateAttr(default_factory=TokenProcess)
|
||||||
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
||||||
|
formatting_errors: int = Field(
|
||||||
|
default=0, description="Number of formatting errors."
|
||||||
|
)
|
||||||
role: str = Field(description="Role of the agent")
|
role: str = Field(description="Role of the agent")
|
||||||
goal: str = Field(description="Objective of the agent")
|
goal: str = Field(description="Objective of the agent")
|
||||||
backstory: str = Field(description="Backstory of the agent")
|
backstory: str = Field(description="Backstory of the agent")
|
||||||
|
config: Optional[Dict[str, Any]] = Field(
|
||||||
|
description="Configuration for the agent", default=None, exclude=True
|
||||||
|
)
|
||||||
cache: bool = Field(
|
cache: bool = Field(
|
||||||
default=True, description="Whether the agent should use a cache for tool usage."
|
default=True, description="Whether the agent should use a cache for tool usage."
|
||||||
)
|
)
|
||||||
config: Optional[Dict[str, Any]] = Field(
|
|
||||||
description="Configuration for the agent", default=None
|
|
||||||
)
|
|
||||||
verbose: bool = Field(
|
verbose: bool = Field(
|
||||||
default=False, description="Verbose mode for the Agent Execution"
|
default=False, description="Verbose mode for the Agent Execution"
|
||||||
)
|
)
|
||||||
@@ -123,20 +128,29 @@ class BaseAgent(ABC, BaseModel):
|
|||||||
default=None, description="Maximum number of tokens for the agent's execution."
|
default=None, description="Maximum number of tokens for the agent's execution."
|
||||||
)
|
)
|
||||||
|
|
||||||
_original_role: str | None = None
|
@model_validator(mode="before")
|
||||||
_original_goal: str | None = None
|
@classmethod
|
||||||
_original_backstory: str | None = None
|
def process_model_config(cls, values):
|
||||||
_token_process: TokenProcess = TokenProcess()
|
return process_config(values, cls)
|
||||||
|
|
||||||
def __init__(__pydantic_self__, **data):
|
|
||||||
config = data.pop("config", {})
|
|
||||||
super().__init__(**config, **data)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def set_config_attributes(self):
|
def validate_and_set_attributes(self):
|
||||||
if self.config:
|
# Validate required fields
|
||||||
for key, value in self.config.items():
|
for field in ["role", "goal", "backstory"]:
|
||||||
setattr(self, key, value)
|
if getattr(self, field) is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"{field} must be provided either directly or through config"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set private attributes
|
||||||
|
self._logger = Logger(verbose=self.verbose)
|
||||||
|
if self.max_rpm and not self._rpm_controller:
|
||||||
|
self._rpm_controller = RPMController(
|
||||||
|
max_rpm=self.max_rpm, logger=self._logger
|
||||||
|
)
|
||||||
|
if not self._token_process:
|
||||||
|
self._token_process = TokenProcess()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@field_validator("id", mode="before")
|
@field_validator("id", mode="before")
|
||||||
@@ -147,14 +161,6 @@ class BaseAgent(ABC, BaseModel):
|
|||||||
"may_not_set_field", "This field is not to be set by the user.", {}
|
"may_not_set_field", "This field is not to be set by the user.", {}
|
||||||
)
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def set_attributes_based_on_config(self) -> "BaseAgent":
|
|
||||||
"""Set attributes based on the agent configuration."""
|
|
||||||
if self.config:
|
|
||||||
for key, value in self.config.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def set_private_attrs(self):
|
def set_private_attrs(self):
|
||||||
"""Set private attributes."""
|
"""Set private attributes."""
|
||||||
@@ -170,7 +176,7 @@ class BaseAgent(ABC, BaseModel):
|
|||||||
@property
|
@property
|
||||||
def key(self):
|
def key(self):
|
||||||
source = [self.role, self.goal, self.backstory]
|
source = [self.role, self.goal, self.backstory]
|
||||||
return md5("|".join(source).encode()).hexdigest()
|
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def execute_task(
|
def execute_task(
|
||||||
|
|||||||
11
src/crewai/agents/cache/cache_handler.py
vendored
11
src/crewai/agents/cache/cache_handler.py
vendored
@@ -1,13 +1,12 @@
|
|||||||
from typing import Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, PrivateAttr
|
||||||
|
|
||||||
|
|
||||||
class CacheHandler:
|
class CacheHandler(BaseModel):
|
||||||
"""Callback handler for tool usage."""
|
"""Callback handler for tool usage."""
|
||||||
|
|
||||||
_cache: dict = {}
|
_cache: Dict[str, Any] = PrivateAttr(default_factory=dict)
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._cache = {}
|
|
||||||
|
|
||||||
def add(self, tool, input, output):
|
def add(self, tool, input, output):
|
||||||
self._cache[f"{tool}-{input}"] = output
|
self._cache[f"{tool}-{input}"] = output
|
||||||
|
|||||||
@@ -1,33 +1,29 @@
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union
|
from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
from langchain.agents import AgentExecutor
|
from langchain.agents import AgentExecutor
|
||||||
from langchain.agents.agent import ExceptionTool
|
from langchain.agents.agent import ExceptionTool
|
||||||
from langchain.callbacks.manager import CallbackManagerForChainRun
|
from langchain.callbacks.manager import CallbackManagerForChainRun
|
||||||
|
from langchain.chains.summarize import load_summarize_chain
|
||||||
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
from langchain_core.agents import AgentAction, AgentFinish, AgentStep
|
from langchain_core.agents import AgentAction, AgentFinish, AgentStep
|
||||||
from langchain_core.exceptions import OutputParserException
|
from langchain_core.exceptions import OutputParserException
|
||||||
from langchain_core.tools import BaseTool
|
from langchain_core.tools import BaseTool
|
||||||
from langchain_core.utils.input import get_color_mapping
|
from langchain_core.utils.input import get_color_mapping
|
||||||
from pydantic import InstanceOf
|
from pydantic import InstanceOf
|
||||||
|
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
|
||||||
from langchain.chains.summarize import load_summarize_chain
|
|
||||||
|
|
||||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||||
from crewai.agents.tools_handler import ToolsHandler
|
from crewai.agents.tools_handler import ToolsHandler
|
||||||
|
|
||||||
|
|
||||||
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
||||||
from crewai.utilities import I18N
|
from crewai.utilities import I18N
|
||||||
from crewai.utilities.constants import TRAINING_DATA_FILE
|
from crewai.utilities.constants import TRAINING_DATA_FILE
|
||||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||||
LLMContextLengthExceededException,
|
LLMContextLengthExceededException,
|
||||||
)
|
)
|
||||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
|
||||||
from crewai.utilities.logger import Logger
|
from crewai.utilities.logger import Logger
|
||||||
|
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||||
|
|
||||||
|
|
||||||
class CrewAgentExecutor(AgentExecutor, CrewAgentExecutorMixin):
|
class CrewAgentExecutor(AgentExecutor, CrewAgentExecutorMixin):
|
||||||
@@ -213,11 +209,7 @@ class CrewAgentExecutor(AgentExecutor, CrewAgentExecutorMixin):
|
|||||||
yield step
|
yield step
|
||||||
return
|
return
|
||||||
|
|
||||||
yield AgentStep(
|
raise e
|
||||||
action=AgentAction("_Exception", str(e), str(e)),
|
|
||||||
observation=str(e),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the tool chosen is the finishing tool, then we end and return.
|
# If the tool chosen is the finishing tool, then we end and return.
|
||||||
if isinstance(output, AgentFinish):
|
if isinstance(output, AgentFinish):
|
||||||
|
|||||||
3
src/crewai/cli/authentication/__init__.py
Normal file
3
src/crewai/cli/authentication/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .main import AuthenticationCommand
|
||||||
|
|
||||||
|
__all__ = ["AuthenticationCommand"]
|
||||||
4
src/crewai/cli/authentication/constants.py
Normal file
4
src/crewai/cli/authentication/constants.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ALGORITHMS = ["RS256"]
|
||||||
|
AUTH0_DOMAIN = "dev-jzsr0j8zs0atl5ha.us.auth0.com"
|
||||||
|
AUTH0_CLIENT_ID = "CZtyRHuVW80HbLSjk4ggXNzjg4KAt7Oe"
|
||||||
|
AUTH0_AUDIENCE = "https://dev-jzsr0j8zs0atl5ha.us.auth0.com/api/v2/"
|
||||||
75
src/crewai/cli/authentication/main.py
Normal file
75
src/crewai/cli/authentication/main.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from .constants import AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN
|
||||||
|
from .utils import TokenManager, validate_token
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationCommand:
|
||||||
|
DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code"
|
||||||
|
TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.token_manager = TokenManager()
|
||||||
|
|
||||||
|
def signup(self) -> None:
|
||||||
|
"""Sign up to CrewAI+"""
|
||||||
|
console.print("Signing Up to CrewAI+ \n", style="bold blue")
|
||||||
|
device_code_data = self._get_device_code()
|
||||||
|
self._display_auth_instructions(device_code_data)
|
||||||
|
|
||||||
|
return self._poll_for_token(device_code_data)
|
||||||
|
|
||||||
|
def _get_device_code(self) -> Dict[str, Any]:
|
||||||
|
"""Get the device code to authenticate the user."""
|
||||||
|
|
||||||
|
device_code_payload = {
|
||||||
|
"client_id": AUTH0_CLIENT_ID,
|
||||||
|
"scope": "openid",
|
||||||
|
"audience": AUTH0_AUDIENCE,
|
||||||
|
}
|
||||||
|
response = requests.post(url=self.DEVICE_CODE_URL, data=device_code_payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def _display_auth_instructions(self, device_code_data: Dict[str, str]) -> None:
|
||||||
|
"""Display the authentication instructions to the user."""
|
||||||
|
console.print("1. Navigate to: ", device_code_data["verification_uri_complete"])
|
||||||
|
console.print("2. Enter the following code: ", device_code_data["user_code"])
|
||||||
|
webbrowser.open(device_code_data["verification_uri_complete"])
|
||||||
|
|
||||||
|
def _poll_for_token(self, device_code_data: Dict[str, Any]) -> None:
|
||||||
|
"""Poll the server for the token."""
|
||||||
|
token_payload = {
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
"device_code": device_code_data["device_code"],
|
||||||
|
"client_id": AUTH0_CLIENT_ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts = 0
|
||||||
|
while True and attempts < 5:
|
||||||
|
response = requests.post(self.TOKEN_URL, data=token_payload)
|
||||||
|
token_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
validate_token(token_data["id_token"])
|
||||||
|
expires_in = 360000 # Token expiration time in seconds
|
||||||
|
self.token_manager.save_tokens(token_data["access_token"], expires_in)
|
||||||
|
console.print("\nWelcome to CrewAI+ !!", style="green")
|
||||||
|
return
|
||||||
|
|
||||||
|
if token_data["error"] not in ("authorization_pending", "slow_down"):
|
||||||
|
raise requests.HTTPError(token_data["error_description"])
|
||||||
|
|
||||||
|
time.sleep(device_code_data["interval"])
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
"Timeout: Failed to get the token. Please try again.", style="bold red"
|
||||||
|
)
|
||||||
144
src/crewai/cli/authentication/utils.py
Normal file
144
src/crewai/cli/authentication/utils.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from auth0.authentication.token_verifier import (
|
||||||
|
AsymmetricSignatureVerifier,
|
||||||
|
TokenVerifier,
|
||||||
|
)
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
def validate_token(id_token: str) -> None:
|
||||||
|
"""
|
||||||
|
Verify the token and its precedence
|
||||||
|
|
||||||
|
:param id_token:
|
||||||
|
"""
|
||||||
|
jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
|
||||||
|
issuer = f"https://{AUTH0_DOMAIN}/"
|
||||||
|
signature_verifier = AsymmetricSignatureVerifier(jwks_url)
|
||||||
|
token_verifier = TokenVerifier(
|
||||||
|
signature_verifier=signature_verifier, issuer=issuer, audience=AUTH0_CLIENT_ID
|
||||||
|
)
|
||||||
|
token_verifier.verify(id_token)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenManager:
|
||||||
|
def __init__(self, file_path: str = "tokens.enc") -> None:
|
||||||
|
"""
|
||||||
|
Initialize the TokenManager class.
|
||||||
|
|
||||||
|
:param file_path: The file path to store the encrypted tokens. Default is "tokens.enc".
|
||||||
|
"""
|
||||||
|
self.file_path = file_path
|
||||||
|
self.key = self._get_or_create_key()
|
||||||
|
self.fernet = Fernet(self.key)
|
||||||
|
|
||||||
|
def _get_or_create_key(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Get or create the encryption key.
|
||||||
|
|
||||||
|
:return: The encryption key.
|
||||||
|
"""
|
||||||
|
key_filename = "secret.key"
|
||||||
|
key = self.read_secure_file(key_filename)
|
||||||
|
|
||||||
|
if key is not None:
|
||||||
|
return key
|
||||||
|
|
||||||
|
new_key = Fernet.generate_key()
|
||||||
|
self.save_secure_file(key_filename, new_key)
|
||||||
|
return new_key
|
||||||
|
|
||||||
|
def save_tokens(self, access_token: str, expires_in: int) -> None:
|
||||||
|
"""
|
||||||
|
Save the access token and its expiration time.
|
||||||
|
|
||||||
|
:param access_token: The access token to save.
|
||||||
|
:param expires_in: The expiration time of the access token in seconds.
|
||||||
|
"""
|
||||||
|
expiration_time = datetime.now() + timedelta(seconds=expires_in)
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"expiration": expiration_time.isoformat(),
|
||||||
|
}
|
||||||
|
encrypted_data = self.fernet.encrypt(json.dumps(data).encode())
|
||||||
|
self.save_secure_file(self.file_path, encrypted_data)
|
||||||
|
|
||||||
|
def get_token(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the access token if it is valid and not expired.
|
||||||
|
|
||||||
|
:return: The access token if valid and not expired, otherwise None.
|
||||||
|
"""
|
||||||
|
encrypted_data = self.read_secure_file(self.file_path)
|
||||||
|
|
||||||
|
decrypted_data = self.fernet.decrypt(encrypted_data)
|
||||||
|
data = json.loads(decrypted_data)
|
||||||
|
|
||||||
|
expiration = datetime.fromisoformat(data["expiration"])
|
||||||
|
if expiration <= datetime.now():
|
||||||
|
return None
|
||||||
|
|
||||||
|
return data["access_token"]
|
||||||
|
|
||||||
|
def get_secure_storage_path(self) -> Path:
|
||||||
|
"""
|
||||||
|
Get the secure storage path based on the operating system.
|
||||||
|
|
||||||
|
:return: The secure storage path.
|
||||||
|
"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Windows: Use %LOCALAPPDATA%
|
||||||
|
base_path = os.environ.get("LOCALAPPDATA")
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
# macOS: Use ~/Library/Application Support
|
||||||
|
base_path = os.path.expanduser("~/Library/Application Support")
|
||||||
|
else:
|
||||||
|
# Linux and other Unix-like: Use ~/.local/share
|
||||||
|
base_path = os.path.expanduser("~/.local/share")
|
||||||
|
|
||||||
|
app_name = "crewai/credentials"
|
||||||
|
storage_path = Path(base_path) / app_name
|
||||||
|
|
||||||
|
storage_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return storage_path
|
||||||
|
|
||||||
|
def save_secure_file(self, filename: str, content: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Save the content to a secure file.
|
||||||
|
|
||||||
|
:param filename: The name of the file.
|
||||||
|
:param content: The content to save.
|
||||||
|
"""
|
||||||
|
storage_path = self.get_secure_storage_path()
|
||||||
|
file_path = storage_path / filename
|
||||||
|
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# Set appropriate permissions (read/write for owner only)
|
||||||
|
os.chmod(file_path, 0o600)
|
||||||
|
|
||||||
|
def read_secure_file(self, filename: str) -> Optional[bytes]:
|
||||||
|
"""
|
||||||
|
Read the content of a secure file.
|
||||||
|
|
||||||
|
:param filename: The name of the file.
|
||||||
|
:return: The content of the file if it exists, otherwise None.
|
||||||
|
"""
|
||||||
|
storage_path = self.get_secure_storage_path()
|
||||||
|
file_path = storage_path / filename
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
return f.read()
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
@@ -7,7 +9,10 @@ from crewai.memory.storage.kickoff_task_outputs_storage import (
|
|||||||
KickoffTaskOutputsSQLiteStorage,
|
KickoffTaskOutputsSQLiteStorage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .authentication.main import AuthenticationCommand
|
||||||
|
from .deploy.main import DeployCommand
|
||||||
from .evaluate_crew import evaluate_crew
|
from .evaluate_crew import evaluate_crew
|
||||||
|
from .install_crew import install_crew
|
||||||
from .replay_from_task import replay_task_command
|
from .replay_from_task import replay_task_command
|
||||||
from .reset_memories_command import reset_memories_command
|
from .reset_memories_command import reset_memories_command
|
||||||
from .run_crew import run_crew
|
from .run_crew import run_crew
|
||||||
@@ -165,12 +170,83 @@ def test(n_iterations: int, model: str):
|
|||||||
evaluate_crew(n_iterations, model)
|
evaluate_crew(n_iterations, model)
|
||||||
|
|
||||||
|
|
||||||
|
@crewai.command()
|
||||||
|
def install():
|
||||||
|
"""Install the Crew."""
|
||||||
|
install_crew()
|
||||||
|
|
||||||
|
|
||||||
@crewai.command()
|
@crewai.command()
|
||||||
def run():
|
def run():
|
||||||
"""Run the crew."""
|
"""Run the Crew."""
|
||||||
click.echo("Running the crew")
|
click.echo("Running the Crew")
|
||||||
run_crew()
|
run_crew()
|
||||||
|
|
||||||
|
|
||||||
|
@crewai.command()
|
||||||
|
def signup():
|
||||||
|
"""Sign Up/Login to CrewAI+."""
|
||||||
|
AuthenticationCommand().signup()
|
||||||
|
|
||||||
|
|
||||||
|
@crewai.command()
|
||||||
|
def login():
|
||||||
|
"""Sign Up/Login to CrewAI+."""
|
||||||
|
AuthenticationCommand().signup()
|
||||||
|
|
||||||
|
|
||||||
|
# DEPLOY CREWAI+ COMMANDS
|
||||||
|
@crewai.group()
|
||||||
|
def deploy():
|
||||||
|
"""Deploy the Crew CLI group."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@deploy.command(name="create")
|
||||||
|
def deploy_create():
|
||||||
|
"""Create a Crew deployment."""
|
||||||
|
deploy_cmd = DeployCommand()
|
||||||
|
deploy_cmd.create_crew()
|
||||||
|
|
||||||
|
|
||||||
|
@deploy.command(name="list")
|
||||||
|
def deploy_list():
|
||||||
|
"""List all deployments."""
|
||||||
|
deploy_cmd = DeployCommand()
|
||||||
|
deploy_cmd.list_crews()
|
||||||
|
|
||||||
|
|
||||||
|
@deploy.command(name="push")
|
||||||
|
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||||
|
def deploy_push(uuid: Optional[str]):
|
||||||
|
"""Deploy the Crew."""
|
||||||
|
deploy_cmd = DeployCommand()
|
||||||
|
deploy_cmd.deploy(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@deploy.command(name="status")
|
||||||
|
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||||
|
def deply_status(uuid: Optional[str]):
|
||||||
|
"""Get the status of a deployment."""
|
||||||
|
deploy_cmd = DeployCommand()
|
||||||
|
deploy_cmd.get_crew_status(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@deploy.command(name="logs")
|
||||||
|
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||||
|
def deploy_logs(uuid: Optional[str]):
|
||||||
|
"""Get the logs of a deployment."""
|
||||||
|
deploy_cmd = DeployCommand()
|
||||||
|
deploy_cmd.get_crew_logs(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@deploy.command(name="remove")
|
||||||
|
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||||
|
def deploy_remove(uuid: Optional[str]):
|
||||||
|
"""Remove a deployment."""
|
||||||
|
deploy_cmd = DeployCommand()
|
||||||
|
deploy_cmd.remove_crew(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
crewai()
|
crewai()
|
||||||
|
|||||||
0
src/crewai/cli/deploy/__init__.py
Normal file
0
src/crewai/cli/deploy/__init__.py
Normal file
63
src/crewai/cli/deploy/api.py
Normal file
63
src/crewai/cli/deploy/api.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from os import getenv
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class CrewAPI:
|
||||||
|
"""
|
||||||
|
CrewAPI class to interact with the crewAI+ API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: str) -> None:
|
||||||
|
self.api_key = api_key
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
self.base_url = getenv(
|
||||||
|
"CREWAI_BASE_URL", "https://dev.crewai.com/crewai_plus/api/v1/crews"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
||||||
|
url = f"{self.base_url}/{endpoint}"
|
||||||
|
return requests.request(method, url, headers=self.headers, **kwargs)
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
def deploy_by_name(self, project_name: str) -> requests.Response:
|
||||||
|
return self._make_request("POST", f"by-name/{project_name}/deploy")
|
||||||
|
|
||||||
|
def deploy_by_uuid(self, uuid: str) -> requests.Response:
|
||||||
|
return self._make_request("POST", f"{uuid}/deploy")
|
||||||
|
|
||||||
|
# Status
|
||||||
|
def status_by_name(self, project_name: str) -> requests.Response:
|
||||||
|
return self._make_request("GET", f"by-name/{project_name}/status")
|
||||||
|
|
||||||
|
def status_by_uuid(self, uuid: str) -> requests.Response:
|
||||||
|
return self._make_request("GET", f"{uuid}/status")
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
def logs_by_name(
|
||||||
|
self, project_name: str, log_type: str = "deployment"
|
||||||
|
) -> requests.Response:
|
||||||
|
return self._make_request("GET", f"by-name/{project_name}/logs/{log_type}")
|
||||||
|
|
||||||
|
def logs_by_uuid(
|
||||||
|
self, uuid: str, log_type: str = "deployment"
|
||||||
|
) -> requests.Response:
|
||||||
|
return self._make_request("GET", f"{uuid}/logs/{log_type}")
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
def delete_by_name(self, project_name: str) -> requests.Response:
|
||||||
|
return self._make_request("DELETE", f"by-name/{project_name}")
|
||||||
|
|
||||||
|
def delete_by_uuid(self, uuid: str) -> requests.Response:
|
||||||
|
return self._make_request("DELETE", f"{uuid}")
|
||||||
|
|
||||||
|
# List
|
||||||
|
def list_crews(self) -> requests.Response:
|
||||||
|
return self._make_request("GET", "")
|
||||||
|
|
||||||
|
# Create
|
||||||
|
def create_crew(self, payload) -> requests.Response:
|
||||||
|
return self._make_request("POST", "", json=payload)
|
||||||
289
src/crewai/cli/deploy/main.py
Normal file
289
src/crewai/cli/deploy/main.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from .api import CrewAPI
|
||||||
|
from .utils import (
|
||||||
|
fetch_and_json_env_file,
|
||||||
|
get_auth_token,
|
||||||
|
get_git_remote_url,
|
||||||
|
get_project_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
class DeployCommand:
|
||||||
|
"""
|
||||||
|
A class to handle deployment-related operations for CrewAI projects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Initialize the DeployCommand with project name and API client.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
access_token = get_auth_token()
|
||||||
|
except Exception:
|
||||||
|
console.print(
|
||||||
|
"Please sign up/login to CrewAI+ before using the CLI.",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
console.print("Run 'crewai signup' to sign up/login.", style="bold green")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
self.project_name = get_project_name()
|
||||||
|
self.client = CrewAPI(api_key=access_token)
|
||||||
|
|
||||||
|
def _handle_error(self, json_response: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Handle and display error messages from API responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_response (Dict[str, Any]): The JSON response containing error information.
|
||||||
|
"""
|
||||||
|
error = json_response.get("error", "Unknown error")
|
||||||
|
message = json_response.get("message", "No message provided")
|
||||||
|
console.print(f"Error: {error}", style="bold red")
|
||||||
|
console.print(f"Message: {message}", style="bold red")
|
||||||
|
|
||||||
|
def _standard_no_param_error_message(self) -> None:
|
||||||
|
"""
|
||||||
|
Display a standard error message when no UUID or project name is available.
|
||||||
|
"""
|
||||||
|
console.print(
|
||||||
|
"No UUID provided, project pyproject.toml not found or with error.",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _display_deployment_info(self, json_response: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Display deployment information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_response (Dict[str, Any]): The deployment information to display.
|
||||||
|
"""
|
||||||
|
console.print("Deploying the crew...\n", style="bold blue")
|
||||||
|
for key, value in json_response.items():
|
||||||
|
console.print(f"{key.title()}: [green]{value}[/green]")
|
||||||
|
console.print("\nTo check the status of the deployment, run:")
|
||||||
|
console.print("crewai deploy status")
|
||||||
|
console.print(" or")
|
||||||
|
console.print(f"crewai deploy status --uuid \"{json_response['uuid']}\"")
|
||||||
|
|
||||||
|
def _display_logs(self, log_messages: List[Dict[str, Any]]) -> None:
|
||||||
|
"""
|
||||||
|
Display log messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_messages (List[Dict[str, Any]]): The log messages to display.
|
||||||
|
"""
|
||||||
|
for log_message in log_messages:
|
||||||
|
console.print(
|
||||||
|
f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def deploy(self, uuid: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Deploy a crew using either UUID or project name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid (Optional[str]): The UUID of the crew to deploy.
|
||||||
|
"""
|
||||||
|
console.print("Starting deployment...", style="bold blue")
|
||||||
|
if uuid:
|
||||||
|
response = self.client.deploy_by_uuid(uuid)
|
||||||
|
elif self.project_name:
|
||||||
|
response = self.client.deploy_by_name(self.project_name)
|
||||||
|
else:
|
||||||
|
self._standard_no_param_error_message()
|
||||||
|
return
|
||||||
|
|
||||||
|
json_response = response.json()
|
||||||
|
if response.status_code == 200:
|
||||||
|
self._display_deployment_info(json_response)
|
||||||
|
else:
|
||||||
|
self._handle_error(json_response)
|
||||||
|
|
||||||
|
def create_crew(self) -> None:
|
||||||
|
"""
|
||||||
|
Create a new crew deployment.
|
||||||
|
"""
|
||||||
|
console.print("Creating deployment...", style="bold blue")
|
||||||
|
env_vars = fetch_and_json_env_file()
|
||||||
|
remote_repo_url = get_git_remote_url()
|
||||||
|
|
||||||
|
self._confirm_input(env_vars, remote_repo_url)
|
||||||
|
payload = self._create_payload(env_vars, remote_repo_url)
|
||||||
|
|
||||||
|
response = self.client.create_crew(payload)
|
||||||
|
if response.status_code == 201:
|
||||||
|
self._display_creation_success(response.json())
|
||||||
|
else:
|
||||||
|
self._handle_error(response.json())
|
||||||
|
|
||||||
|
def _confirm_input(self, env_vars: Dict[str, str], remote_repo_url: str) -> None:
|
||||||
|
"""
|
||||||
|
Confirm input parameters with the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_vars (Dict[str, str]): Environment variables.
|
||||||
|
remote_repo_url (str): Remote repository URL.
|
||||||
|
"""
|
||||||
|
input(f"Press Enter to continue with the following Env vars: {env_vars}")
|
||||||
|
input(
|
||||||
|
f"Press Enter to continue with the following remote repository: {remote_repo_url}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_payload(
|
||||||
|
self,
|
||||||
|
env_vars: Dict[str, str],
|
||||||
|
remote_repo_url: str,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create the payload for crew creation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote_repo_url (str): Remote repository URL.
|
||||||
|
env_vars (Dict[str, str]): Environment variables.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: The payload for crew creation.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"deploy": {
|
||||||
|
"name": self.project_name,
|
||||||
|
"repo_clone_url": remote_repo_url,
|
||||||
|
"env": env_vars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _display_creation_success(self, json_response: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Display success message after crew creation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_response (Dict[str, Any]): The response containing crew information.
|
||||||
|
"""
|
||||||
|
console.print("Deployment created successfully!\n", style="bold green")
|
||||||
|
console.print(
|
||||||
|
f"Name: {self.project_name} ({json_response['uuid']})", style="bold green"
|
||||||
|
)
|
||||||
|
console.print(f"Status: {json_response['status']}", style="bold green")
|
||||||
|
console.print("\nTo (re)deploy the crew, run:")
|
||||||
|
console.print("crewai deploy push")
|
||||||
|
console.print(" or")
|
||||||
|
console.print(f"crewai deploy push --uuid {json_response['uuid']}")
|
||||||
|
|
||||||
|
def list_crews(self) -> None:
|
||||||
|
"""
|
||||||
|
List all available crews.
|
||||||
|
"""
|
||||||
|
console.print("Listing all Crews\n", style="bold blue")
|
||||||
|
|
||||||
|
response = self.client.list_crews()
|
||||||
|
json_response = response.json()
|
||||||
|
if response.status_code == 200:
|
||||||
|
self._display_crews(json_response)
|
||||||
|
else:
|
||||||
|
self._display_no_crews_message()
|
||||||
|
|
||||||
|
def _display_crews(self, crews_data: List[Dict[str, Any]]) -> None:
|
||||||
|
"""
|
||||||
|
Display the list of crews.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crews_data (List[Dict[str, Any]]): List of crew data to display.
|
||||||
|
"""
|
||||||
|
for crew_data in crews_data:
|
||||||
|
console.print(
|
||||||
|
f"- {crew_data['name']} ({crew_data['uuid']}) [blue]{crew_data['status']}[/blue]"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _display_no_crews_message(self) -> None:
|
||||||
|
"""
|
||||||
|
Display a message when no crews are available.
|
||||||
|
"""
|
||||||
|
console.print("You don't have any Crews yet. Let's create one!", style="yellow")
|
||||||
|
console.print(" crewai create crew <crew_name>", style="green")
|
||||||
|
|
||||||
|
def get_crew_status(self, uuid: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Get the status of a crew.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid (Optional[str]): The UUID of the crew to check.
|
||||||
|
"""
|
||||||
|
console.print("Fetching deployment status...", style="bold blue")
|
||||||
|
if uuid:
|
||||||
|
response = self.client.status_by_uuid(uuid)
|
||||||
|
elif self.project_name:
|
||||||
|
response = self.client.status_by_name(self.project_name)
|
||||||
|
else:
|
||||||
|
self._standard_no_param_error_message()
|
||||||
|
return
|
||||||
|
|
||||||
|
json_response = response.json()
|
||||||
|
if response.status_code == 200:
|
||||||
|
self._display_crew_status(json_response)
|
||||||
|
else:
|
||||||
|
self._handle_error(json_response)
|
||||||
|
|
||||||
|
def _display_crew_status(self, status_data: Dict[str, str]) -> None:
|
||||||
|
"""
|
||||||
|
Display the status of a crew.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status_data (Dict[str, str]): The status data to display.
|
||||||
|
"""
|
||||||
|
console.print(f"Name:\t {status_data['name']}")
|
||||||
|
console.print(f"Status:\t {status_data['status']}")
|
||||||
|
|
||||||
|
def get_crew_logs(self, uuid: Optional[str], log_type: str = "deployment") -> None:
|
||||||
|
"""
|
||||||
|
Get logs for a crew.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid (Optional[str]): The UUID of the crew to get logs for.
|
||||||
|
log_type (str): The type of logs to retrieve (default: "deployment").
|
||||||
|
"""
|
||||||
|
console.print(f"Fetching {log_type} logs...", style="bold blue")
|
||||||
|
|
||||||
|
if uuid:
|
||||||
|
response = self.client.logs_by_uuid(uuid, log_type)
|
||||||
|
elif self.project_name:
|
||||||
|
response = self.client.logs_by_name(self.project_name, log_type)
|
||||||
|
else:
|
||||||
|
self._standard_no_param_error_message()
|
||||||
|
return
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
self._display_logs(response.json())
|
||||||
|
else:
|
||||||
|
self._handle_error(response.json())
|
||||||
|
|
||||||
|
def remove_crew(self, uuid: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Remove a crew deployment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid (Optional[str]): The UUID of the crew to remove.
|
||||||
|
"""
|
||||||
|
console.print("Removing deployment...", style="bold blue")
|
||||||
|
|
||||||
|
if uuid:
|
||||||
|
response = self.client.delete_by_uuid(uuid)
|
||||||
|
elif self.project_name:
|
||||||
|
response = self.client.delete_by_name(self.project_name)
|
||||||
|
else:
|
||||||
|
self._standard_no_param_error_message()
|
||||||
|
return
|
||||||
|
|
||||||
|
if response.status_code == 204:
|
||||||
|
console.print(
|
||||||
|
f"Crew '{self.project_name}' removed successfully.", style="green"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print(
|
||||||
|
f"Failed to remove crew '{self.project_name}'", style="bold red"
|
||||||
|
)
|
||||||
117
src/crewai/cli/deploy/utils.py
Normal file
117
src/crewai/cli/deploy/utils.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
from ..authentication.utils import TokenManager
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_remote_url() -> str:
|
||||||
|
"""Get the Git repository's remote URL."""
|
||||||
|
try:
|
||||||
|
# Run the git remote -v command
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "remote", "-v"], capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the output
|
||||||
|
output = result.stdout
|
||||||
|
|
||||||
|
# Parse the output to find the origin URL
|
||||||
|
matches = re.findall(r"origin\s+(.*?)\s+\(fetch\)", output)
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
return matches[0] # Return the first match (origin URL)
|
||||||
|
else:
|
||||||
|
print("No origin remote found.")
|
||||||
|
return "No remote URL found"
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return f"Error running trying to fetch the Git Repository: {e}"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "Git command not found. Make sure Git is installed and in your PATH."
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_name(pyproject_path: str = "pyproject.toml"):
|
||||||
|
"""Get the project name from the pyproject.toml file."""
|
||||||
|
try:
|
||||||
|
# Read the pyproject.toml file
|
||||||
|
with open(pyproject_path, "rb") as f:
|
||||||
|
pyproject_content = tomllib.load(f)
|
||||||
|
|
||||||
|
# Extract the project name
|
||||||
|
project_name = pyproject_content["tool"]["poetry"]["name"]
|
||||||
|
|
||||||
|
if "crewai" not in pyproject_content["tool"]["poetry"]["dependencies"]:
|
||||||
|
raise Exception("crewai is not in the dependencies.")
|
||||||
|
|
||||||
|
return project_name
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: {pyproject_path} not found.")
|
||||||
|
except KeyError:
|
||||||
|
print(f"Error: {pyproject_path} is not a valid pyproject.toml file.")
|
||||||
|
except tomllib.TOMLDecodeError:
|
||||||
|
print(f"Error: {pyproject_path} is not a valid TOML file.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading the pyproject.toml file: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str:
|
||||||
|
"""Get the version number of crewai from the pyproject.toml file."""
|
||||||
|
try:
|
||||||
|
# Read the pyproject.toml file
|
||||||
|
with open("pyproject.toml", "rb") as f:
|
||||||
|
pyproject_content = tomllib.load(f)
|
||||||
|
|
||||||
|
# Extract the version number of crewai
|
||||||
|
crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][
|
||||||
|
"version"
|
||||||
|
]
|
||||||
|
|
||||||
|
return crewai_version
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: {pyproject_path} not found.")
|
||||||
|
except KeyError:
|
||||||
|
print(f"Error: {pyproject_path} is not a valid pyproject.toml file.")
|
||||||
|
except tomllib.TOMLDecodeError:
|
||||||
|
print(f"Error: {pyproject_path} is not a valid TOML file.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading the pyproject.toml file: {e}")
|
||||||
|
|
||||||
|
return "no-version-found"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
|
||||||
|
"""Fetch the environment variables from a .env file and return them as a dictionary."""
|
||||||
|
try:
|
||||||
|
# Read the .env file
|
||||||
|
with open(env_file_path, "r") as f:
|
||||||
|
env_content = f.read()
|
||||||
|
|
||||||
|
# Parse the .env file content to a dictionary
|
||||||
|
env_dict = {}
|
||||||
|
for line in env_content.splitlines():
|
||||||
|
if line.strip() and not line.strip().startswith("#"):
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
env_dict[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return env_dict
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: {env_file_path} not found.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading the .env file: {e}")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_token() -> str:
|
||||||
|
"""Get the authentication token."""
|
||||||
|
access_token = TokenManager().get_token()
|
||||||
|
if not access_token:
|
||||||
|
raise Exception()
|
||||||
|
return access_token
|
||||||
21
src/crewai/cli/install_crew.py
Normal file
21
src/crewai/cli/install_crew.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
def install_crew() -> None:
|
||||||
|
"""
|
||||||
|
Install the crew by running the Poetry command to lock and install.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
subprocess.run(["poetry", "lock"], check=True, capture_output=False, text=True)
|
||||||
|
subprocess.run(
|
||||||
|
["poetry", "install"], check=True, capture_output=False, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
click.echo(f"An error occurred while running the crew: {e}", err=True)
|
||||||
|
click.echo(e.output, err=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||||
@@ -14,12 +14,9 @@ pip install poetry
|
|||||||
|
|
||||||
Next, navigate to your project directory and install the dependencies:
|
Next, navigate to your project directory and install the dependencies:
|
||||||
|
|
||||||
1. First lock the dependencies and then install them:
|
1. First lock the dependencies and install them by using the CLI command:
|
||||||
```bash
|
```bash
|
||||||
poetry lock
|
crewai install
|
||||||
```
|
|
||||||
```bash
|
|
||||||
poetry install
|
|
||||||
```
|
```
|
||||||
### Customizing
|
### Customizing
|
||||||
|
|
||||||
@@ -37,10 +34,6 @@ To kickstart your crew of AI agents and begin task execution, run this from the
|
|||||||
```bash
|
```bash
|
||||||
$ crewai run
|
$ crewai run
|
||||||
```
|
```
|
||||||
or
|
|
||||||
```bash
|
|
||||||
poetry run {{folder_name}}
|
|
||||||
```
|
|
||||||
|
|
||||||
This command initializes the {{name}} Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
This command initializes the {{name}} Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ authors = ["Your Name <you@example.com>"]
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.10,<=3.13"
|
python = ">=3.10,<=3.13"
|
||||||
crewai = { extras = ["tools"], version = "^0.51.0" }
|
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
{{folder_name}} = "{{folder_name}}.main:run"
|
{{folder_name}} = "{{folder_name}}.main:run"
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ pip install poetry
|
|||||||
Next, navigate to your project directory and install the dependencies:
|
Next, navigate to your project directory and install the dependencies:
|
||||||
|
|
||||||
1. First lock the dependencies and then install them:
|
1. First lock the dependencies and then install them:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry lock
|
crewai install
|
||||||
```
|
|
||||||
```bash
|
|
||||||
poetry install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customizing
|
### Customizing
|
||||||
|
|
||||||
**Add your `OPENAI_API_KEY` into the `.env` file**
|
**Add your `OPENAI_API_KEY` into the `.env` file**
|
||||||
@@ -35,7 +34,7 @@ poetry install
|
|||||||
To kickstart your crew of AI agents and begin task execution, run this from the root folder of your project:
|
To kickstart your crew of AI agents and begin task execution, run this from the root folder of your project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry run {{folder_name}}
|
crewai run
|
||||||
```
|
```
|
||||||
|
|
||||||
This command initializes the {{name}} Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
This command initializes the {{name}} Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
||||||
@@ -49,6 +48,7 @@ The {{name}} Crew is composed of multiple AI agents, each with unique roles, goa
|
|||||||
## Support
|
## Support
|
||||||
|
|
||||||
For support, questions, or feedback regarding the {{crew_name}} Crew or crewAI.
|
For support, questions, or feedback regarding the {{crew_name}} Crew or crewAI.
|
||||||
|
|
||||||
- Visit our [documentation](https://docs.crewai.com)
|
- Visit our [documentation](https://docs.crewai.com)
|
||||||
- Reach out to us through our [GitHub repository](https://github.com/joaomdmoura/crewai)
|
- Reach out to us through our [GitHub repository](https://github.com/joaomdmoura/crewai)
|
||||||
- [Join our Discord](https://discord.com/invite/X4JWnZnxPb)
|
- [Join our Discord](https://discord.com/invite/X4JWnZnxPb)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ authors = ["Your Name <you@example.com>"]
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.10,<=3.13"
|
python = ">=3.10,<=3.13"
|
||||||
crewai = { extras = ["tools"], version = "^0.51.0" }
|
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
||||||
asyncio = "*"
|
asyncio = "*"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ Next, navigate to your project directory and install the dependencies:
|
|||||||
|
|
||||||
1. First lock the dependencies and then install them:
|
1. First lock the dependencies and then install them:
|
||||||
```bash
|
```bash
|
||||||
poetry lock
|
crewai install
|
||||||
```
|
|
||||||
```bash
|
|
||||||
poetry install
|
|
||||||
```
|
```
|
||||||
### Customizing
|
### Customizing
|
||||||
|
|
||||||
@@ -35,7 +32,7 @@ poetry install
|
|||||||
To kickstart your crew of AI agents and begin task execution, run this from the root folder of your project:
|
To kickstart your crew of AI agents and begin task execution, run this from the root folder of your project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry run {{folder_name}}
|
crewai run
|
||||||
```
|
```
|
||||||
|
|
||||||
This command initializes the {{name}} Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
This command initializes the {{name}} Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ authors = ["Your Name <you@example.com>"]
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.10,<=3.13"
|
python = ">=3.10,<=3.13"
|
||||||
crewai = { extras = ["tools"], version = "^0.51.0" }
|
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
{{folder_name}} = "{{folder_name}}.main:main"
|
{{folder_name}} = "{{folder_name}}.main:main"
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import os
|
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from langchain_core.callbacks import BaseCallbackHandler
|
from langchain_core.callbacks import BaseCallbackHandler
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
UUID4,
|
UUID4,
|
||||||
BaseModel,
|
BaseModel,
|
||||||
ConfigDict,
|
|
||||||
Field,
|
Field,
|
||||||
InstanceOf,
|
InstanceOf,
|
||||||
Json,
|
Json,
|
||||||
@@ -48,11 +47,10 @@ from crewai.utilities.planning_handler import CrewPlanner
|
|||||||
from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler
|
from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler
|
||||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||||
|
|
||||||
|
|
||||||
agentops = None
|
agentops = None
|
||||||
if os.environ.get("AGENTOPS_API_KEY"):
|
if os.environ.get("AGENTOPS_API_KEY"):
|
||||||
try:
|
try:
|
||||||
import agentops
|
import agentops # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -106,7 +104,6 @@ class Crew(BaseModel):
|
|||||||
|
|
||||||
name: Optional[str] = Field(default=None)
|
name: Optional[str] = Field(default=None)
|
||||||
cache: bool = Field(default=True)
|
cache: bool = Field(default=True)
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
||||||
tasks: List[Task] = Field(default_factory=list)
|
tasks: List[Task] = Field(default_factory=list)
|
||||||
agents: List[BaseAgent] = Field(default_factory=list)
|
agents: List[BaseAgent] = Field(default_factory=list)
|
||||||
process: Process = Field(default=Process.sequential)
|
process: Process = Field(default=Process.sequential)
|
||||||
@@ -364,7 +361,7 @@ class Crew(BaseModel):
|
|||||||
source = [agent.key for agent in self.agents] + [
|
source = [agent.key for agent in self.agents] + [
|
||||||
task.key for task in self.tasks
|
task.key for task in self.tasks
|
||||||
]
|
]
|
||||||
return md5("|".join(source).encode()).hexdigest()
|
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
def _setup_from_config(self):
|
def _setup_from_config(self):
|
||||||
assert self.config is not None, "Config should not be None."
|
assert self.config is not None, "Config should not be None."
|
||||||
@@ -541,7 +538,7 @@ class Crew(BaseModel):
|
|||||||
)._handle_crew_planning()
|
)._handle_crew_planning()
|
||||||
|
|
||||||
for task, step_plan in zip(self.tasks, result.list_of_plans_per_task):
|
for task, step_plan in zip(self.tasks, result.list_of_plans_per_task):
|
||||||
task.description += step_plan
|
task.description += step_plan.plan
|
||||||
|
|
||||||
def _store_execution_log(
|
def _store_execution_log(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Memory:
|
|||||||
if agent:
|
if agent:
|
||||||
metadata["agent"] = agent
|
metadata["agent"] = agent
|
||||||
|
|
||||||
self.storage.save(value, metadata) # type: ignore # Maybe BUG? Should be self.storage.save(key, value, metadata)
|
self.storage.save(value, metadata)
|
||||||
|
|
||||||
def search(self, query: str) -> Dict[str, Any]:
|
def search(self, query: str) -> Dict[str, Any]:
|
||||||
return self.storage.search(query)
|
return self.storage.search(query)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
from crewai.project.utils import memoize
|
from crewai.project.utils import memoize
|
||||||
|
|
||||||
|
|
||||||
@@ -5,13 +7,17 @@ def task(func):
|
|||||||
if not hasattr(task, "registration_order"):
|
if not hasattr(task, "registration_order"):
|
||||||
task.registration_order = []
|
task.registration_order = []
|
||||||
|
|
||||||
func.is_task = True
|
@wraps(func)
|
||||||
wrapped_func = memoize(func)
|
def wrapper(*args, **kwargs):
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
if not result.name:
|
||||||
|
result.name = func.__name__
|
||||||
|
return result
|
||||||
|
|
||||||
# Append the function name to the registration order list
|
setattr(wrapper, "is_task", True)
|
||||||
task.registration_order.append(func.__name__)
|
task.registration_order.append(func.__name__)
|
||||||
|
|
||||||
return wrapped_func
|
return memoize(wrapper)
|
||||||
|
|
||||||
|
|
||||||
def agent(func):
|
def agent(func):
|
||||||
|
|||||||
@@ -1,56 +1,45 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pydantic import ConfigDict
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
def CrewBase(cls):
|
def CrewBase(cls):
|
||||||
class WrappedClass(cls):
|
class WrappedClass(cls):
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
||||||
is_crew_class: bool = True # type: ignore
|
is_crew_class: bool = True # type: ignore
|
||||||
|
|
||||||
base_directory = None
|
# Get the directory of the class being decorated
|
||||||
for frame_info in inspect.stack():
|
base_directory = Path(inspect.getfile(cls)).parent
|
||||||
if "site-packages" not in frame_info.filename:
|
|
||||||
base_directory = Path(frame_info.filename).parent.resolve()
|
|
||||||
break
|
|
||||||
|
|
||||||
original_agents_config_path = getattr(
|
original_agents_config_path = getattr(
|
||||||
cls, "agents_config", "config/agents.yaml"
|
cls, "agents_config", "config/agents.yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
original_tasks_config_path = getattr(cls, "tasks_config", "config/tasks.yaml")
|
original_tasks_config_path = getattr(cls, "tasks_config", "config/tasks.yaml")
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.base_directory is None:
|
agents_config_path = self.base_directory / self.original_agents_config_path
|
||||||
raise Exception(
|
tasks_config_path = self.base_directory / self.original_tasks_config_path
|
||||||
"Unable to dynamically determine the project's base directory, you must run it from the project's root directory."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.agents_config = self.load_yaml(
|
self.agents_config = self.load_yaml(agents_config_path)
|
||||||
os.path.join(self.base_directory, self.original_agents_config_path)
|
self.tasks_config = self.load_yaml(tasks_config_path)
|
||||||
)
|
|
||||||
|
|
||||||
self.tasks_config = self.load_yaml(
|
|
||||||
os.path.join(self.base_directory, self.original_tasks_config_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.map_all_agent_variables()
|
self.map_all_agent_variables()
|
||||||
self.map_all_task_variables()
|
self.map_all_task_variables()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_yaml(config_path: str):
|
def load_yaml(config_path: Path):
|
||||||
with open(config_path, "r") as file:
|
try:
|
||||||
# parsedContent = YamlParser.parse(file) # type: ignore # Argument 1 to "parse" has incompatible type "TextIOWrapper"; expected "YamlParser"
|
with open(config_path, "r") as file:
|
||||||
return yaml.safe_load(file)
|
return yaml.safe_load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"File not found: {config_path}")
|
||||||
|
raise
|
||||||
|
|
||||||
def _get_all_functions(self):
|
def _get_all_functions(self):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
from typing import Callable, Dict
|
from typing import Any, Callable, Dict, List, Type, Union
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
|
||||||
|
|
||||||
from crewai.crew import Crew
|
from crewai.crew import Crew
|
||||||
from crewai.pipeline.pipeline import Pipeline
|
from crewai.pipeline.pipeline import Pipeline
|
||||||
from crewai.routers.router import Router
|
from crewai.routers.router import Router
|
||||||
|
|
||||||
|
PipelineStage = Union[Crew, List[Crew], Router]
|
||||||
|
|
||||||
|
|
||||||
# TODO: Could potentially remove. Need to check with @joao and @gui if this is needed for CrewAI+
|
# TODO: Could potentially remove. Need to check with @joao and @gui if this is needed for CrewAI+
|
||||||
def PipelineBase(cls):
|
def PipelineBase(cls: Type[Any]) -> Type[Any]:
|
||||||
class WrappedClass(cls):
|
class WrappedClass(cls):
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
is_pipeline_class: bool = True # type: ignore
|
||||||
is_pipeline_class: bool = True
|
stages: List[PipelineStage]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.stages = []
|
self.stages = []
|
||||||
self._map_pipeline_components()
|
self._map_pipeline_components()
|
||||||
|
|
||||||
def _get_all_functions(self):
|
def _get_all_functions(self) -> Dict[str, Callable[..., Any]]:
|
||||||
return {
|
return {
|
||||||
name: getattr(self, name)
|
name: getattr(self, name)
|
||||||
for name in dir(self)
|
for name in dir(self)
|
||||||
@@ -26,15 +26,15 @@ def PipelineBase(cls):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _filter_functions(
|
def _filter_functions(
|
||||||
self, functions: Dict[str, Callable], attribute: str
|
self, functions: Dict[str, Callable[..., Any]], attribute: str
|
||||||
) -> Dict[str, Callable]:
|
) -> Dict[str, Callable[..., Any]]:
|
||||||
return {
|
return {
|
||||||
name: func
|
name: func
|
||||||
for name, func in functions.items()
|
for name, func in functions.items()
|
||||||
if hasattr(func, attribute)
|
if hasattr(func, attribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
def _map_pipeline_components(self):
|
def _map_pipeline_components(self) -> None:
|
||||||
all_functions = self._get_all_functions()
|
all_functions = self._get_all_functions()
|
||||||
crew_functions = self._filter_functions(all_functions, "is_crew")
|
crew_functions = self._filter_functions(all_functions, "is_crew")
|
||||||
router_functions = self._filter_functions(all_functions, "is_router")
|
router_functions = self._filter_functions(all_functions, "is_router")
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Callable, Dict, Generic, Tuple, TypeVar
|
from typing import Any, Callable, Dict, Tuple
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, PrivateAttr
|
from pydantic import BaseModel, Field, PrivateAttr
|
||||||
|
|
||||||
T = TypeVar("T", bound=Dict[str, Any])
|
|
||||||
U = TypeVar("U")
|
class Route(BaseModel):
|
||||||
|
condition: Callable[[Dict[str, Any]], bool]
|
||||||
|
pipeline: Any
|
||||||
|
|
||||||
|
|
||||||
class Route(Generic[T, U]):
|
class Router(BaseModel):
|
||||||
condition: Callable[[T], bool]
|
routes: Dict[str, Route] = Field(
|
||||||
pipeline: U
|
|
||||||
|
|
||||||
def __init__(self, condition: Callable[[T], bool], pipeline: U):
|
|
||||||
self.condition = condition
|
|
||||||
self.pipeline = pipeline
|
|
||||||
|
|
||||||
|
|
||||||
class Router(BaseModel, Generic[T, U]):
|
|
||||||
routes: Dict[str, Route[T, U]] = Field(
|
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Dictionary of route names to (condition, pipeline) tuples",
|
description="Dictionary of route names to (condition, pipeline) tuples",
|
||||||
)
|
)
|
||||||
default: U = Field(..., description="Default pipeline if no conditions are met")
|
default: Any = Field(..., description="Default pipeline if no conditions are met")
|
||||||
_route_types: Dict[str, type] = PrivateAttr(default_factory=dict)
|
_route_types: Dict[str, type] = PrivateAttr(default_factory=dict)
|
||||||
|
|
||||||
model_config = {"arbitrary_types_allowed": True}
|
class Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
def __init__(self, routes: Dict[str, Route[T, U]], default: U, **data):
|
def __init__(self, routes: Dict[str, Route], default: Any, **data):
|
||||||
super().__init__(routes=routes, default=default, **data)
|
super().__init__(routes=routes, default=default, **data)
|
||||||
self._check_copyable(default)
|
self._check_copyable(default)
|
||||||
for name, route in routes.items():
|
for name, route in routes.items():
|
||||||
@@ -34,16 +28,16 @@ class Router(BaseModel, Generic[T, U]):
|
|||||||
self._route_types[name] = type(route.pipeline)
|
self._route_types[name] = type(route.pipeline)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_copyable(obj):
|
def _check_copyable(obj: Any) -> None:
|
||||||
if not hasattr(obj, "copy") or not callable(getattr(obj, "copy")):
|
if not hasattr(obj, "copy") or not callable(getattr(obj, "copy")):
|
||||||
raise ValueError(f"Object of type {type(obj)} must have a 'copy' method")
|
raise ValueError(f"Object of type {type(obj)} must have a 'copy' method")
|
||||||
|
|
||||||
def add_route(
|
def add_route(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
condition: Callable[[T], bool],
|
condition: Callable[[Dict[str, Any]], bool],
|
||||||
pipeline: U,
|
pipeline: Any,
|
||||||
) -> "Router[T, U]":
|
) -> "Router":
|
||||||
"""
|
"""
|
||||||
Add a named route with its condition and corresponding pipeline to the router.
|
Add a named route with its condition and corresponding pipeline to the router.
|
||||||
|
|
||||||
@@ -60,7 +54,7 @@ class Router(BaseModel, Generic[T, U]):
|
|||||||
self._route_types[name] = type(pipeline)
|
self._route_types[name] = type(pipeline)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def route(self, input_data: T) -> Tuple[U, str]:
|
def route(self, input_data: Dict[str, Any]) -> Tuple[Any, str]:
|
||||||
"""
|
"""
|
||||||
Evaluate the input against the conditions and return the appropriate pipeline.
|
Evaluate the input against the conditions and return the appropriate pipeline.
|
||||||
|
|
||||||
@@ -76,15 +70,15 @@ class Router(BaseModel, Generic[T, U]):
|
|||||||
|
|
||||||
return self.default, "default"
|
return self.default, "default"
|
||||||
|
|
||||||
def copy(self) -> "Router[T, U]":
|
def copy(self) -> "Router":
|
||||||
"""Create a deep copy of the Router."""
|
"""Create a deep copy of the Router."""
|
||||||
new_routes = {
|
new_routes = {
|
||||||
name: Route(
|
name: Route(
|
||||||
condition=deepcopy(route.condition),
|
condition=deepcopy(route.condition),
|
||||||
pipeline=route.pipeline.copy(), # type: ignore
|
pipeline=route.pipeline.copy(),
|
||||||
)
|
)
|
||||||
for name, route in self.routes.items()
|
for name, route in self.routes.items()
|
||||||
}
|
}
|
||||||
new_default = self.default.copy() # type: ignore
|
new_default = self.default.copy()
|
||||||
|
|
||||||
return Router(routes=new_routes, default=new_default)
|
return Router(routes=new_routes, default=new_default)
|
||||||
|
|||||||
@@ -9,13 +9,21 @@ from hashlib import md5
|
|||||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from opentelemetry.trace import Span
|
from opentelemetry.trace import Span
|
||||||
from pydantic import UUID4, BaseModel, Field, field_validator, model_validator
|
from pydantic import (
|
||||||
|
UUID4,
|
||||||
|
BaseModel,
|
||||||
|
Field,
|
||||||
|
PrivateAttr,
|
||||||
|
field_validator,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
from pydantic_core import PydanticCustomError
|
from pydantic_core import PydanticCustomError
|
||||||
|
|
||||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||||
from crewai.tasks.output_format import OutputFormat
|
from crewai.tasks.output_format import OutputFormat
|
||||||
from crewai.tasks.task_output import TaskOutput
|
from crewai.tasks.task_output import TaskOutput
|
||||||
from crewai.telemetry.telemetry import Telemetry
|
from crewai.telemetry.telemetry import Telemetry
|
||||||
|
from crewai.utilities.config import process_config
|
||||||
from crewai.utilities.converter import Converter, convert_to_model
|
from crewai.utilities.converter import Converter, convert_to_model
|
||||||
from crewai.utilities.i18n import I18N
|
from crewai.utilities.i18n import I18N
|
||||||
|
|
||||||
@@ -39,9 +47,6 @@ class Task(BaseModel):
|
|||||||
tools: List of tools/resources limited for task execution.
|
tools: List of tools/resources limited for task execution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Config:
|
|
||||||
arbitrary_types_allowed = True
|
|
||||||
|
|
||||||
__hash__ = object.__hash__ # type: ignore
|
__hash__ = object.__hash__ # type: ignore
|
||||||
used_tools: int = 0
|
used_tools: int = 0
|
||||||
tools_errors: int = 0
|
tools_errors: int = 0
|
||||||
@@ -104,16 +109,27 @@ class Task(BaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
_telemetry: Telemetry
|
_telemetry: Telemetry = PrivateAttr(default_factory=Telemetry)
|
||||||
_execution_span: Span | None = None
|
_execution_span: Optional[Span] = PrivateAttr(default=None)
|
||||||
_original_description: str | None = None
|
_original_description: Optional[str] = PrivateAttr(default=None)
|
||||||
_original_expected_output: str | None = None
|
_original_expected_output: Optional[str] = PrivateAttr(default=None)
|
||||||
_thread: threading.Thread | None = None
|
_thread: Optional[threading.Thread] = PrivateAttr(default=None)
|
||||||
_execution_time: float | None = None
|
_execution_time: Optional[float] = PrivateAttr(default=None)
|
||||||
|
|
||||||
def __init__(__pydantic_self__, **data):
|
@model_validator(mode="before")
|
||||||
config = data.pop("config", {})
|
@classmethod
|
||||||
super().__init__(**config, **data)
|
def process_model_config(cls, values):
|
||||||
|
return process_config(values, cls)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_required_fields(self):
|
||||||
|
required_fields = ["description", "expected_output"]
|
||||||
|
for field in required_fields:
|
||||||
|
if getattr(self, field) is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"{field} must be provided either directly or through config"
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
@field_validator("id", mode="before")
|
@field_validator("id", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -137,12 +153,6 @@ class Task(BaseModel):
|
|||||||
return value[1:]
|
return value[1:]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def set_private_attrs(self) -> "Task":
|
|
||||||
"""Set private attributes."""
|
|
||||||
self._telemetry = Telemetry()
|
|
||||||
return self
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def set_attributes_based_on_config(self) -> "Task":
|
def set_attributes_based_on_config(self) -> "Task":
|
||||||
"""Set attributes based on the agent configuration."""
|
"""Set attributes based on the agent configuration."""
|
||||||
@@ -185,7 +195,7 @@ class Task(BaseModel):
|
|||||||
expected_output = self._original_expected_output or self.expected_output
|
expected_output = self._original_expected_output or self.expected_output
|
||||||
source = [description, expected_output]
|
source = [description, expected_output]
|
||||||
|
|
||||||
return md5("|".join(source).encode()).hexdigest()
|
return md5("|".join(source).encode(), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
def execute_async(
|
def execute_async(
|
||||||
self,
|
self,
|
||||||
@@ -240,7 +250,9 @@ class Task(BaseModel):
|
|||||||
pydantic_output, json_output = self._export_output(result)
|
pydantic_output, json_output = self._export_output(result)
|
||||||
|
|
||||||
task_output = TaskOutput(
|
task_output = TaskOutput(
|
||||||
|
name=self.name,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
|
expected_output=self.expected_output,
|
||||||
raw=result,
|
raw=result,
|
||||||
pydantic=pydantic_output,
|
pydantic=pydantic_output,
|
||||||
json_dict=json_output,
|
json_dict=json_output,
|
||||||
@@ -261,9 +273,7 @@ class Task(BaseModel):
|
|||||||
content = (
|
content = (
|
||||||
json_output
|
json_output
|
||||||
if json_output
|
if json_output
|
||||||
else pydantic_output.model_dump_json()
|
else pydantic_output.model_dump_json() if pydantic_output else result
|
||||||
if pydantic_output
|
|
||||||
else result
|
|
||||||
)
|
)
|
||||||
self._save_file(content)
|
self._save_file(content)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ class TaskOutput(BaseModel):
|
|||||||
"""Class that represents the result of a task."""
|
"""Class that represents the result of a task."""
|
||||||
|
|
||||||
description: str = Field(description="Description of the task")
|
description: str = Field(description="Description of the task")
|
||||||
|
name: Optional[str] = Field(description="Name of the task", default=None)
|
||||||
|
expected_output: Optional[str] = Field(
|
||||||
|
description="Expected output of the task", default=None
|
||||||
|
)
|
||||||
summary: Optional[str] = Field(description="Summary of the task", default=None)
|
summary: Optional[str] = Field(description="Summary of the task", default=None)
|
||||||
raw: str = Field(description="Raw output of the task", default="")
|
raw: str = Field(description="Raw output of the task", default="")
|
||||||
pydantic: Optional[BaseModel] = Field(
|
pydantic: Optional[BaseModel] = Field(
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ class Telemetry:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def individual_test_result_span(
|
def individual_test_result_span(
|
||||||
self, crew: Crew, quality: int, exec_time: int, model_name: str
|
self, crew: Crew, quality: float, exec_time: int, model_name: str
|
||||||
):
|
):
|
||||||
if self.ready:
|
if self.ready:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from langchain.tools import StructuredTool
|
from langchain.tools import StructuredTool
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from crewai.agents.cache import CacheHandler
|
from crewai.agents.cache import CacheHandler
|
||||||
|
|
||||||
@@ -7,11 +7,10 @@ from crewai.agents.cache import CacheHandler
|
|||||||
class CacheTools(BaseModel):
|
class CacheTools(BaseModel):
|
||||||
"""Default tools to hit the cache."""
|
"""Default tools to hit the cache."""
|
||||||
|
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
||||||
name: str = "Hit Cache"
|
name: str = "Hit Cache"
|
||||||
cache_handler: CacheHandler = Field(
|
cache_handler: CacheHandler = Field(
|
||||||
description="Cache Handler for the crew",
|
description="Cache Handler for the crew",
|
||||||
default=CacheHandler(),
|
default_factory=CacheHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
def tool(self):
|
def tool(self):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ast
|
import ast
|
||||||
from difflib import SequenceMatcher
|
|
||||||
import os
|
import os
|
||||||
|
from difflib import SequenceMatcher
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Any, List, Union
|
from typing import Any, List, Union
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ from crewai.utilities import I18N, Converter, ConverterError, Printer
|
|||||||
agentops = None
|
agentops = None
|
||||||
if os.environ.get("AGENTOPS_API_KEY"):
|
if os.environ.get("AGENTOPS_API_KEY"):
|
||||||
try:
|
try:
|
||||||
import agentops
|
import agentops # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ class ToolUsage:
|
|||||||
tool: BaseTool,
|
tool: BaseTool,
|
||||||
calling: Union[ToolCalling, InstructorToolCalling],
|
calling: Union[ToolCalling, InstructorToolCalling],
|
||||||
) -> str: # TODO: Fix this return type
|
) -> str: # TODO: Fix this return type
|
||||||
tool_event = agentops.ToolEvent(name=calling.tool_name) if agentops else None
|
tool_event = agentops.ToolEvent(name=calling.tool_name) if agentops else None # type: ignore
|
||||||
if self._check_tool_repeated_usage(calling=calling): # type: ignore # _check_tool_repeated_usage of "ToolUsage" does not return a value (it only ever returns None)
|
if self._check_tool_repeated_usage(calling=calling): # type: ignore # _check_tool_repeated_usage of "ToolUsage" does not return a value (it only ever returns None)
|
||||||
try:
|
try:
|
||||||
result = self._i18n.errors("task_repeated_usage").format(
|
result = self._i18n.errors("task_repeated_usage").format(
|
||||||
|
|||||||
40
src/crewai/utilities/config.py
Normal file
40
src/crewai/utilities/config.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from typing import Any, Dict, Type
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
def process_config(
|
||||||
|
values: Dict[str, Any], model_class: Type[BaseModel]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process the config dictionary and update the values accordingly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
values (Dict[str, Any]): The dictionary of values to update.
|
||||||
|
model_class (Type[BaseModel]): The Pydantic model class to reference for field validation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: The updated values dictionary.
|
||||||
|
"""
|
||||||
|
config = values.get("config", {})
|
||||||
|
if not config:
|
||||||
|
return values
|
||||||
|
|
||||||
|
# Copy values from config (originally from YAML) to the model's attributes.
|
||||||
|
# Only copy if the attribute isn't already set, preserving any explicitly defined values.
|
||||||
|
for key, value in config.items():
|
||||||
|
if key not in model_class.model_fields:
|
||||||
|
continue
|
||||||
|
if values.get(key) is not None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, (str, int, float, bool, list)):
|
||||||
|
values[key] = value
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
if isinstance(values.get(key), dict):
|
||||||
|
values[key].update(value)
|
||||||
|
else:
|
||||||
|
values[key] = value
|
||||||
|
|
||||||
|
# Remove the config from values to avoid duplicate processing
|
||||||
|
values.pop("config", None)
|
||||||
|
return values
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, PrivateAttr
|
||||||
|
|
||||||
from crewai.utilities.printer import Printer
|
from crewai.utilities.printer import Printer
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
class Logger(BaseModel):
|
||||||
_printer = Printer()
|
verbose: bool = Field(default=False)
|
||||||
|
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||||
def __init__(self, verbose=False):
|
|
||||||
self.verbose = verbose
|
|
||||||
|
|
||||||
def log(self, level, message, color="bold_green"):
|
def log(self, level, message, color="bold_green"):
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from crewai.agent import Agent
|
from crewai.agent import Agent
|
||||||
from crewai.task import Task
|
from crewai.task import Task
|
||||||
|
|
||||||
|
|
||||||
|
class PlanPerTask(BaseModel):
|
||||||
|
task: str = Field(..., description="The task for which the plan is created")
|
||||||
|
plan: str = Field(
|
||||||
|
...,
|
||||||
|
description="The step by step plan on how the agents can execute their tasks using the available tools with mastery",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PlannerTaskPydanticOutput(BaseModel):
|
class PlannerTaskPydanticOutput(BaseModel):
|
||||||
list_of_plans_per_task: List[str]
|
list_of_plans_per_task: List[PlanPerTask] = Field(
|
||||||
|
...,
|
||||||
|
description="Step by step plan on how the agents can execute their tasks using the available tools with mastery",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CrewPlanner:
|
class CrewPlanner:
|
||||||
|
|||||||
@@ -1,44 +1,50 @@
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Union
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
|
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
||||||
|
|
||||||
from crewai.utilities.logger import Logger
|
from crewai.utilities.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
class RPMController(BaseModel):
|
class RPMController(BaseModel):
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
max_rpm: Optional[int] = Field(default=None)
|
||||||
max_rpm: Union[int, None] = Field(default=None)
|
logger: Logger = Field(default_factory=lambda: Logger(verbose=False))
|
||||||
logger: Logger = Field(default=None)
|
|
||||||
_current_rpm: int = PrivateAttr(default=0)
|
_current_rpm: int = PrivateAttr(default=0)
|
||||||
_timer: threading.Timer | None = PrivateAttr(default=None)
|
_timer: Optional[threading.Timer] = PrivateAttr(default=None)
|
||||||
_lock: threading.Lock = PrivateAttr(default=None)
|
_lock: Optional[threading.Lock] = PrivateAttr(default=None)
|
||||||
_shutdown_flag = False
|
_shutdown_flag: bool = PrivateAttr(default=False)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def reset_counter(self):
|
def reset_counter(self):
|
||||||
if self.max_rpm:
|
if self.max_rpm is not None:
|
||||||
if not self._shutdown_flag:
|
if not self._shutdown_flag:
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._reset_request_count()
|
self._reset_request_count()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def check_or_wait(self):
|
def check_or_wait(self):
|
||||||
if not self.max_rpm:
|
if self.max_rpm is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
with self._lock:
|
def _check_and_increment():
|
||||||
if self._current_rpm < self.max_rpm:
|
if self.max_rpm is not None and self._current_rpm < self.max_rpm:
|
||||||
self._current_rpm += 1
|
self._current_rpm += 1
|
||||||
return True
|
return True
|
||||||
else:
|
elif self.max_rpm is not None:
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
"info", "Max RPM reached, waiting for next minute to start."
|
"info", "Max RPM reached, waiting for next minute to start."
|
||||||
)
|
)
|
||||||
self._wait_for_next_minute()
|
self._wait_for_next_minute()
|
||||||
self._current_rpm = 1
|
self._current_rpm = 1
|
||||||
return True
|
return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self._lock:
|
||||||
|
with self._lock:
|
||||||
|
return _check_and_increment()
|
||||||
|
else:
|
||||||
|
return _check_and_increment()
|
||||||
|
|
||||||
def stop_rpm_counter(self):
|
def stop_rpm_counter(self):
|
||||||
if self._timer:
|
if self._timer:
|
||||||
@@ -50,10 +56,18 @@ class RPMController(BaseModel):
|
|||||||
self._current_rpm = 0
|
self._current_rpm = 0
|
||||||
|
|
||||||
def _reset_request_count(self):
|
def _reset_request_count(self):
|
||||||
with self._lock:
|
def _reset():
|
||||||
self._current_rpm = 0
|
self._current_rpm = 0
|
||||||
|
if not self._shutdown_flag:
|
||||||
|
self._timer = threading.Timer(60.0, self._reset_request_count)
|
||||||
|
self._timer.start()
|
||||||
|
|
||||||
|
if self._lock:
|
||||||
|
with self._lock:
|
||||||
|
_reset()
|
||||||
|
else:
|
||||||
|
_reset()
|
||||||
|
|
||||||
if self._timer:
|
if self._timer:
|
||||||
self._shutdown_flag = True
|
self._shutdown_flag = True
|
||||||
self._timer.cancel()
|
self._timer.cancel()
|
||||||
self._timer = threading.Timer(60.0, self._reset_request_count)
|
|
||||||
self._timer.start()
|
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ from unittest import mock
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain.tools import tool
|
|
||||||
from langchain_core.exceptions import OutputParserException
|
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langchain.schema import AgentAction
|
|
||||||
|
|
||||||
from crewai import Agent, Crew, Task
|
from crewai import Agent, Crew, Task
|
||||||
from crewai.agents.cache import CacheHandler
|
from crewai.agents.cache import CacheHandler
|
||||||
from crewai.agents.executor import CrewAgentExecutor
|
from crewai.agents.executor import CrewAgentExecutor
|
||||||
@@ -16,6 +11,10 @@ from crewai.agents.parser import CrewAgentParser
|
|||||||
from crewai.tools.tool_calling import InstructorToolCalling
|
from crewai.tools.tool_calling import InstructorToolCalling
|
||||||
from crewai.tools.tool_usage import ToolUsage
|
from crewai.tools.tool_usage import ToolUsage
|
||||||
from crewai.utilities import RPMController
|
from crewai.utilities import RPMController
|
||||||
|
from langchain.schema import AgentAction
|
||||||
|
from langchain.tools import tool
|
||||||
|
from langchain_core.exceptions import OutputParserException
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
|
||||||
def test_agent_creation():
|
def test_agent_creation():
|
||||||
@@ -817,7 +816,7 @@ def test_agent_definition_based_on_dict():
|
|||||||
"verbose": True,
|
"verbose": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
agent = Agent(config=config)
|
agent = Agent(**config)
|
||||||
|
|
||||||
assert agent.role == "test role"
|
assert agent.role == "test role"
|
||||||
assert agent.goal == "test goal"
|
assert agent.goal == "test goal"
|
||||||
@@ -837,7 +836,7 @@ def test_agent_human_input():
|
|||||||
"backstory": "test backstory",
|
"backstory": "test backstory",
|
||||||
}
|
}
|
||||||
|
|
||||||
agent = Agent(config=config)
|
agent = Agent(**config)
|
||||||
|
|
||||||
task = Task(
|
task = Task(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
|
|||||||
94
tests/cli/authentication/test_auth_main.py
Normal file
94
tests/cli/authentication/test_auth_main.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from crewai.cli.authentication.main import AuthenticationCommand
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthenticationCommand(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.auth_command = AuthenticationCommand()
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.main.requests.post")
|
||||||
|
def test_get_device_code(self, mock_post):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"device_code": "123456",
|
||||||
|
"user_code": "ABCDEF",
|
||||||
|
"verification_uri_complete": "https://example.com",
|
||||||
|
"interval": 5,
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
device_code_data = self.auth_command._get_device_code()
|
||||||
|
|
||||||
|
self.assertEqual(device_code_data["device_code"], "123456")
|
||||||
|
self.assertEqual(device_code_data["user_code"], "ABCDEF")
|
||||||
|
self.assertEqual(
|
||||||
|
device_code_data["verification_uri_complete"], "https://example.com"
|
||||||
|
)
|
||||||
|
self.assertEqual(device_code_data["interval"], 5)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.main.console.print")
|
||||||
|
@patch("crewai.cli.authentication.main.webbrowser.open")
|
||||||
|
def test_display_auth_instructions(self, mock_open, mock_print):
|
||||||
|
device_code_data = {
|
||||||
|
"verification_uri_complete": "https://example.com",
|
||||||
|
"user_code": "ABCDEF",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auth_command._display_auth_instructions(device_code_data)
|
||||||
|
|
||||||
|
mock_print.assert_any_call("1. Navigate to: ", "https://example.com")
|
||||||
|
mock_print.assert_any_call("2. Enter the following code: ", "ABCDEF")
|
||||||
|
mock_open.assert_called_once_with("https://example.com")
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.main.requests.post")
|
||||||
|
@patch("crewai.cli.authentication.main.validate_token")
|
||||||
|
@patch("crewai.cli.authentication.main.console.print")
|
||||||
|
def test_poll_for_token_success(self, mock_print, mock_validate_token, mock_post):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"id_token": "TOKEN",
|
||||||
|
"access_token": "ACCESS_TOKEN",
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
self.auth_command._poll_for_token({"device_code": "123456"})
|
||||||
|
|
||||||
|
mock_validate_token.assert_called_once_with("TOKEN")
|
||||||
|
mock_print.assert_called_once_with("\nWelcome to CrewAI+ !!", style="green")
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.main.requests.post")
|
||||||
|
@patch("crewai.cli.authentication.main.console.print")
|
||||||
|
def test_poll_for_token_error(self, mock_print, mock_post):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"error": "invalid_request",
|
||||||
|
"error_description": "Invalid request",
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
with self.assertRaises(requests.HTTPError):
|
||||||
|
self.auth_command._poll_for_token({"device_code": "123456"})
|
||||||
|
|
||||||
|
mock_print.assert_not_called()
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.main.requests.post")
|
||||||
|
@patch("crewai.cli.authentication.main.console.print")
|
||||||
|
def test_poll_for_token_timeout(self, mock_print, mock_post):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"error": "authorization_pending",
|
||||||
|
"error_description": "Authorization pending",
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
self.auth_command._poll_for_token({"device_code": "123456", "interval": 0.01})
|
||||||
|
|
||||||
|
mock_print.assert_called_once_with(
|
||||||
|
"Timeout: Failed to get the token. Please try again.", style="bold red"
|
||||||
|
)
|
||||||
147
tests/cli/authentication/test_utils.py
Normal file
147
tests/cli/authentication/test_utils.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from crewai.cli.authentication.utils import TokenManager, validate_token
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateToken(unittest.TestCase):
|
||||||
|
@patch("crewai.cli.authentication.utils.AsymmetricSignatureVerifier")
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenVerifier")
|
||||||
|
def test_validate_token(self, mock_token_verifier, mock_asymmetric_verifier):
|
||||||
|
mock_verifier_instance = mock_token_verifier.return_value
|
||||||
|
mock_id_token = "mock_id_token"
|
||||||
|
|
||||||
|
validate_token(mock_id_token)
|
||||||
|
|
||||||
|
mock_asymmetric_verifier.assert_called_once_with(
|
||||||
|
"https://dev-jzsr0j8zs0atl5ha.us.auth0.com/.well-known/jwks.json"
|
||||||
|
)
|
||||||
|
mock_token_verifier.assert_called_once_with(
|
||||||
|
signature_verifier=mock_asymmetric_verifier.return_value,
|
||||||
|
issuer="https://dev-jzsr0j8zs0atl5ha.us.auth0.com/",
|
||||||
|
audience="CZtyRHuVW80HbLSjk4ggXNzjg4KAt7Oe",
|
||||||
|
)
|
||||||
|
mock_verifier_instance.verify.assert_called_once_with(mock_id_token)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTokenManager(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.token_manager = TokenManager()
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.read_secure_file")
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.save_secure_file")
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager._get_or_create_key")
|
||||||
|
def test_get_or_create_key_existing(self, mock_get_or_create, mock_save, mock_read):
|
||||||
|
mock_key = Fernet.generate_key()
|
||||||
|
mock_get_or_create.return_value = mock_key
|
||||||
|
|
||||||
|
token_manager = TokenManager()
|
||||||
|
result = token_manager.key
|
||||||
|
|
||||||
|
self.assertEqual(result, mock_key)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.Fernet.generate_key")
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.read_secure_file")
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.save_secure_file")
|
||||||
|
def test_get_or_create_key_new(self, mock_save, mock_read, mock_generate):
|
||||||
|
mock_key = b"new_key"
|
||||||
|
mock_read.return_value = None
|
||||||
|
mock_generate.return_value = mock_key
|
||||||
|
|
||||||
|
result = self.token_manager._get_or_create_key()
|
||||||
|
|
||||||
|
self.assertEqual(result, mock_key)
|
||||||
|
mock_read.assert_called_once_with("secret.key")
|
||||||
|
mock_generate.assert_called_once()
|
||||||
|
mock_save.assert_called_once_with("secret.key", mock_key)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.save_secure_file")
|
||||||
|
def test_save_tokens(self, mock_save):
|
||||||
|
access_token = "test_token"
|
||||||
|
expires_in = 3600
|
||||||
|
|
||||||
|
self.token_manager.save_tokens(access_token, expires_in)
|
||||||
|
|
||||||
|
mock_save.assert_called_once()
|
||||||
|
args = mock_save.call_args[0]
|
||||||
|
self.assertEqual(args[0], "tokens.enc")
|
||||||
|
decrypted_data = self.token_manager.fernet.decrypt(args[1])
|
||||||
|
data = json.loads(decrypted_data)
|
||||||
|
self.assertEqual(data["access_token"], access_token)
|
||||||
|
expiration = datetime.fromisoformat(data["expiration"])
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
expiration,
|
||||||
|
datetime.now() + timedelta(seconds=expires_in),
|
||||||
|
delta=timedelta(seconds=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.read_secure_file")
|
||||||
|
def test_get_token_valid(self, mock_read):
|
||||||
|
access_token = "test_token"
|
||||||
|
expiration = (datetime.now() + timedelta(hours=1)).isoformat()
|
||||||
|
data = {"access_token": access_token, "expiration": expiration}
|
||||||
|
encrypted_data = self.token_manager.fernet.encrypt(json.dumps(data).encode())
|
||||||
|
mock_read.return_value = encrypted_data
|
||||||
|
|
||||||
|
result = self.token_manager.get_token()
|
||||||
|
|
||||||
|
self.assertEqual(result, access_token)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.read_secure_file")
|
||||||
|
def test_get_token_expired(self, mock_read):
|
||||||
|
access_token = "test_token"
|
||||||
|
expiration = (datetime.now() - timedelta(hours=1)).isoformat()
|
||||||
|
data = {"access_token": access_token, "expiration": expiration}
|
||||||
|
encrypted_data = self.token_manager.fernet.encrypt(json.dumps(data).encode())
|
||||||
|
mock_read.return_value = encrypted_data
|
||||||
|
|
||||||
|
result = self.token_manager.get_token()
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.get_secure_storage_path")
|
||||||
|
@patch("builtins.open", new_callable=unittest.mock.mock_open)
|
||||||
|
@patch("crewai.cli.authentication.utils.os.chmod")
|
||||||
|
def test_save_secure_file(self, mock_chmod, mock_open, mock_get_path):
|
||||||
|
mock_path = MagicMock()
|
||||||
|
mock_get_path.return_value = mock_path
|
||||||
|
filename = "test_file.txt"
|
||||||
|
content = b"test_content"
|
||||||
|
|
||||||
|
self.token_manager.save_secure_file(filename, content)
|
||||||
|
|
||||||
|
mock_path.__truediv__.assert_called_once_with(filename)
|
||||||
|
mock_open.assert_called_once_with(mock_path.__truediv__.return_value, "wb")
|
||||||
|
mock_open().write.assert_called_once_with(content)
|
||||||
|
mock_chmod.assert_called_once_with(mock_path.__truediv__.return_value, 0o600)
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.get_secure_storage_path")
|
||||||
|
@patch(
|
||||||
|
"builtins.open", new_callable=unittest.mock.mock_open, read_data=b"test_content"
|
||||||
|
)
|
||||||
|
def test_read_secure_file_exists(self, mock_open, mock_get_path):
|
||||||
|
mock_path = MagicMock()
|
||||||
|
mock_get_path.return_value = mock_path
|
||||||
|
mock_path.__truediv__.return_value.exists.return_value = True
|
||||||
|
filename = "test_file.txt"
|
||||||
|
|
||||||
|
result = self.token_manager.read_secure_file(filename)
|
||||||
|
|
||||||
|
self.assertEqual(result, b"test_content")
|
||||||
|
mock_path.__truediv__.assert_called_once_with(filename)
|
||||||
|
mock_open.assert_called_once_with(mock_path.__truediv__.return_value, "rb")
|
||||||
|
|
||||||
|
@patch("crewai.cli.authentication.utils.TokenManager.get_secure_storage_path")
|
||||||
|
def test_read_secure_file_not_exists(self, mock_get_path):
|
||||||
|
mock_path = MagicMock()
|
||||||
|
mock_get_path.return_value = mock_path
|
||||||
|
mock_path.__truediv__.return_value.exists.return_value = False
|
||||||
|
filename = "test_file.txt"
|
||||||
|
|
||||||
|
result = self.token_manager.read_secure_file(filename)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
mock_path.__truediv__.assert_called_once_with(filename)
|
||||||
@@ -2,8 +2,19 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
from crewai.cli.cli import (
|
||||||
from crewai.cli.cli import reset_memories, test, train, version
|
deploy_create,
|
||||||
|
deploy_list,
|
||||||
|
deploy_logs,
|
||||||
|
deploy_push,
|
||||||
|
deploy_remove,
|
||||||
|
deply_status,
|
||||||
|
reset_memories,
|
||||||
|
signup,
|
||||||
|
test,
|
||||||
|
train,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -163,3 +174,106 @@ def test_test_invalid_string_iterations(evaluate_crew, runner):
|
|||||||
"Usage: test [OPTIONS]\nTry 'test --help' for help.\n\nError: Invalid value for '-n' / '--n_iterations': 'invalid' is not a valid integer.\n"
|
"Usage: test [OPTIONS]\nTry 'test --help' for help.\n\nError: Invalid value for '-n' / '--n_iterations': 'invalid' is not a valid integer.\n"
|
||||||
in result.output
|
in result.output
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.AuthenticationCommand")
|
||||||
|
def test_signup(command, runner):
|
||||||
|
mock_auth = command.return_value
|
||||||
|
result = runner.invoke(signup)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_auth.signup.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_create(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
result = runner.invoke(deploy_create)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.create_crew.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_list(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
result = runner.invoke(deploy_list)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.list_crews.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_push(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
uuid = "test-uuid"
|
||||||
|
result = runner.invoke(deploy_push, ["-u", uuid])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.deploy.assert_called_once_with(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_push_no_uuid(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
result = runner.invoke(deploy_push)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.deploy.assert_called_once_with(uuid=None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_status(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
uuid = "test-uuid"
|
||||||
|
result = runner.invoke(deply_status, ["-u", uuid])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.get_crew_status.assert_called_once_with(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_status_no_uuid(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
result = runner.invoke(deply_status)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.get_crew_status.assert_called_once_with(uuid=None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_logs(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
uuid = "test-uuid"
|
||||||
|
result = runner.invoke(deploy_logs, ["-u", uuid])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.get_crew_logs.assert_called_once_with(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_logs_no_uuid(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
result = runner.invoke(deploy_logs)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.get_crew_logs.assert_called_once_with(uuid=None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_remove(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
uuid = "test-uuid"
|
||||||
|
result = runner.invoke(deploy_remove, ["-u", uuid])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.remove_crew.assert_called_once_with(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("crewai.cli.cli.DeployCommand")
|
||||||
|
def test_deploy_remove_no_uuid(command, runner):
|
||||||
|
mock_deploy = command.return_value
|
||||||
|
result = runner.invoke(deploy_remove)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_deploy.remove_crew.assert_called_once_with(uuid=None)
|
||||||
|
|||||||
102
tests/cli/deploy/test_api.py
Normal file
102
tests/cli/deploy/test_api.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import unittest
|
||||||
|
from os import environ
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from crewai.cli.deploy.api import CrewAPI
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrewAPI(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.api_key = "test_api_key"
|
||||||
|
self.api = CrewAPI(self.api_key)
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
self.assertEqual(self.api.api_key, self.api_key)
|
||||||
|
self.assertEqual(
|
||||||
|
self.api.headers,
|
||||||
|
{
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.requests.request")
|
||||||
|
def test_make_request(self, mock_request):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
response = self.api._make_request("GET", "test_endpoint")
|
||||||
|
|
||||||
|
mock_request.assert_called_once_with(
|
||||||
|
"GET", f"{self.api.base_url}/test_endpoint", headers=self.api.headers
|
||||||
|
)
|
||||||
|
self.assertEqual(response, mock_response)
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_deploy_by_name(self, mock_make_request):
|
||||||
|
self.api.deploy_by_name("test_project")
|
||||||
|
mock_make_request.assert_called_once_with("POST", "by-name/test_project/deploy")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_deploy_by_uuid(self, mock_make_request):
|
||||||
|
self.api.deploy_by_uuid("test_uuid")
|
||||||
|
mock_make_request.assert_called_once_with("POST", "test_uuid/deploy")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_status_by_name(self, mock_make_request):
|
||||||
|
self.api.status_by_name("test_project")
|
||||||
|
mock_make_request.assert_called_once_with("GET", "by-name/test_project/status")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_status_by_uuid(self, mock_make_request):
|
||||||
|
self.api.status_by_uuid("test_uuid")
|
||||||
|
mock_make_request.assert_called_once_with("GET", "test_uuid/status")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_logs_by_name(self, mock_make_request):
|
||||||
|
self.api.logs_by_name("test_project")
|
||||||
|
mock_make_request.assert_called_once_with(
|
||||||
|
"GET", "by-name/test_project/logs/deployment"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api.logs_by_name("test_project", "custom_log")
|
||||||
|
mock_make_request.assert_called_with(
|
||||||
|
"GET", "by-name/test_project/logs/custom_log"
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_logs_by_uuid(self, mock_make_request):
|
||||||
|
self.api.logs_by_uuid("test_uuid")
|
||||||
|
mock_make_request.assert_called_once_with("GET", "test_uuid/logs/deployment")
|
||||||
|
|
||||||
|
self.api.logs_by_uuid("test_uuid", "custom_log")
|
||||||
|
mock_make_request.assert_called_with("GET", "test_uuid/logs/custom_log")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_delete_by_name(self, mock_make_request):
|
||||||
|
self.api.delete_by_name("test_project")
|
||||||
|
mock_make_request.assert_called_once_with("DELETE", "by-name/test_project")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_delete_by_uuid(self, mock_make_request):
|
||||||
|
self.api.delete_by_uuid("test_uuid")
|
||||||
|
mock_make_request.assert_called_once_with("DELETE", "test_uuid")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_list_crews(self, mock_make_request):
|
||||||
|
self.api.list_crews()
|
||||||
|
mock_make_request.assert_called_once_with("GET", "")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.api.CrewAPI._make_request")
|
||||||
|
def test_create_crew(self, mock_make_request):
|
||||||
|
payload = {"name": "test_crew"}
|
||||||
|
self.api.create_crew(payload)
|
||||||
|
mock_make_request.assert_called_once_with("POST", "", json=payload)
|
||||||
|
|
||||||
|
@patch.dict(environ, {"CREWAI_BASE_URL": "https://custom-url.com/api"})
|
||||||
|
def test_custom_base_url(self):
|
||||||
|
custom_api = CrewAPI("test_key")
|
||||||
|
self.assertEqual(
|
||||||
|
custom_api.base_url,
|
||||||
|
"https://custom-url.com/api",
|
||||||
|
)
|
||||||
153
tests/cli/deploy/test_deploy_main.py
Normal file
153
tests/cli/deploy/test_deploy_main.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import unittest
|
||||||
|
from io import StringIO
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from crewai.cli.deploy.main import DeployCommand
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeployCommand(unittest.TestCase):
|
||||||
|
@patch("crewai.cli.deploy.main.get_auth_token")
|
||||||
|
@patch("crewai.cli.deploy.main.get_project_name")
|
||||||
|
@patch("crewai.cli.deploy.main.CrewAPI")
|
||||||
|
def setUp(self, mock_crew_api, mock_get_project_name, mock_get_auth_token):
|
||||||
|
self.mock_get_auth_token = mock_get_auth_token
|
||||||
|
self.mock_get_project_name = mock_get_project_name
|
||||||
|
self.mock_crew_api = mock_crew_api
|
||||||
|
|
||||||
|
self.mock_get_auth_token.return_value = "test_token"
|
||||||
|
self.mock_get_project_name.return_value = "test_project"
|
||||||
|
|
||||||
|
self.deploy_command = DeployCommand()
|
||||||
|
self.mock_client = self.deploy_command.client
|
||||||
|
|
||||||
|
def test_init_success(self):
|
||||||
|
self.assertEqual(self.deploy_command.project_name, "test_project")
|
||||||
|
self.mock_crew_api.assert_called_once_with(api_key="test_token")
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.main.get_auth_token")
|
||||||
|
def test_init_failure(self, mock_get_auth_token):
|
||||||
|
mock_get_auth_token.side_effect = Exception("Auth failed")
|
||||||
|
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
DeployCommand()
|
||||||
|
|
||||||
|
def test_handle_error(self):
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command._handle_error(
|
||||||
|
{"error": "Test error", "message": "Test message"}
|
||||||
|
)
|
||||||
|
self.assertIn("Error: Test error", fake_out.getvalue())
|
||||||
|
self.assertIn("Message: Test message", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_standard_no_param_error_message(self):
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command._standard_no_param_error_message()
|
||||||
|
self.assertIn("No UUID provided", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_display_deployment_info(self):
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command._display_deployment_info(
|
||||||
|
{"uuid": "test-uuid", "status": "deployed"}
|
||||||
|
)
|
||||||
|
self.assertIn("Deploying the crew...", fake_out.getvalue())
|
||||||
|
self.assertIn("test-uuid", fake_out.getvalue())
|
||||||
|
self.assertIn("deployed", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_display_logs(self):
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command._display_logs(
|
||||||
|
[{"timestamp": "2023-01-01", "level": "INFO", "message": "Test log"}]
|
||||||
|
)
|
||||||
|
self.assertIn("2023-01-01 - INFO: Test log", fake_out.getvalue())
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.main.DeployCommand._display_deployment_info")
|
||||||
|
def test_deploy_with_uuid(self, mock_display):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||||
|
self.mock_client.deploy_by_uuid.return_value = mock_response
|
||||||
|
|
||||||
|
self.deploy_command.deploy(uuid="test-uuid")
|
||||||
|
|
||||||
|
self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid")
|
||||||
|
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.main.DeployCommand._display_deployment_info")
|
||||||
|
def test_deploy_with_project_name(self, mock_display):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||||
|
self.mock_client.deploy_by_name.return_value = mock_response
|
||||||
|
|
||||||
|
self.deploy_command.deploy()
|
||||||
|
|
||||||
|
self.mock_client.deploy_by_name.assert_called_once_with("test_project")
|
||||||
|
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||||
|
|
||||||
|
@patch("crewai.cli.deploy.main.fetch_and_json_env_file")
|
||||||
|
@patch("crewai.cli.deploy.main.get_git_remote_url")
|
||||||
|
@patch("builtins.input")
|
||||||
|
def test_create_crew(self, mock_input, mock_get_git_remote_url, mock_fetch_env):
|
||||||
|
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||||
|
mock_get_git_remote_url.return_value = "https://github.com/test/repo.git"
|
||||||
|
mock_input.return_value = ""
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 201
|
||||||
|
mock_response.json.return_value = {"uuid": "new-uuid", "status": "created"}
|
||||||
|
self.mock_client.create_crew.return_value = mock_response
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command.create_crew()
|
||||||
|
self.assertIn("Deployment created successfully!", fake_out.getvalue())
|
||||||
|
self.assertIn("new-uuid", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_list_crews(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = [
|
||||||
|
{"name": "Crew1", "uuid": "uuid1", "status": "active"},
|
||||||
|
{"name": "Crew2", "uuid": "uuid2", "status": "inactive"},
|
||||||
|
]
|
||||||
|
self.mock_client.list_crews.return_value = mock_response
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command.list_crews()
|
||||||
|
self.assertIn("Crew1 (uuid1) active", fake_out.getvalue())
|
||||||
|
self.assertIn("Crew2 (uuid2) inactive", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_get_crew_status(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"name": "TestCrew", "status": "active"}
|
||||||
|
self.mock_client.status_by_name.return_value = mock_response
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command.get_crew_status()
|
||||||
|
self.assertIn("TestCrew", fake_out.getvalue())
|
||||||
|
self.assertIn("active", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_get_crew_logs(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = [
|
||||||
|
{"timestamp": "2023-01-01", "level": "INFO", "message": "Log1"},
|
||||||
|
{"timestamp": "2023-01-02", "level": "ERROR", "message": "Log2"},
|
||||||
|
]
|
||||||
|
self.mock_client.logs_by_name.return_value = mock_response
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command.get_crew_logs(None)
|
||||||
|
self.assertIn("2023-01-01 - INFO: Log1", fake_out.getvalue())
|
||||||
|
self.assertIn("2023-01-02 - ERROR: Log2", fake_out.getvalue())
|
||||||
|
|
||||||
|
def test_remove_crew(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 204
|
||||||
|
self.mock_client.delete_by_name.return_value = mock_response
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
self.deploy_command.remove_crew(None)
|
||||||
|
self.assertIn(
|
||||||
|
"Crew 'test_project' removed successfully", fake_out.getvalue()
|
||||||
|
)
|
||||||
@@ -8,7 +8,6 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pydantic_core
|
import pydantic_core
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from crewai.agent import Agent
|
from crewai.agent import Agent
|
||||||
from crewai.agents.cache import CacheHandler
|
from crewai.agents.cache import CacheHandler
|
||||||
from crewai.crew import Crew
|
from crewai.crew import Crew
|
||||||
|
|||||||
@@ -25,14 +25,20 @@ def mock_crew_factory():
|
|||||||
MockCrewClass = type("MockCrew", (MagicMock, Crew), {})
|
MockCrewClass = type("MockCrew", (MagicMock, Crew), {})
|
||||||
|
|
||||||
class MockCrew(MockCrewClass):
|
class MockCrew(MockCrewClass):
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self):
|
||||||
result = MockCrewClass()
|
result = MockCrewClass()
|
||||||
result.kickoff_async = self.kickoff_async
|
result.kickoff_async = self.kickoff_async
|
||||||
result.name = self.name
|
result.name = self.name
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def copy(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
return self
|
||||||
|
|
||||||
crew = MockCrew()
|
crew = MockCrew()
|
||||||
crew.name = name
|
crew.name = name
|
||||||
|
|
||||||
task_output = TaskOutput(
|
task_output = TaskOutput(
|
||||||
description="Test task", raw="Task output", agent="Test Agent"
|
description="Test task", raw="Task output", agent="Test Agent"
|
||||||
)
|
)
|
||||||
@@ -44,9 +50,15 @@ def mock_crew_factory():
|
|||||||
pydantic=pydantic_output,
|
pydantic=pydantic_output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_kickoff(inputs=None):
|
async def kickoff_async(inputs=None):
|
||||||
return crew_output
|
return crew_output
|
||||||
|
|
||||||
|
# Create an AsyncMock for kickoff_async
|
||||||
|
crew.kickoff_async = AsyncMock(side_effect=kickoff_async)
|
||||||
|
|
||||||
|
# Mock the synchronous kickoff method
|
||||||
|
crew.kickoff = MagicMock(return_value=crew_output)
|
||||||
|
|
||||||
# Add more attributes that Procedure might be expecting
|
# Add more attributes that Procedure might be expecting
|
||||||
crew.verbose = False
|
crew.verbose = False
|
||||||
crew.output_log_file = None
|
crew.output_log_file = None
|
||||||
@@ -56,30 +68,16 @@ def mock_crew_factory():
|
|||||||
crew.config = None
|
crew.config = None
|
||||||
crew.cache = True
|
crew.cache = True
|
||||||
|
|
||||||
# # Create a valid Agent instance
|
# Add non-empty agents and tasks
|
||||||
mock_agent = Agent(
|
mock_agent = MagicMock(spec=Agent)
|
||||||
name="Mock Agent",
|
mock_task = MagicMock(spec=Task)
|
||||||
role="Mock Role",
|
mock_task.agent = mock_agent
|
||||||
goal="Mock Goal",
|
mock_task.async_execution = False
|
||||||
backstory="Mock Backstory",
|
mock_task.context = None
|
||||||
allow_delegation=False,
|
|
||||||
verbose=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a valid Task instance
|
|
||||||
mock_task = Task(
|
|
||||||
description="Return: Test output",
|
|
||||||
expected_output="Test output",
|
|
||||||
agent=mock_agent,
|
|
||||||
async_execution=False,
|
|
||||||
context=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
crew.agents = [mock_agent]
|
crew.agents = [mock_agent]
|
||||||
crew.tasks = [mock_task]
|
crew.tasks = [mock_task]
|
||||||
|
|
||||||
crew.kickoff_async = AsyncMock(side_effect=async_kickoff)
|
|
||||||
|
|
||||||
return crew
|
return crew
|
||||||
|
|
||||||
return _create_mock_crew
|
return _create_mock_crew
|
||||||
@@ -115,9 +113,7 @@ def mock_router_factory(mock_crew_factory):
|
|||||||
(
|
(
|
||||||
"route1"
|
"route1"
|
||||||
if x.get("score", 0) > 80
|
if x.get("score", 0) > 80
|
||||||
else "route2"
|
else "route2" if x.get("score", 0) > 50 else "default"
|
||||||
if x.get("score", 0) > 50
|
|
||||||
else "default"
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -477,31 +473,17 @@ async def test_pipeline_with_parallel_stages_end_in_single_stage(mock_crew_facto
|
|||||||
"""
|
"""
|
||||||
Test that Pipeline correctly handles parallel stages.
|
Test that Pipeline correctly handles parallel stages.
|
||||||
"""
|
"""
|
||||||
crew1 = Crew(name="Crew 1", tasks=[task], agents=[agent])
|
crew1 = mock_crew_factory(name="Crew 1")
|
||||||
crew2 = Crew(name="Crew 2", tasks=[task], agents=[agent])
|
crew2 = mock_crew_factory(name="Crew 2")
|
||||||
crew3 = Crew(name="Crew 3", tasks=[task], agents=[agent])
|
crew3 = mock_crew_factory(name="Crew 3")
|
||||||
crew4 = Crew(name="Crew 4", tasks=[task], agents=[agent])
|
crew4 = mock_crew_factory(name="Crew 4")
|
||||||
|
|
||||||
pipeline = Pipeline(stages=[crew1, [crew2, crew3], crew4])
|
pipeline = Pipeline(stages=[crew1, [crew2, crew3], crew4])
|
||||||
input_data = [{"initial": "data"}]
|
input_data = [{"initial": "data"}]
|
||||||
|
|
||||||
pipeline_result = await pipeline.kickoff(input_data)
|
pipeline_result = await pipeline.kickoff(input_data)
|
||||||
|
|
||||||
with patch.object(Crew, "kickoff_async") as mock_kickoff:
|
crew1.kickoff_async.assert_called_once_with(inputs={"initial": "data"})
|
||||||
mock_kickoff.return_value = CrewOutput(
|
|
||||||
raw="Test output",
|
|
||||||
tasks_output=[
|
|
||||||
TaskOutput(
|
|
||||||
description="Test task", raw="Task output", agent="Test Agent"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
token_usage=DEFAULT_TOKEN_USAGE,
|
|
||||||
json_dict=None,
|
|
||||||
pydantic=None,
|
|
||||||
)
|
|
||||||
pipeline_result = await pipeline.kickoff(input_data)
|
|
||||||
|
|
||||||
mock_kickoff.assert_called_with(inputs={"initial": "data"})
|
|
||||||
|
|
||||||
assert len(pipeline_result) == 1
|
assert len(pipeline_result) == 1
|
||||||
pipeline_result_1 = pipeline_result[0]
|
pipeline_result_1 = pipeline_result[0]
|
||||||
@@ -649,33 +631,21 @@ Options:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pipeline_data_accumulation():
|
async def test_pipeline_data_accumulation(mock_crew_factory):
|
||||||
crew1 = Crew(name="Crew 1", tasks=[task], agents=[agent])
|
crew1 = mock_crew_factory(name="Crew 1", output_json_dict={"key1": "value1"})
|
||||||
crew2 = Crew(name="Crew 2", tasks=[task], agents=[agent])
|
crew2 = mock_crew_factory(name="Crew 2", output_json_dict={"key2": "value2"})
|
||||||
|
|
||||||
pipeline = Pipeline(stages=[crew1, crew2])
|
pipeline = Pipeline(stages=[crew1, crew2])
|
||||||
input_data = [{"initial": "data"}]
|
input_data = [{"initial": "data"}]
|
||||||
results = await pipeline.kickoff(input_data)
|
results = await pipeline.kickoff(input_data)
|
||||||
|
|
||||||
with patch.object(Crew, "kickoff_async") as mock_kickoff:
|
# Check that crew1 was called with only the initial input
|
||||||
mock_kickoff.side_effect = [
|
crew1.kickoff_async.assert_called_once_with(inputs={"initial": "data"})
|
||||||
CrewOutput(
|
|
||||||
raw="Test output from Crew 1",
|
|
||||||
tasks_output=[],
|
|
||||||
token_usage=DEFAULT_TOKEN_USAGE,
|
|
||||||
json_dict={"key1": "value1"},
|
|
||||||
pydantic=None,
|
|
||||||
),
|
|
||||||
CrewOutput(
|
|
||||||
raw="Test output from Crew 2",
|
|
||||||
tasks_output=[],
|
|
||||||
token_usage=DEFAULT_TOKEN_USAGE,
|
|
||||||
json_dict={"key2": "value2"},
|
|
||||||
pydantic=None,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
results = await pipeline.kickoff(input_data)
|
# Check that crew2 was called with the combined input from the initial data and crew1's output
|
||||||
|
crew2.kickoff_async.assert_called_once_with(
|
||||||
|
inputs={"initial": "data", "key1": "value1"}
|
||||||
|
)
|
||||||
|
|
||||||
# Check the final output
|
# Check the final output
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ class SimpleCrew:
|
|||||||
def simple_task(self):
|
def simple_task(self):
|
||||||
return Task(description="Simple Description", expected_output="Simple Output")
|
return Task(description="Simple Description", expected_output="Simple Output")
|
||||||
|
|
||||||
|
@task
|
||||||
|
def custom_named_task(self):
|
||||||
|
return Task(
|
||||||
|
description="Simple Description",
|
||||||
|
expected_output="Simple Output",
|
||||||
|
name="Custom",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_agent_memoization():
|
def test_agent_memoization():
|
||||||
crew = SimpleCrew()
|
crew = SimpleCrew()
|
||||||
@@ -33,3 +41,15 @@ def test_task_memoization():
|
|||||||
assert (
|
assert (
|
||||||
first_call_result is second_call_result
|
first_call_result is second_call_result
|
||||||
), "Task memoization is not working as expected"
|
), "Task memoization is not working as expected"
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_name():
|
||||||
|
simple_task = SimpleCrew().simple_task()
|
||||||
|
assert (
|
||||||
|
simple_task.name == "simple_task"
|
||||||
|
), "Task name is not inferred from function name as expected"
|
||||||
|
|
||||||
|
custom_named_task = SimpleCrew().custom_named_task()
|
||||||
|
assert (
|
||||||
|
custom_named_task.name == "Custom"
|
||||||
|
), "Custom task name is not being set as expected"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Test Agent creation and execution basic functionality."""
|
"""Test Agent creation and execution basic functionality."""
|
||||||
|
|
||||||
import os
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -98,6 +98,7 @@ def test_task_callback():
|
|||||||
task_completed = MagicMock(return_value="done")
|
task_completed = MagicMock(return_value="done")
|
||||||
|
|
||||||
task = Task(
|
task = Task(
|
||||||
|
name="Brainstorm",
|
||||||
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
||||||
expected_output="Bullet point list of 5 interesting ideas.",
|
expected_output="Bullet point list of 5 interesting ideas.",
|
||||||
agent=researcher,
|
agent=researcher,
|
||||||
@@ -109,6 +110,10 @@ def test_task_callback():
|
|||||||
task.execute_sync(agent=researcher)
|
task.execute_sync(agent=researcher)
|
||||||
task_completed.assert_called_once_with(task.output)
|
task_completed.assert_called_once_with(task.output)
|
||||||
|
|
||||||
|
assert task.output.description == task.description
|
||||||
|
assert task.output.expected_output == task.expected_output
|
||||||
|
assert task.output.name == task.name
|
||||||
|
|
||||||
|
|
||||||
def test_task_callback_returns_task_output():
|
def test_task_callback_returns_task_output():
|
||||||
from crewai.tasks.output_format import OutputFormat
|
from crewai.tasks.output_format import OutputFormat
|
||||||
@@ -149,6 +154,8 @@ def test_task_callback_returns_task_output():
|
|||||||
"json_dict": None,
|
"json_dict": None,
|
||||||
"agent": researcher.role,
|
"agent": researcher.role,
|
||||||
"summary": "Give me a list of 5 interesting ideas to explore...",
|
"summary": "Give me a list of 5 interesting ideas to explore...",
|
||||||
|
"name": None,
|
||||||
|
"expected_output": "Bullet point list of 5 interesting ideas.",
|
||||||
"output_format": OutputFormat.RAW,
|
"output_format": OutputFormat.RAW,
|
||||||
}
|
}
|
||||||
assert output_dict == expected_output
|
assert output_dict == expected_output
|
||||||
@@ -696,7 +703,7 @@ def test_task_definition_based_on_dict():
|
|||||||
"expected_output": "The score of the title.",
|
"expected_output": "The score of the title.",
|
||||||
}
|
}
|
||||||
|
|
||||||
task = Task(config=config)
|
task = Task(**config)
|
||||||
|
|
||||||
assert task.description == config["description"]
|
assert task.description == config["description"]
|
||||||
assert task.expected_output == config["expected_output"]
|
assert task.expected_output == config["expected_output"]
|
||||||
@@ -709,7 +716,7 @@ def test_conditional_task_definition_based_on_dict():
|
|||||||
"expected_output": "The score of the title.",
|
"expected_output": "The score of the title.",
|
||||||
}
|
}
|
||||||
|
|
||||||
task = ConditionalTask(config=config, condition=lambda x: True)
|
task = ConditionalTask(**config, condition=lambda x: True)
|
||||||
|
|
||||||
assert task.description == config["description"]
|
assert task.description == config["description"]
|
||||||
assert task.expected_output == config["expected_output"]
|
assert task.expected_output == config["expected_output"]
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ from langchain_openai import ChatOpenAI
|
|||||||
from crewai.agent import Agent
|
from crewai.agent import Agent
|
||||||
from crewai.task import Task
|
from crewai.task import Task
|
||||||
from crewai.tasks.task_output import TaskOutput
|
from crewai.tasks.task_output import TaskOutput
|
||||||
from crewai.utilities.planning_handler import CrewPlanner, PlannerTaskPydanticOutput
|
from crewai.utilities.planning_handler import (
|
||||||
|
CrewPlanner,
|
||||||
|
PlannerTaskPydanticOutput,
|
||||||
|
PlanPerTask,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCrewPlanner:
|
class TestCrewPlanner:
|
||||||
@@ -44,12 +48,17 @@ class TestCrewPlanner:
|
|||||||
return CrewPlanner(tasks, planning_agent_llm)
|
return CrewPlanner(tasks, planning_agent_llm)
|
||||||
|
|
||||||
def test_handle_crew_planning(self, crew_planner):
|
def test_handle_crew_planning(self, crew_planner):
|
||||||
|
list_of_plans_per_task = [
|
||||||
|
PlanPerTask(task="Task1", plan="Plan 1"),
|
||||||
|
PlanPerTask(task="Task2", plan="Plan 2"),
|
||||||
|
PlanPerTask(task="Task3", plan="Plan 3"),
|
||||||
|
]
|
||||||
with patch.object(Task, "execute_sync") as execute:
|
with patch.object(Task, "execute_sync") as execute:
|
||||||
execute.return_value = TaskOutput(
|
execute.return_value = TaskOutput(
|
||||||
description="Description",
|
description="Description",
|
||||||
agent="agent",
|
agent="agent",
|
||||||
pydantic=PlannerTaskPydanticOutput(
|
pydantic=PlannerTaskPydanticOutput(
|
||||||
list_of_plans_per_task=["Plan 1", "Plan 2", "Plan 3"]
|
list_of_plans_per_task=list_of_plans_per_task
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
result = crew_planner._handle_crew_planning()
|
result = crew_planner._handle_crew_planning()
|
||||||
@@ -91,7 +100,9 @@ class TestCrewPlanner:
|
|||||||
execute.return_value = TaskOutput(
|
execute.return_value = TaskOutput(
|
||||||
description="Description",
|
description="Description",
|
||||||
agent="agent",
|
agent="agent",
|
||||||
pydantic=PlannerTaskPydanticOutput(list_of_plans_per_task=["Plan 1"]),
|
pydantic=PlannerTaskPydanticOutput(
|
||||||
|
list_of_plans_per_task=[PlanPerTask(task="Task1", plan="Plan 1")]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
result = crew_planner_different_llm._handle_crew_planning()
|
result = crew_planner_different_llm._handle_crew_planning()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user