Compare commits

...

8 Commits

Author SHA1 Message Date
Devin AI
9460e5e182 fix: use huggingface_hub InferenceClient for HuggingFace embeddings
Fixes #4145

The HuggingFace embedder was failing with 'could not convert string to float: error'
because chromadb's HuggingFaceEmbeddingFunction uses the deprecated
api-inference.huggingface.co endpoint which returns error messages instead of embeddings.

This fix creates a custom HuggingFaceEmbeddingFunction that uses huggingface_hub's
InferenceClient with provider='hf-inference' instead of the deprecated endpoint.

Changes:
- Add custom embedding_callable.py using huggingface_hub.InferenceClient
- Update HuggingFaceProvider to use the new embedding callable
- Handle different embedding response formats (1D, 2D, 3D arrays)
- Add comprehensive error handling with actionable error messages
- Add 16 test cases covering initialization, embedding generation, error handling,
  and ChromaDB integration

Co-Authored-By: João <joao@crewai.com>
2025-12-22 22:44:13 +00:00
Heitor Carvalho
be70a04153 fix: correct error fetching for workos login polling (#4124)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
2025-12-19 20:00:26 -03:00
Greyson LaLonde
0c359f4df8 feat: bump versions to 1.7.2
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
2025-12-19 15:47:00 -05:00
Lucas Gomide
fe288dbe73 Resolving some connection issues (#4129)
* fix: use CREWAI_PLUS_URL env var in precedence over PlusAPI configured value

* feat: bypass TLS certificate verification when calling platform

* test: fix test
2025-12-19 10:15:20 -05:00
Heitor Carvalho
dc63bc2319 chore: remove CREWAI_BASE_URL and fetch url from settings instead
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2025-12-18 15:41:38 -03:00
Greyson LaLonde
8d0effafec chore: add commitizen pre-commit hook
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2025-12-17 15:49:24 -05:00
Greyson LaLonde
1cdbe79b34 chore: add deployment action, trigger for releases 2025-12-17 08:40:14 -05:00
Lorenze Jay
84328d9311 fixed api-reference/status docs page (#4109)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2025-12-16 15:31:30 -08:00
34 changed files with 1007 additions and 234 deletions

View File

@@ -1,9 +1,14 @@
name: Publish to PyPI
on:
release:
types: [ published ]
repository_dispatch:
types: [deployment-tests-passed]
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag to publish'
required: false
type: string
jobs:
build:
@@ -12,7 +17,21 @@ jobs:
permissions:
contents: read
steps:
- name: Determine release tag
id: release
run: |
# Priority: workflow_dispatch input > repository_dispatch payload > default branch
if [ -n "${{ inputs.release_tag }}" ]; then
echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT
elif [ -n "${{ github.event.client_payload.release_tag }}" ]; then
echo "tag=${{ github.event.client_payload.release_tag }}" >> $GITHUB_OUTPUT
else
echo "tag=" >> $GITHUB_OUTPUT
fi
- uses: actions/checkout@v4
with:
ref: ${{ steps.release.outputs.tag || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5

View File

@@ -0,0 +1,18 @@
name: Trigger Deployment Tests
on:
release:
types: [published]
jobs:
trigger:
name: Trigger deployment tests
runs-on: ubuntu-latest
steps:
- name: Trigger deployment tests
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.CREWAI_DEPLOYMENTS_PAT }}
repository: ${{ secrets.CREWAI_DEPLOYMENTS_REPOSITORY }}
event-type: crewai-release
client-payload: '{"release_tag": "${{ github.event.release.tag_name }}", "release_name": "${{ github.event.release.name }}"}'

View File

@@ -24,4 +24,10 @@ repos:
rev: 0.9.3
hooks:
- id: uv-lock
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.10.1
hooks:
- id: commitizen
- id: commitizen-branch
stages: [ pre-push ]

View File

@@ -16,16 +16,17 @@ Welcome to the CrewAI AOP API reference. This API allows you to programmatically
Navigate to your crew's detail page in the CrewAI AOP dashboard and copy your Bearer Token from the Status tab.
</Step>
<Step title="Discover Required Inputs">
Use the `GET /inputs` endpoint to see what parameters your crew expects.
</Step>
<Step title="Discover Required Inputs">
Use the `GET /inputs` endpoint to see what parameters your crew expects.
</Step>
<Step title="Start a Crew Execution">
Call `POST /kickoff` with your inputs to start the crew execution and receive a `kickoff_id`.
</Step>
<Step title="Start a Crew Execution">
Call `POST /kickoff` with your inputs to start the crew execution and receive
a `kickoff_id`.
</Step>
<Step title="Monitor Progress">
Use `GET /status/{kickoff_id}` to check execution status and retrieve results.
Use `GET /{kickoff_id}/status` to check execution status and retrieve results.
</Step>
</Steps>
@@ -40,13 +41,14 @@ curl -H "Authorization: Bearer YOUR_CREW_TOKEN" \
### Token Types
| Token Type | Scope | Use Case |
|:-----------|:--------|:----------|
| **Bearer Token** | Organization-level access | Full crew operations, ideal for server-to-server integration |
| **User Bearer Token** | User-scoped access | Limited permissions, suitable for user-specific operations |
| Token Type | Scope | Use Case |
| :-------------------- | :------------------------ | :----------------------------------------------------------- |
| **Bearer Token** | Organization-level access | Full crew operations, ideal for server-to-server integration |
| **User Bearer Token** | User-scoped access | Limited permissions, suitable for user-specific operations |
<Tip>
You can find both token types in the Status tab of your crew's detail page in the CrewAI AOP dashboard.
You can find both token types in the Status tab of your crew's detail page in
the CrewAI AOP dashboard.
</Tip>
## Base URL
@@ -63,29 +65,33 @@ Replace `your-crew-name` with your actual crew's URL from the dashboard.
1. **Discovery**: Call `GET /inputs` to understand what your crew needs
2. **Execution**: Submit inputs via `POST /kickoff` to start processing
3. **Monitoring**: Poll `GET /status/{kickoff_id}` until completion
3. **Monitoring**: Poll `GET /{kickoff_id}/status` until completion
4. **Results**: Extract the final output from the completed response
## Error Handling
The API uses standard HTTP status codes:
| Code | Meaning |
|------|:--------|
| `200` | Success |
| `400` | Bad Request - Invalid input format |
| `401` | Unauthorized - Invalid bearer token |
| `404` | Not Found - Resource doesn't exist |
| Code | Meaning |
| ----- | :----------------------------------------- |
| `200` | Success |
| `400` | Bad Request - Invalid input format |
| `401` | Unauthorized - Invalid bearer token |
| `404` | Not Found - Resource doesn't exist |
| `422` | Validation Error - Missing required inputs |
| `500` | Server Error - Contact support |
| `500` | Server Error - Contact support |
## Interactive Testing
<Info>
**Why no "Send" button?** Since each CrewAI AOP user has their own unique crew URL, we use **reference mode** instead of an interactive playground to avoid confusion. This shows you exactly what the requests should look like without non-functional send buttons.
**Why no "Send" button?** Since each CrewAI AOP user has their own unique crew
URL, we use **reference mode** instead of an interactive playground to avoid
confusion. This shows you exactly what the requests should look like without
non-functional send buttons.
</Info>
Each endpoint page shows you:
- ✅ **Exact request format** with all parameters
- ✅ **Response examples** for success and error cases
- ✅ **Code samples** in multiple languages (cURL, Python, JavaScript, etc.)
@@ -103,6 +109,7 @@ Each endpoint page shows you:
</CardGroup>
**Example workflow:**
1. **Copy this cURL example** from any endpoint page
2. **Replace `your-actual-crew-name.crewai.com`** with your real crew URL
3. **Replace the Bearer token** with your real token from the dashboard
@@ -111,10 +118,18 @@ Each endpoint page shows you:
## Need Help?
<CardGroup cols={2}>
<Card title="Enterprise Support" icon="headset" href="mailto:support@crewai.com">
<Card
title="Enterprise Support"
icon="headset"
href="mailto:support@crewai.com"
>
Get help with API integration and troubleshooting
</Card>
<Card title="Enterprise Dashboard" icon="chart-line" href="https://app.crewai.com">
<Card
title="Enterprise Dashboard"
icon="chart-line"
href="https://app.crewai.com"
>
Manage your crews and view execution logs
</Card>
</CardGroup>

View File

@@ -1,8 +1,6 @@
---
title: "GET /status/{kickoff_id}"
title: "GET /{kickoff_id}/status"
description: "Get execution status"
openapi: "/enterprise-api.en.yaml GET /status/{kickoff_id}"
openapi: "/enterprise-api.en.yaml GET /{kickoff_id}/status"
mode: "wide"
---

View File

@@ -35,7 +35,7 @@ info:
1. **Discover inputs** using `GET /inputs`
2. **Start execution** using `POST /kickoff`
3. **Monitor progress** using `GET /status/{kickoff_id}`
3. **Monitor progress** using `GET /{kickoff_id}/status`
version: 1.0.0
contact:
name: CrewAI Support
@@ -63,7 +63,7 @@ paths:
Use this endpoint to discover what inputs you need to provide when starting a crew execution.
operationId: getRequiredInputs
responses:
'200':
"200":
description: Successfully retrieved required inputs
content:
application/json:
@@ -84,13 +84,21 @@ paths:
outreach_crew:
summary: Outreach crew inputs
value:
inputs: ["name", "title", "company", "industry", "our_product", "linkedin_url"]
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'500':
$ref: '#/components/responses/ServerError'
inputs:
[
"name",
"title",
"company",
"industry",
"our_product",
"linkedin_url",
]
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/ServerError"
/kickoff:
post:
@@ -170,7 +178,7 @@ paths:
taskWebhookUrl: "https://api.example.com/webhooks/task"
crewWebhookUrl: "https://api.example.com/webhooks/crew"
responses:
'200':
"200":
description: Crew execution started successfully
content:
application/json:
@@ -182,24 +190,24 @@ paths:
format: uuid
description: Unique identifier for tracking this execution
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
'400':
"400":
description: Invalid request body or missing required inputs
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/UnauthorizedError'
'422':
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/UnauthorizedError"
"422":
description: Validation error - ensure all required inputs are provided
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
'500':
$ref: '#/components/responses/ServerError'
$ref: "#/components/schemas/ValidationError"
"500":
$ref: "#/components/responses/ServerError"
/status/{kickoff_id}:
/{kickoff_id}/status:
get:
summary: Get Execution Status
description: |
@@ -222,15 +230,15 @@ paths:
format: uuid
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
responses:
'200':
"200":
description: Successfully retrieved execution status
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ExecutionRunning'
- $ref: '#/components/schemas/ExecutionCompleted'
- $ref: '#/components/schemas/ExecutionError'
- $ref: "#/components/schemas/ExecutionRunning"
- $ref: "#/components/schemas/ExecutionCompleted"
- $ref: "#/components/schemas/ExecutionError"
examples:
running:
summary: Execution in progress
@@ -262,19 +270,19 @@ paths:
status: "error"
error: "Task execution failed: Invalid API key for external service"
execution_time: 23.1
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
description: Kickoff ID not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Execution not found"
message: "No execution found with ID: abcd1234-5678-90ef-ghij-klmnopqrstuv"
'500':
$ref: '#/components/responses/ServerError'
"500":
$ref: "#/components/responses/ServerError"
/resume:
post:
@@ -354,7 +362,7 @@ paths:
taskWebhookUrl: "https://api.example.com/webhooks/task"
crewWebhookUrl: "https://api.example.com/webhooks/crew"
responses:
'200':
"200":
description: Execution resumed successfully
content:
application/json:
@@ -381,28 +389,28 @@ paths:
value:
status: "retrying"
message: "Task will be retried with your feedback"
'400':
"400":
description: Invalid request body or execution not in pending state
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Invalid Request"
message: "Execution is not in pending human input state"
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
description: Execution ID or Task ID not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Not Found"
message: "Execution ID not found"
'500':
$ref: '#/components/responses/ServerError'
"500":
$ref: "#/components/responses/ServerError"
components:
securitySchemes:
@@ -458,7 +466,7 @@ components:
tasks:
type: array
items:
$ref: '#/components/schemas/TaskResult'
$ref: "#/components/schemas/TaskResult"
execution_time:
type: number
description: Total execution time in seconds
@@ -536,7 +544,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Unauthorized"
message: "Invalid or missing bearer token"
@@ -546,7 +554,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Not Found"
message: "The requested resource was not found"
@@ -556,7 +564,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Internal Server Error"
message: "An unexpected error occurred"

View File

@@ -35,7 +35,7 @@ info:
1. **Discover inputs** using `GET /inputs`
2. **Start execution** using `POST /kickoff`
3. **Monitor progress** using `GET /status/{kickoff_id}`
3. **Monitor progress** using `GET /{kickoff_id}/status`
version: 1.0.0
contact:
name: CrewAI Support
@@ -63,7 +63,7 @@ paths:
Use this endpoint to discover what inputs you need to provide when starting a crew execution.
operationId: getRequiredInputs
responses:
'200':
"200":
description: Successfully retrieved required inputs
content:
application/json:
@@ -84,13 +84,21 @@ paths:
outreach_crew:
summary: Outreach crew inputs
value:
inputs: ["name", "title", "company", "industry", "our_product", "linkedin_url"]
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'500':
$ref: '#/components/responses/ServerError'
inputs:
[
"name",
"title",
"company",
"industry",
"our_product",
"linkedin_url",
]
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/ServerError"
/kickoff:
post:
@@ -170,7 +178,7 @@ paths:
taskWebhookUrl: "https://api.example.com/webhooks/task"
crewWebhookUrl: "https://api.example.com/webhooks/crew"
responses:
'200':
"200":
description: Crew execution started successfully
content:
application/json:
@@ -182,24 +190,24 @@ paths:
format: uuid
description: Unique identifier for tracking this execution
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
'400':
"400":
description: Invalid request body or missing required inputs
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/UnauthorizedError'
'422':
$ref: "#/components/schemas/Error"
"401":
$ref: "#/components/responses/UnauthorizedError"
"422":
description: Validation error - ensure all required inputs are provided
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
'500':
$ref: '#/components/responses/ServerError'
$ref: "#/components/schemas/ValidationError"
"500":
$ref: "#/components/responses/ServerError"
/status/{kickoff_id}:
/{kickoff_id}/status:
get:
summary: Get Execution Status
description: |
@@ -222,15 +230,15 @@ paths:
format: uuid
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
responses:
'200':
"200":
description: Successfully retrieved execution status
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ExecutionRunning'
- $ref: '#/components/schemas/ExecutionCompleted'
- $ref: '#/components/schemas/ExecutionError'
- $ref: "#/components/schemas/ExecutionRunning"
- $ref: "#/components/schemas/ExecutionCompleted"
- $ref: "#/components/schemas/ExecutionError"
examples:
running:
summary: Execution in progress
@@ -262,19 +270,19 @@ paths:
status: "error"
error: "Task execution failed: Invalid API key for external service"
execution_time: 23.1
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
description: Kickoff ID not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Execution not found"
message: "No execution found with ID: abcd1234-5678-90ef-ghij-klmnopqrstuv"
'500':
$ref: '#/components/responses/ServerError'
"500":
$ref: "#/components/responses/ServerError"
/resume:
post:
@@ -354,7 +362,7 @@ paths:
taskWebhookUrl: "https://api.example.com/webhooks/task"
crewWebhookUrl: "https://api.example.com/webhooks/crew"
responses:
'200':
"200":
description: Execution resumed successfully
content:
application/json:
@@ -381,28 +389,28 @@ paths:
value:
status: "retrying"
message: "Task will be retried with your feedback"
'400':
"400":
description: Invalid request body or execution not in pending state
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Invalid Request"
message: "Execution is not in pending human input state"
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
description: Execution ID or Task ID not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Not Found"
message: "Execution ID not found"
'500':
$ref: '#/components/responses/ServerError'
"500":
$ref: "#/components/responses/ServerError"
components:
securitySchemes:
@@ -458,7 +466,7 @@ components:
tasks:
type: array
items:
$ref: '#/components/schemas/TaskResult'
$ref: "#/components/schemas/TaskResult"
execution_time:
type: number
description: Total execution time in seconds
@@ -536,7 +544,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Unauthorized"
message: "Invalid or missing bearer token"
@@ -546,7 +554,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Not Found"
message: "The requested resource was not found"
@@ -556,7 +564,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Internal Server Error"
message: "An unexpected error occurred"

View File

@@ -84,7 +84,7 @@ paths:
'500':
$ref: '#/components/responses/ServerError'
/status/{kickoff_id}:
/{kickoff_id}/status:
get:
summary: 실행 상태 조회
description: |

View File

@@ -35,7 +35,7 @@ info:
1. **Descubra os inputs** usando `GET /inputs`
2. **Inicie a execução** usando `POST /kickoff`
3. **Monitore o progresso** usando `GET /status/{kickoff_id}`
3. **Monitore o progresso** usando `GET /{kickoff_id}/status`
version: 1.0.0
contact:
name: CrewAI Suporte
@@ -56,7 +56,7 @@ paths:
Retorna a lista de parâmetros de entrada que sua crew espera.
operationId: getRequiredInputs
responses:
'200':
"200":
description: Inputs requeridos obtidos com sucesso
content:
application/json:
@@ -69,12 +69,12 @@ paths:
type: string
description: Nomes dos parâmetros de entrada
example: ["budget", "interests", "duration", "age"]
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'500':
$ref: '#/components/responses/ServerError'
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/ServerError"
/kickoff:
post:
@@ -104,7 +104,7 @@ paths:
age: "35"
responses:
'200':
"200":
description: Execução iniciada com sucesso
content:
application/json:
@@ -115,12 +115,12 @@ paths:
type: string
format: uuid
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
'401':
$ref: '#/components/responses/UnauthorizedError'
'500':
$ref: '#/components/responses/ServerError'
"401":
$ref: "#/components/responses/UnauthorizedError"
"500":
$ref: "#/components/responses/ServerError"
/status/{kickoff_id}:
/{kickoff_id}/status:
get:
summary: Obter Status da Execução
description: |
@@ -136,25 +136,25 @@ paths:
type: string
format: uuid
responses:
'200':
"200":
description: Status recuperado com sucesso
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ExecutionRunning'
- $ref: '#/components/schemas/ExecutionCompleted'
- $ref: '#/components/schemas/ExecutionError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
- $ref: "#/components/schemas/ExecutionRunning"
- $ref: "#/components/schemas/ExecutionCompleted"
- $ref: "#/components/schemas/ExecutionError"
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
description: Kickoff ID não encontrado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/ServerError'
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/ServerError"
/resume:
post:
@@ -234,7 +234,7 @@ paths:
taskWebhookUrl: "https://api.example.com/webhooks/task"
crewWebhookUrl: "https://api.example.com/webhooks/crew"
responses:
'200':
"200":
description: Execution resumed successfully
content:
application/json:
@@ -261,28 +261,28 @@ paths:
value:
status: "retrying"
message: "Task will be retried with your feedback"
'400':
"400":
description: Invalid request body or execution not in pending state
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Invalid Request"
message: "Execution is not in pending human input state"
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
description: Execution ID or Task ID not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
example:
error: "Not Found"
message: "Execution ID not found"
'500':
$ref: '#/components/responses/ServerError'
"500":
$ref: "#/components/responses/ServerError"
components:
securitySchemes:
@@ -324,7 +324,7 @@ components:
tasks:
type: array
items:
$ref: '#/components/schemas/TaskResult'
$ref: "#/components/schemas/TaskResult"
execution_time:
type: number
@@ -380,16 +380,16 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
NotFoundError:
description: Recurso não encontrado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"
ServerError:
description: Erro interno do servidor
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: "#/components/schemas/Error"

View File

@@ -16,16 +16,17 @@ CrewAI 엔터프라이즈 API 참고 자료에 오신 것을 환영합니다.
CrewAI AOP 대시보드에서 자신의 crew 상세 페이지로 이동하여 Status 탭에서 Bearer Token을 복사하세요.
</Step>
<Step title="필수 입력값 확인하기">
`GET /inputs` 엔드포인트를 사용하여 crew가 기대하는 파라미터를 확인하세요.
</Step>
<Step title="필수 입력값 확인하기">
`GET /inputs` 엔드포인트를 사용하여 crew가 기대하는 파라미터를 확인하세요.
</Step>
<Step title="Crew 실행 시작하기">
입력값과 함께 `POST /kickoff`를 호출하여 crew 실행을 시작하고 `kickoff_id`를 받으세요.
</Step>
<Step title="Crew 실행 시작하기">
입력값과 함께 `POST /kickoff`를 호출하여 crew 실행을 시작하고 `kickoff_id`를
받으세요.
</Step>
<Step title="진행 상황 모니터링">
`GET /status/{kickoff_id}`를 사용하여 실행 상태를 확인하고 결과를 조회하세요.
`GET /{kickoff_id}/status`를 사용하여 실행 상태를 확인하고 결과를 조회하세요.
</Step>
</Steps>
@@ -40,13 +41,14 @@ curl -H "Authorization: Bearer YOUR_CREW_TOKEN" \
### 토큰 유형
| 토큰 유형 | 범위 | 사용 사례 |
|:-----------|:--------|:----------|
| **Bearer Token** | 조직 단위 접근 | 전체 crew 운영, 서버 간 통합에 이상적 |
| **User Bearer Token** | 사용자 범위 접근 | 제한된 권한, 사용자별 작업에 적합 |
| 토큰 유형 | 범위 | 사용 사례 |
| :-------------------- | :--------------- | :------------------------------------ |
| **Bearer Token** | 조직 단위 접근 | 전체 crew 운영, 서버 간 통합에 이상적 |
| **User Bearer Token** | 사용자 범위 접근 | 제한된 권한, 사용자별 작업에 적합 |
<Tip>
두 토큰 유형 모두 CrewAI AOP 대시보드의 crew 상세 페이지 Status 탭에서 확인할 수 있습니다.
두 토큰 유형 모두 CrewAI AOP 대시보드의 crew 상세 페이지 Status 탭에서 확인할
수 있습니다.
</Tip>
## 기본 URL
@@ -63,29 +65,33 @@ https://your-crew-name.crewai.com
1. **탐색**: `GET /inputs`를 호출하여 crew가 필요한 것을 파악합니다.
2. **실행**: `POST /kickoff`를 통해 입력값을 제출하여 처리를 시작합니다.
3. **모니터링**: 완료될 때까지 `GET /status/{kickoff_id}`를 주기적으로 조회합니다.
3. **모니터링**: 완료될 때까지 `GET /{kickoff_id}/status`를 주기적으로 조회합니다.
4. **결과**: 완료된 응답에서 최종 출력을 추출합니다.
## 오류 처리
API는 표준 HTTP 상태 코드를 사용합니다:
| 코드 | 의미 |
|------|:--------|
| `200` | 성공 |
| `400` | 잘못된 요청 - 잘못된 입력 형식 |
| `401` | 인증 실패 - 잘못된 베어러 토큰 |
| 코드 | 의미 |
| ----- | :------------------------------------ |
| `200` | 성공 |
| `400` | 잘못된 요청 - 잘못된 입력 형식 |
| `401` | 인증 실패 - 잘못된 베어러 토큰 |
| `404` | 찾을 수 없음 - 리소스가 존재하지 않음 |
| `422` | 유효성 검사 오류 - 필수 입력 누락 |
| `500` | 서버 오류 - 지원팀에 문의하십시오 |
| `422` | 유효성 검사 오류 - 필수 입력 누락 |
| `500` | 서버 오류 - 지원팀에 문의하십시오 |
## 인터랙티브 테스트
<Info>
**왜 "전송" 버튼이 없나요?** 각 CrewAI AOP 사용자는 고유한 crew URL을 가지므로, 혼동을 피하기 위해 인터랙티브 플레이그라운드 대신 **참조 모드**를 사용합니다. 이를 통해 비작동 전송 버튼 없이 요청이 어떻게 생겼는지 정확히 보여줍니다.
**왜 "전송" 버튼이 없나요?** 각 CrewAI AOP 사용자는 고유한 crew URL을
가지므로, 혼동을 피하기 위해 인터랙티브 플레이그라운드 대신 **참조 모드**를
사용합니다. 이를 통해 비작동 전송 버튼 없이 요청이 어떻게 생겼는지 정확히
보여줍니다.
</Info>
각 엔드포인트 페이지에서는 다음을 확인할 수 있습니다:
- ✅ 모든 파라미터가 포함된 **정확한 요청 형식**
- ✅ 성공 및 오류 사례에 대한 **응답 예시**
- ✅ 여러 언어(cURL, Python, JavaScript 등)로 제공되는 **코드 샘플**
@@ -103,6 +109,7 @@ API는 표준 HTTP 상태 코드를 사용합니다:
</CardGroup>
**예시 작업 흐름:**
1. **cURL 예제를 복사**합니다 (엔드포인트 페이지에서)
2. **`your-actual-crew-name.crewai.com`**을(를) 실제 crew URL로 교체합니다
3. **Bearer 토큰을** 대시보드에서 복사한 실제 토큰으로 교체합니다
@@ -111,10 +118,18 @@ API는 표준 HTTP 상태 코드를 사용합니다:
## 도움이 필요하신가요?
<CardGroup cols={2}>
<Card title="Enterprise Support" icon="headset" href="mailto:support@crewai.com">
<Card
title="Enterprise Support"
icon="headset"
href="mailto:support@crewai.com"
>
API 통합 및 문제 해결에 대한 지원을 받으세요
</Card>
<Card title="Enterprise Dashboard" icon="chart-line" href="https://app.crewai.com">
<Card
title="Enterprise Dashboard"
icon="chart-line"
href="https://app.crewai.com"
>
crew를 관리하고 실행 로그를 확인하세요
</Card>
</CardGroup>

View File

@@ -1,8 +1,6 @@
---
title: "GET /status/{kickoff_id}"
title: "GET /{kickoff_id}/status"
description: "실행 상태 조회"
openapi: "/enterprise-api.ko.yaml GET /status/{kickoff_id}"
openapi: "/enterprise-api.ko.yaml GET /{kickoff_id}/status"
mode: "wide"
---

View File

@@ -16,16 +16,17 @@ Bem-vindo à referência da API do CrewAI AOP. Esta API permite que você intera
Navegue até a página de detalhes do seu crew no painel do CrewAI AOP e copie seu Bearer Token na aba Status.
</Step>
<Step title="Descubra os Inputs Necessários">
Use o endpoint `GET /inputs` para ver quais parâmetros seu crew espera.
</Step>
<Step title="Descubra os Inputs Necessários">
Use o endpoint `GET /inputs` para ver quais parâmetros seu crew espera.
</Step>
<Step title="Inicie uma Execução de Crew">
Chame `POST /kickoff` com seus inputs para iniciar a execução do crew e receber um `kickoff_id`.
</Step>
<Step title="Inicie uma Execução de Crew">
Chame `POST /kickoff` com seus inputs para iniciar a execução do crew e
receber um `kickoff_id`.
</Step>
<Step title="Monitore o Progresso">
Use `GET /status/{kickoff_id}` para checar o status da execução e recuperar os resultados.
Use `GET /{kickoff_id}/status` para checar o status da execução e recuperar os resultados.
</Step>
</Steps>
@@ -40,13 +41,14 @@ curl -H "Authorization: Bearer YOUR_CREW_TOKEN" \
### Tipos de Token
| Tipo de Token | Escopo | Caso de Uso |
|:--------------------|:------------------------|:---------------------------------------------------------|
| **Bearer Token** | Acesso em nível de organização | Operações completas de crew, ideal para integração server-to-server |
| **User Bearer Token** | Acesso com escopo de usuário | Permissões limitadas, adequado para operações específicas de usuário |
| Tipo de Token | Escopo | Caso de Uso |
| :-------------------- | :----------------------------- | :------------------------------------------------------------------- |
| **Bearer Token** | Acesso em nível de organização | Operações completas de crew, ideal para integração server-to-server |
| **User Bearer Token** | Acesso com escopo de usuário | Permissões limitadas, adequado para operações específicas de usuário |
<Tip>
Você pode encontrar ambos os tipos de token na aba Status da página de detalhes do seu crew no painel do CrewAI AOP.
Você pode encontrar ambos os tipos de token na aba Status da página de
detalhes do seu crew no painel do CrewAI AOP.
</Tip>
## URL Base
@@ -63,29 +65,33 @@ Substitua `your-crew-name` pela URL real do seu crew no painel.
1. **Descoberta**: Chame `GET /inputs` para entender o que seu crew precisa
2. **Execução**: Envie os inputs via `POST /kickoff` para iniciar o processamento
3. **Monitoramento**: Faça polling em `GET /status/{kickoff_id}` até a conclusão
3. **Monitoramento**: Faça polling em `GET /{kickoff_id}/status` até a conclusão
4. **Resultados**: Extraia o output final da resposta concluída
## Tratamento de Erros
A API utiliza códigos de status HTTP padrão:
| Código | Significado |
|--------|:--------------------------------------|
| `200` | Sucesso |
| `400` | Requisição Inválida - Formato de input inválido |
| `401` | Não Autorizado - Bearer token inválido |
| `404` | Não Encontrado - Recurso não existe |
| Código | Significado |
| ------ | :----------------------------------------------- |
| `200` | Sucesso |
| `400` | Requisição Inválida - Formato de input inválido |
| `401` | Não Autorizado - Bearer token inválido |
| `404` | Não Encontrado - Recurso não existe |
| `422` | Erro de Validação - Inputs obrigatórios ausentes |
| `500` | Erro no Servidor - Contate o suporte |
| `500` | Erro no Servidor - Contate o suporte |
## Testes Interativos
<Info>
**Por que não há botão "Enviar"?** Como cada usuário do CrewAI AOP possui sua própria URL de crew, utilizamos o **modo referência** em vez de um playground interativo para evitar confusão. Isso mostra exatamente como as requisições devem ser feitas, sem botões de envio não funcionais.
**Por que não há botão "Enviar"?** Como cada usuário do CrewAI AOP possui sua
própria URL de crew, utilizamos o **modo referência** em vez de um playground
interativo para evitar confusão. Isso mostra exatamente como as requisições
devem ser feitas, sem botões de envio não funcionais.
</Info>
Cada página de endpoint mostra para você:
- ✅ **Formato exato da requisição** com todos os parâmetros
- ✅ **Exemplos de resposta** para casos de sucesso e erro
- ✅ **Exemplos de código** em várias linguagens (cURL, Python, JavaScript, etc.)
@@ -103,6 +109,7 @@ Cada página de endpoint mostra para você:
</CardGroup>
**Exemplo de fluxo:**
1. **Copie este exemplo cURL** de qualquer página de endpoint
2. **Substitua `your-actual-crew-name.crewai.com`** pela URL real do seu crew
3. **Substitua o Bearer token** pelo seu token real do painel
@@ -111,10 +118,18 @@ Cada página de endpoint mostra para você:
## Precisa de Ajuda?
<CardGroup cols={2}>
<Card title="Suporte Enterprise" icon="headset" href="mailto:support@crewai.com">
<Card
title="Suporte Enterprise"
icon="headset"
href="mailto:support@crewai.com"
>
Obtenha ajuda com integração da API e resolução de problemas
</Card>
<Card title="Painel Enterprise" icon="chart-line" href="https://app.crewai.com">
<Card
title="Painel Enterprise"
icon="chart-line"
href="https://app.crewai.com"
>
Gerencie seus crews e visualize logs de execução
</Card>
</CardGroup>

View File

@@ -1,8 +1,6 @@
---
title: "GET /status/{kickoff_id}"
title: "GET /{kickoff_id}/status"
description: "Obter o status da execução"
openapi: "/enterprise-api.pt-BR.yaml GET /status/{kickoff_id}"
openapi: "/enterprise-api.pt-BR.yaml GET /{kickoff_id}/status"
mode: "wide"
---

View File

@@ -12,7 +12,7 @@ dependencies = [
"pytube~=15.0.0",
"requests~=2.32.5",
"docker~=7.1.0",
"crewai==1.7.1",
"crewai==1.7.2",
"lancedb~=0.5.4",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",

View File

@@ -291,4 +291,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.7.1"
__version__ = "1.7.2"

View File

@@ -1,5 +1,5 @@
"""Crewai Enterprise Tools."""
import os
import json
import re
from typing import Any, Optional, Union, cast, get_origin
@@ -432,7 +432,11 @@ class CrewAIPlatformActionTool(BaseTool):
payload = cleaned_kwargs
response = requests.post(
url=api_url, headers=headers, json=payload, timeout=60
url=api_url,
headers=headers,
json=payload,
timeout=60,
verify=os.environ.get("CREWAI_FACTORY", "false").lower() != "true",
)
data = response.json()

View File

@@ -1,5 +1,5 @@
from typing import Any
import os
from crewai.tools import BaseTool
import requests
@@ -37,6 +37,7 @@ class CrewaiPlatformToolBuilder:
headers=headers,
timeout=30,
params={"apps": ",".join(self._apps)},
verify=os.environ.get("CREWAI_FACTORY", "false").lower() != "true",
)
response.raise_for_status()
except Exception:

View File

@@ -1,4 +1,6 @@
from typing import Union, get_args, get_origin
from unittest.mock import patch, Mock
import os
from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import (
CrewAIPlatformActionTool,
@@ -249,3 +251,109 @@ class TestSchemaProcessing:
result_type = tool._process_schema_type(test_schema, "TestFieldAllOfMixed")
assert result_type is str
class TestCrewAIPlatformActionToolVerify:
"""Test suite for SSL verification behavior based on CREWAI_FACTORY environment variable"""
def setup_method(self):
self.action_schema = {
"function": {
"name": "test_action",
"parameters": {
"properties": {
"test_param": {
"type": "string",
"description": "Test parameter"
}
},
"required": []
}
}
}
def create_test_tool(self):
return CrewAIPlatformActionTool(
description="Test action tool",
action_name="test_action",
action_schema=self.action_schema
)
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, clear=True)
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
def test_run_with_ssl_verification_default(self, mock_post):
"""Test that _run uses SSL verification by default when CREWAI_FACTORY is not set"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.return_value = {"result": "success"}
mock_post.return_value = mock_response
tool = self.create_test_tool()
tool._run(test_param="test_value")
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["verify"] is True
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "false"}, clear=True)
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
def test_run_with_ssl_verification_factory_false(self, mock_post):
"""Test that _run uses SSL verification when CREWAI_FACTORY is 'false'"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.return_value = {"result": "success"}
mock_post.return_value = mock_response
tool = self.create_test_tool()
tool._run(test_param="test_value")
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["verify"] is True
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "FALSE"}, clear=True)
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
def test_run_with_ssl_verification_factory_false_uppercase(self, mock_post):
"""Test that _run uses SSL verification when CREWAI_FACTORY is 'FALSE' (case-insensitive)"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.return_value = {"result": "success"}
mock_post.return_value = mock_response
tool = self.create_test_tool()
tool._run(test_param="test_value")
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["verify"] is True
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "true"}, clear=True)
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
def test_run_without_ssl_verification_factory_true(self, mock_post):
"""Test that _run disables SSL verification when CREWAI_FACTORY is 'true'"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.return_value = {"result": "success"}
mock_post.return_value = mock_response
tool = self.create_test_tool()
tool._run(test_param="test_value")
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["verify"] is False
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "TRUE"}, clear=True)
@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post")
def test_run_without_ssl_verification_factory_true_uppercase(self, mock_post):
"""Test that _run disables SSL verification when CREWAI_FACTORY is 'TRUE' (case-insensitive)"""
mock_response = Mock()
mock_response.ok = True
mock_response.json.return_value = {"result": "success"}
mock_post.return_value = mock_response
tool = self.create_test_tool()
tool._run(test_param="test_value")
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args.kwargs["verify"] is False

View File

@@ -258,3 +258,98 @@ class TestCrewaiPlatformToolBuilder(unittest.TestCase):
assert "simple_string" in description_text
assert "nested_object" in description_text
assert "array_prop" in description_text
class TestCrewaiPlatformToolBuilderVerify(unittest.TestCase):
"""Test suite for SSL verification behavior in CrewaiPlatformToolBuilder"""
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}, clear=True)
@patch(
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
)
def test_fetch_actions_with_ssl_verification_default(self, mock_get):
"""Test that _fetch_actions uses SSL verification by default when CREWAI_FACTORY is not set"""
mock_response = Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"actions": {}}
mock_get.return_value = mock_response
builder = CrewaiPlatformToolBuilder(apps=["github"])
builder._fetch_actions()
mock_get.assert_called_once()
call_args = mock_get.call_args
assert call_args.kwargs["verify"] is True
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "false"}, clear=True)
@patch(
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
)
def test_fetch_actions_with_ssl_verification_factory_false(self, mock_get):
"""Test that _fetch_actions uses SSL verification when CREWAI_FACTORY is 'false'"""
mock_response = Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"actions": {}}
mock_get.return_value = mock_response
builder = CrewaiPlatformToolBuilder(apps=["github"])
builder._fetch_actions()
mock_get.assert_called_once()
call_args = mock_get.call_args
assert call_args.kwargs["verify"] is True
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "FALSE"}, clear=True)
@patch(
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
)
def test_fetch_actions_with_ssl_verification_factory_false_uppercase(self, mock_get):
"""Test that _fetch_actions uses SSL verification when CREWAI_FACTORY is 'FALSE' (case-insensitive)"""
mock_response = Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"actions": {}}
mock_get.return_value = mock_response
builder = CrewaiPlatformToolBuilder(apps=["github"])
builder._fetch_actions()
mock_get.assert_called_once()
call_args = mock_get.call_args
assert call_args.kwargs["verify"] is True
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "true"}, clear=True)
@patch(
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
)
def test_fetch_actions_without_ssl_verification_factory_true(self, mock_get):
"""Test that _fetch_actions disables SSL verification when CREWAI_FACTORY is 'true'"""
mock_response = Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"actions": {}}
mock_get.return_value = mock_response
builder = CrewaiPlatformToolBuilder(apps=["github"])
builder._fetch_actions()
mock_get.assert_called_once()
call_args = mock_get.call_args
assert call_args.kwargs["verify"] is False
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token", "CREWAI_FACTORY": "TRUE"}, clear=True)
@patch(
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
)
def test_fetch_actions_without_ssl_verification_factory_true_uppercase(self, mock_get):
"""Test that _fetch_actions disables SSL verification when CREWAI_FACTORY is 'TRUE' (case-insensitive)"""
mock_response = Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"actions": {}}
mock_get.return_value = mock_response
builder = CrewaiPlatformToolBuilder(apps=["github"])
builder._fetch_actions()
mock_get.assert_called_once()
call_args = mock_get.call_args
assert call_args.kwargs["verify"] is False

View File

@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.7.1",
"crewai-tools==1.7.2",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.7.1"
__version__ = "1.7.2"
_telemetry_submitted = False

View File

@@ -149,7 +149,9 @@ class AuthenticationCommand:
return
if token_data["error"] not in ("authorization_pending", "slow_down"):
raise requests.HTTPError(token_data["error_description"])
raise requests.HTTPError(
token_data.get("error_description") or token_data.get("error")
)
time.sleep(device_code_data["interval"])
attempts += 1

View File

@@ -1,6 +1,6 @@
from typing import Any
from urllib.parse import urljoin
import os
import requests
from crewai.cli.config import Settings
@@ -33,9 +33,7 @@ class PlusAPI:
if settings.org_uuid:
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
self.base_url = (
str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
)
self.base_url = os.getenv("CREWAI_PLUS_URL") or str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
def _make_request(
self, method: str, endpoint: str, **kwargs: Any

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.7.1"
"crewai[tools]==1.7.2"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.7.1"
"crewai[tools]==1.7.2"
]
[project.scripts]

View File

@@ -12,6 +12,7 @@ from rich.console import Console
from crewai.cli import git
from crewai.cli.command import BaseCommand, PlusAPIMixin
from crewai.cli.config import Settings
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai.cli.utils import (
build_env_with_tool_repository_credentials,
extract_available_exports,
@@ -131,10 +132,13 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
self._validate_response(publish_response)
published_handle = publish_response.json()["handle"]
settings = Settings()
base_url = settings.enterprise_base_url or DEFAULT_CREWAI_ENTERPRISE_URL
console.print(
f"Successfully published `{published_handle}` ({project_version}).\n\n"
+ "⚠️ Security checks are running in the background. Your tool will be available once these are complete.\n"
+ f"You can monitor the status or access your tool here:\nhttps://app.crewai.com/crewai_plus/tools/{published_handle}",
+ f"You can monitor the status or access your tool here:\n{base_url}/crewai_plus/tools/{published_handle}",
style="bold green",
)

View File

@@ -9,6 +9,8 @@ from rich.console import Console
from rich.panel import Panel
from crewai.cli.authentication.token import AuthError, get_auth_token
from crewai.cli.config import Settings
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai.cli.plus_api import PlusAPI
from crewai.cli.version import get_crewai_version
from crewai.events.listeners.tracing.types import TraceEvent
@@ -16,7 +18,6 @@ from crewai.events.listeners.tracing.utils import (
is_tracing_enabled_in_context,
should_auto_collect_first_time_traces,
)
from crewai.utilities.constants import CREWAI_BASE_URL
logger = getLogger(__name__)
@@ -326,10 +327,12 @@ class TraceBatchManager:
if response.status_code == 200:
access_code = response.json().get("access_code", None)
console = Console()
settings = Settings()
base_url = settings.enterprise_base_url or DEFAULT_CREWAI_ENTERPRISE_URL
return_link = (
f"{CREWAI_BASE_URL}/crewai_plus/trace_batches/{self.trace_batch_id}"
f"{base_url}/crewai_plus/trace_batches/{self.trace_batch_id}"
if not self.is_current_batch_ephemeral and access_code is None
else f"{CREWAI_BASE_URL}/crewai_plus/ephemeral_trace_batches/{self.trace_batch_id}?access_code={access_code}"
else f"{base_url}/crewai_plus/ephemeral_trace_batches/{self.trace_batch_id}?access_code={access_code}"
)
if self.is_current_batch_ephemeral:

View File

@@ -1,5 +1,8 @@
"""HuggingFace embedding providers."""
from crewai.rag.embeddings.providers.huggingface.embedding_callable import (
HuggingFaceEmbeddingFunction,
)
from crewai.rag.embeddings.providers.huggingface.huggingface_provider import (
HuggingFaceProvider,
)
@@ -10,6 +13,7 @@ from crewai.rag.embeddings.providers.huggingface.types import (
__all__ = [
"HuggingFaceEmbeddingFunction",
"HuggingFaceProvider",
"HuggingFaceProviderConfig",
"HuggingFaceProviderSpec",

View File

@@ -0,0 +1,158 @@
"""HuggingFace embedding function implementation using huggingface_hub."""
from typing import Any
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings
import numpy as np
from typing_extensions import Unpack
from crewai.rag.embeddings.providers.huggingface.types import HuggingFaceProviderConfig
class HuggingFaceEmbeddingFunction(EmbeddingFunction[Documents]):
"""Embedding function for HuggingFace models using the Inference API.
This implementation uses huggingface_hub's InferenceClient instead of the
deprecated api-inference.huggingface.co endpoint that chromadb uses.
"""
def __init__(self, **kwargs: Unpack[HuggingFaceProviderConfig]) -> None:
"""Initialize HuggingFace embedding function.
Args:
**kwargs: Configuration parameters for HuggingFace.
- api_key: HuggingFace API key (optional for public models)
- model_name: Model name to use for embeddings
"""
try:
from huggingface_hub import InferenceClient
except ImportError as e:
raise ImportError(
"huggingface_hub is required for HuggingFace embeddings. "
"Install it with: uv add huggingface_hub"
) from e
self._config = kwargs
self._model_name = kwargs.get(
"model_name", "sentence-transformers/all-MiniLM-L6-v2"
)
api_key = kwargs.get("api_key")
self._client = InferenceClient(
provider="hf-inference",
token=api_key,
)
@staticmethod
def name() -> str:
"""Return the name of the embedding function for ChromaDB compatibility."""
return "huggingface"
def __call__(self, input: Documents) -> Embeddings:
"""Generate embeddings for input documents.
Args:
input: List of documents to embed.
Returns:
List of embedding vectors.
Raises:
ValueError: If the API returns an error or unexpected response format.
"""
if isinstance(input, str):
input = [input]
embeddings: list[list[float]] = []
for text in input:
embedding = self._get_embedding_for_text(text)
embeddings.append(embedding)
return embeddings
def _get_embedding_for_text(self, text: str) -> list[float]:
"""Get embedding for a single text.
Args:
text: The text to embed.
Returns:
The embedding vector.
Raises:
ValueError: If the API returns an error.
"""
try:
result = self._client.feature_extraction(
text=text,
model=self._model_name,
)
# Handle different response formats
return self._process_embedding_result(result)
except Exception as e:
error_msg = str(e)
# Provide more helpful error messages for common issues
if "deprecated" in error_msg.lower() or "no longer supported" in error_msg.lower():
raise ValueError(
f"HuggingFace API endpoint error: {error_msg}. "
"Please ensure you have the latest version of huggingface_hub installed."
) from e
if "unauthorized" in error_msg.lower() or "401" in error_msg:
raise ValueError(
f"HuggingFace API authentication error: {error_msg}. "
"Please check your API key configuration."
) from e
if "not found" in error_msg.lower() or "404" in error_msg:
raise ValueError(
f"HuggingFace model not found: {error_msg}. "
f"Please verify the model name '{self._model_name}' is correct "
"and supports feature extraction."
) from e
raise ValueError(f"HuggingFace API error: {error_msg}") from e
def _process_embedding_result(self, result: Any) -> list[float]:
"""Process the embedding result from the API.
The HuggingFace API can return different formats depending on the model:
- 1D array: Direct embedding vector
- 2D array: Token-level embeddings (needs pooling)
- Nested structure: Various model-specific formats
Args:
result: The raw result from the API.
Returns:
A 1D list of floats representing the embedding.
Raises:
ValueError: If the result format is unexpected.
"""
# Convert to numpy array for easier processing
arr = np.array(result)
# Handle different dimensionalities
if arr.ndim == 1:
# Already a 1D embedding vector
return arr.astype(np.float32).tolist()
if arr.ndim == 2:
# Token-level embeddings - apply mean pooling
pooled = np.mean(arr, axis=0)
return pooled.astype(np.float32).tolist()
if arr.ndim == 3:
# Batch of token-level embeddings - take first and apply mean pooling
pooled = np.mean(arr[0], axis=0)
return pooled.astype(np.float32).tolist()
raise ValueError(
f"Unexpected embedding result shape: {arr.shape}. "
"Expected 1D, 2D, or 3D array."
)
def get_config(self) -> dict[str, Any]:
"""Return the configuration for serialization."""
return {
"model_name": self._model_name,
"api_key": self._config.get("api_key"),
}

View File

@@ -1,11 +1,11 @@
"""HuggingFace embeddings provider."""
from chromadb.utils.embedding_functions.huggingface_embedding_function import (
HuggingFaceEmbeddingFunction,
)
from pydantic import AliasChoices, Field
from crewai.rag.core.base_embeddings_provider import BaseEmbeddingsProvider
from crewai.rag.embeddings.providers.huggingface.embedding_callable import (
HuggingFaceEmbeddingFunction,
)
class HuggingFaceProvider(BaseEmbeddingsProvider[HuggingFaceEmbeddingFunction]):

View File

@@ -30,4 +30,3 @@ NOT_SPECIFIED: Final[
"allows us to distinguish between 'not passed at all' and 'explicitly passed None' or '[]'.",
]
] = _NotSpecified()
CREWAI_BASE_URL: Final[str] = "https://app.crewai.com"

View File

@@ -1,7 +1,7 @@
import os
import unittest
from unittest.mock import ANY, MagicMock, patch
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai.cli.plus_api import PlusAPI
@@ -35,7 +35,7 @@ class TestPlusAPI(unittest.TestCase):
):
mock_make_request.assert_called_once_with(
method,
f"{DEFAULT_CREWAI_ENTERPRISE_URL}{endpoint}",
f"{os.getenv('CREWAI_PLUS_URL')}{endpoint}",
headers={
"Authorization": ANY,
"Content-Type": ANY,
@@ -53,7 +53,7 @@ class TestPlusAPI(unittest.TestCase):
):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
@@ -84,7 +84,7 @@ class TestPlusAPI(unittest.TestCase):
def test_get_agent_with_org_uuid(self, mock_make_request, mock_settings_class):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
@@ -115,7 +115,7 @@ class TestPlusAPI(unittest.TestCase):
def test_get_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
@@ -163,7 +163,7 @@ class TestPlusAPI(unittest.TestCase):
def test_publish_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
mock_settings = MagicMock()
mock_settings.org_uuid = self.org_uuid
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
mock_settings_class.return_value = mock_settings
# re-initialize Client
self.api = PlusAPI(self.api_key)
@@ -320,6 +320,7 @@ class TestPlusAPI(unittest.TestCase):
)
@patch("crewai.cli.plus_api.Settings")
@patch.dict(os.environ, {"CREWAI_PLUS_URL": ""})
def test_custom_base_url(self, mock_settings_class):
mock_settings = MagicMock()
mock_settings.enterprise_base_url = "https://custom-url.com/api"
@@ -329,3 +330,11 @@ class TestPlusAPI(unittest.TestCase):
custom_api.base_url,
"https://custom-url.com/api",
)
@patch.dict(os.environ, {"CREWAI_PLUS_URL": "https://custom-url-from-env.com"})
def test_custom_base_url_from_env(self):
custom_api = PlusAPI("test_key")
self.assertEqual(
custom_api.base_url,
"https://custom-url-from-env.com",
)

View File

@@ -0,0 +1,290 @@
"""Tests for HuggingFace embedding function."""
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from crewai.rag.embeddings.providers.huggingface.embedding_callable import (
HuggingFaceEmbeddingFunction,
)
class TestHuggingFaceEmbeddingFunction:
"""Test HuggingFace embedding function."""
@patch("huggingface_hub.InferenceClient")
def test_initialization_with_api_key(self, mock_client_class):
"""Test initialization with API key."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
ef = HuggingFaceEmbeddingFunction(
api_key="test-api-key",
model_name="sentence-transformers/all-MiniLM-L6-v2",
)
mock_client_class.assert_called_once_with(
provider="hf-inference",
token="test-api-key",
)
assert ef._model_name == "sentence-transformers/all-MiniLM-L6-v2"
@patch("huggingface_hub.InferenceClient")
def test_initialization_without_api_key(self, mock_client_class):
"""Test initialization without API key (for public models)."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
ef = HuggingFaceEmbeddingFunction(
model_name="sentence-transformers/all-MiniLM-L6-v2",
)
mock_client_class.assert_called_once_with(
provider="hf-inference",
token=None,
)
assert ef._model_name == "sentence-transformers/all-MiniLM-L6-v2"
@patch("huggingface_hub.InferenceClient")
def test_initialization_with_default_model(self, mock_client_class):
"""Test initialization with default model name."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
ef = HuggingFaceEmbeddingFunction()
assert ef._model_name == "sentence-transformers/all-MiniLM-L6-v2"
@patch("huggingface_hub.InferenceClient")
def test_call_with_single_document(self, mock_client_class):
"""Test embedding generation for a single document."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
# Mock the feature_extraction response (1D embedding)
mock_embedding = [0.1, 0.2, 0.3, 0.4, 0.5]
mock_client.feature_extraction.return_value = mock_embedding
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
result = ef(["Hello, world!"])
mock_client.feature_extraction.assert_called_once_with(
text="Hello, world!",
model="sentence-transformers/all-MiniLM-L6-v2",
)
assert len(result) == 1
assert result[0] == pytest.approx(mock_embedding, rel=1e-5)
@patch("huggingface_hub.InferenceClient")
def test_call_with_multiple_documents(self, mock_client_class):
"""Test embedding generation for multiple documents."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
# Mock the feature_extraction response
mock_embedding1 = [0.1, 0.2, 0.3]
mock_embedding2 = [0.4, 0.5, 0.6]
mock_client.feature_extraction.side_effect = [mock_embedding1, mock_embedding2]
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
result = ef(["Hello", "World"])
assert mock_client.feature_extraction.call_count == 2
assert len(result) == 2
assert result[0] == pytest.approx(mock_embedding1, rel=1e-5)
assert result[1] == pytest.approx(mock_embedding2, rel=1e-5)
@patch("huggingface_hub.InferenceClient")
def test_call_with_string_input(self, mock_client_class):
"""Test that string input is converted to list."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_embedding = [0.1, 0.2, 0.3]
mock_client.feature_extraction.return_value = mock_embedding
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
result = ef("Hello") # type: ignore[arg-type]
mock_client.feature_extraction.assert_called_once()
assert len(result) == 1
@patch("huggingface_hub.InferenceClient")
def test_process_2d_embedding_result(self, mock_client_class):
"""Test processing of 2D token-level embeddings (mean pooling)."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
# Mock 2D token-level embeddings (3 tokens, 4 dimensions each)
mock_token_embeddings = [
[0.1, 0.2, 0.3, 0.4],
[0.2, 0.3, 0.4, 0.5],
[0.3, 0.4, 0.5, 0.6],
]
mock_client.feature_extraction.return_value = mock_token_embeddings
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
result = ef(["Hello"])
# Expected: mean pooling across tokens
expected = np.mean(mock_token_embeddings, axis=0).tolist()
assert len(result) == 1
assert result[0] == pytest.approx(expected, rel=1e-5)
@patch("huggingface_hub.InferenceClient")
def test_process_3d_embedding_result(self, mock_client_class):
"""Test processing of 3D batch token-level embeddings."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
# Mock 3D embeddings (1 batch, 3 tokens, 4 dimensions)
mock_batch_embeddings = [
[
[0.1, 0.2, 0.3, 0.4],
[0.2, 0.3, 0.4, 0.5],
[0.3, 0.4, 0.5, 0.6],
]
]
mock_client.feature_extraction.return_value = mock_batch_embeddings
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
result = ef(["Hello"])
# Expected: take first batch, then mean pooling
expected = np.mean(mock_batch_embeddings[0], axis=0).tolist()
assert len(result) == 1
assert result[0] == pytest.approx(expected, rel=1e-5)
@patch("huggingface_hub.InferenceClient")
def test_error_handling_deprecated_endpoint(self, mock_client_class):
"""Test error handling for deprecated endpoint error."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.feature_extraction.side_effect = Exception(
"https://api-inference.huggingface.co is no longer supported"
)
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
with pytest.raises(ValueError, match="HuggingFace API endpoint error"):
ef(["Hello"])
@patch("huggingface_hub.InferenceClient")
def test_error_handling_unauthorized(self, mock_client_class):
"""Test error handling for authentication error."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.feature_extraction.side_effect = Exception("401 Unauthorized")
ef = HuggingFaceEmbeddingFunction(api_key="invalid-key")
with pytest.raises(ValueError, match="HuggingFace API authentication error"):
ef(["Hello"])
@patch("huggingface_hub.InferenceClient")
def test_error_handling_model_not_found(self, mock_client_class):
"""Test error handling for model not found error."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.feature_extraction.side_effect = Exception("404 Not Found")
ef = HuggingFaceEmbeddingFunction(
api_key="test-key", model_name="nonexistent/model"
)
with pytest.raises(ValueError, match="HuggingFace model not found"):
ef(["Hello"])
@patch("huggingface_hub.InferenceClient")
def test_error_handling_generic_error(self, mock_client_class):
"""Test error handling for generic API error."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.feature_extraction.side_effect = Exception("Some unexpected error")
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
with pytest.raises(ValueError, match="HuggingFace API error"):
ef(["Hello"])
@patch("huggingface_hub.InferenceClient")
def test_name_method(self, mock_client_class):
"""Test the name() static method."""
assert HuggingFaceEmbeddingFunction.name() == "huggingface"
@patch("huggingface_hub.InferenceClient")
def test_get_config(self, mock_client_class):
"""Test get_config method."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
ef = HuggingFaceEmbeddingFunction(
api_key="test-key",
model_name="custom/model",
)
config = ef.get_config()
assert config["model_name"] == "custom/model"
assert config["api_key"] == "test-key"
class TestHuggingFaceEmbeddingFunctionIntegration:
"""Integration tests for HuggingFace embedding function with RAGStorage."""
@patch("huggingface_hub.InferenceClient")
def test_embedding_function_works_with_rag_storage_validation(
self, mock_client_class
):
"""Test that the embedding function works with RAGStorage validation.
This test simulates the validation that happens in RAGStorage.__init__
where it calls embedding_function(["test"]) to verify the embedder works.
"""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
# Mock a valid embedding response
mock_embedding = [0.1] * 384 # 384 dimensions like all-MiniLM-L6-v2
mock_client.feature_extraction.return_value = mock_embedding
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
# This is what RAGStorage does to validate the embedder
result = ef(["test"])
assert len(result) == 1
assert len(result[0]) == 384
# Values should be numeric (float or numpy float)
assert all(isinstance(x, (int, float)) or hasattr(x, "__float__") for x in result[0])
@patch("huggingface_hub.InferenceClient")
def test_embedding_function_returns_correct_format_for_chromadb(
self, mock_client_class
):
"""Test that embeddings are in the correct format for ChromaDB.
ChromaDB expects embeddings as a sequence of embedding vectors where each
inner element is a 1D embedding vector with numeric values.
"""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_embedding = [0.1, 0.2, 0.3, 0.4, 0.5]
mock_client.feature_extraction.return_value = mock_embedding
ef = HuggingFaceEmbeddingFunction(api_key="test-key")
result = ef(["Hello", "World"])
# ChromaDB expects a sequence of embedding vectors
assert isinstance(result, list)
for embedding in result:
# Each embedding should be a sequence of numeric values
assert len(embedding) == 5
for value in embedding:
# Values should be numeric (float or numpy float)
assert isinstance(value, (int, float)) or hasattr(value, "__float__")

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.7.1"
__version__ = "1.7.2"