Compare commits

..

4 Commits

Author SHA1 Message Date
Devin AI
be3e7998ae fix: merge system messages into user messages for Ollama models
Some Ollama models (like Olmo, Nemotron-3-nano) don't properly respect
system messages, causing them to ignore tool-format instructions.

This fix modifies _format_messages_for_provider() to merge system message
content into the first user message for Ollama models, ensuring the
instructions are visible to the model.

Changes:
- Accumulate all system messages and merge into first user message
- Handle edge cases: system-only messages, system then assistant
- Preserve conversation history after system message merge
- Add comprehensive tests for all scenarios

Fixes #4117

Co-Authored-By: João <joao@crewai.com>
2025-12-18 11:39:51 +00: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
23 changed files with 4632 additions and 4897 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

@@ -912,14 +912,12 @@ class Agent(BaseAgent):
url=mcp_config.url,
headers=mcp_config.headers,
streamable=mcp_config.streamable,
verify=mcp_config.verify,
)
server_name = self._extract_server_name(mcp_config.url)
elif isinstance(mcp_config, MCPServerSSE):
transport = SSETransport(
url=mcp_config.url,
headers=mcp_config.headers,
verify=mcp_config.verify,
)
server_name = self._extract_server_name(mcp_config.url)
else:

View File

@@ -1919,14 +1919,50 @@ class LLM(BaseLLM):
return [*messages, {"role": "user", "content": "Please continue."}] # type: ignore[list-item]
return messages # type: ignore[return-value]
# TODO: Remove this code after merging PR https://github.com/BerriAI/litellm/pull/10917
# Ollama doesn't supports last message to be 'assistant'
if (
"ollama" in self.model.lower()
and messages
and messages[-1]["role"] == "assistant"
):
return [*messages, {"role": "user", "content": ""}] # type: ignore[list-item]
# Handle Ollama models - merge system messages into user messages
# Some Ollama models (like Olmo, Nemotron-3-nano) don't properly respect
# system messages, causing them to ignore tool-format instructions.
# By merging system content into the first user message, we ensure
# the instructions are visible to the model.
if self._get_custom_llm_provider() == "ollama":
formatted_messages: list[dict[str, str]] = []
system_contents: list[str] = []
for msg in messages:
if msg["role"] == "system":
# Accumulate all system message contents
system_contents.append(str(msg["content"]))
else:
# For the first non-system message, prepend accumulated system content
if system_contents and not formatted_messages:
merged_content = "\n\n".join(system_contents)
if msg["role"] == "user":
# Merge system content into the first user message
formatted_messages.append({
"role": "user",
"content": f"{merged_content}\n\n{msg['content']}"
})
else:
# If first non-system message isn't user, prepend a user message
formatted_messages.append({
"role": "user",
"content": merged_content
})
formatted_messages.append({"role": msg["role"], "content": str(msg["content"])})
system_contents = []
else:
formatted_messages.append({"role": msg["role"], "content": str(msg["content"])})
# Handle case where there are only system messages
if system_contents and not formatted_messages:
merged_content = "\n\n".join(system_contents)
formatted_messages.append({"role": "user", "content": merged_content})
# Ollama doesn't support last message being 'assistant'
if formatted_messages and formatted_messages[-1]["role"] == "assistant":
formatted_messages.append({"role": "user", "content": ""})
return formatted_messages
# Handle Anthropic models
if not self.is_anthropic:

View File

@@ -63,18 +63,6 @@ class MCPServerHTTP(BaseModel):
headers={"Authorization": "Bearer ..."},
cache_tools_list=True,
)
# Disable SSL verification (for self-signed certificates)
mcp_server = MCPServerHTTP(
url="https://internal-server.example.com/mcp",
verify=False,
)
# Use custom CA bundle
mcp_server = MCPServerHTTP(
url="https://internal-server.example.com/mcp",
verify="/path/to/ca-bundle.crt",
)
```
"""
@@ -89,11 +77,6 @@ class MCPServerHTTP(BaseModel):
default=True,
description="Whether to use streamable HTTP transport (default: True).",
)
verify: bool | str = Field(
default=True,
description="SSL certificate verification. Set to False to disable verification, "
"or provide a path to a CA bundle file.",
)
tool_filter: ToolFilter | None = Field(
default=None,
description="Optional tool filter for filtering available tools.",
@@ -116,18 +99,6 @@ class MCPServerSSE(BaseModel):
url="https://api.example.com/mcp/sse",
headers={"Authorization": "Bearer ..."},
)
# Disable SSL verification (for self-signed certificates)
mcp_server = MCPServerSSE(
url="https://internal-server.example.com/mcp/sse",
verify=False,
)
# Use custom CA bundle
mcp_server = MCPServerSSE(
url="https://internal-server.example.com/mcp/sse",
verify="/path/to/ca-bundle.crt",
)
```
"""
@@ -139,11 +110,6 @@ class MCPServerSSE(BaseModel):
default=None,
description="Optional HTTP headers for authentication or other purposes.",
)
verify: bool | str = Field(
default=True,
description="SSL certificate verification. Set to False to disable verification, "
"or provide a path to a CA bundle file.",
)
tool_filter: ToolFilter | None = Field(
default=None,
description="Optional tool filter for filtering available tools.",

View File

@@ -3,7 +3,6 @@
import asyncio
from typing import Any
import httpx
from typing_extensions import Self
@@ -17,48 +16,6 @@ except ImportError:
from crewai.mcp.transports.base import BaseTransport, TransportType
def _create_httpx_client_factory(
verify: bool | str,
) -> Any:
"""Create a custom httpx client factory with SSL verification settings.
This factory preserves MCP's default client settings (follow_redirects, timeout)
while allowing customization of SSL verification.
Args:
verify: SSL verification setting. True for default verification,
False to disable, or a path to a CA bundle file.
Returns:
A factory function compatible with MCP's McpHttpClientFactory protocol.
"""
def factory(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> httpx.AsyncClient:
kwargs: dict[str, Any] = {
"follow_redirects": True,
"verify": verify,
}
if timeout is None:
kwargs["timeout"] = httpx.Timeout(30.0)
else:
kwargs["timeout"] = timeout
if headers is not None:
kwargs["headers"] = headers
if auth is not None:
kwargs["auth"] = auth
return httpx.AsyncClient(**kwargs)
return factory
class HTTPTransport(BaseTransport):
"""HTTP/Streamable HTTP transport for connecting to remote MCP servers.
@@ -73,12 +30,6 @@ class HTTPTransport(BaseTransport):
)
async with transport:
# Use transport...
# With SSL verification disabled
transport = HTTPTransport(
url="https://internal-server.example.com/mcp",
verify=False
)
```
"""
@@ -87,7 +38,6 @@ class HTTPTransport(BaseTransport):
url: str,
headers: dict[str, str] | None = None,
streamable: bool = True,
verify: bool | str = True,
**kwargs: Any,
) -> None:
"""Initialize HTTP transport.
@@ -96,15 +46,12 @@ class HTTPTransport(BaseTransport):
url: Server URL (e.g., "https://api.example.com/mcp").
headers: Optional HTTP headers.
streamable: Whether to use streamable HTTP (default: True).
verify: SSL certificate verification. Set to False to disable,
or provide a path to a CA bundle file (default: True).
**kwargs: Additional transport options.
"""
super().__init__(**kwargs)
self.url = url
self.headers = headers or {}
self.streamable = streamable
self.verify = verify
self._transport_context: Any = None
@property
@@ -128,17 +75,11 @@ class HTTPTransport(BaseTransport):
try:
from mcp.client.streamable_http import streamablehttp_client
client_kwargs: dict[str, Any] = {
"headers": self.headers if self.headers else None,
"terminate_on_close": True,
}
if self.verify is not True:
client_kwargs["httpx_client_factory"] = _create_httpx_client_factory(
self.verify
)
self._transport_context = streamablehttp_client(self.url, **client_kwargs)
self._transport_context = streamablehttp_client(
self.url,
headers=self.headers if self.headers else None,
terminate_on_close=True,
)
try:
read, write, _ = await asyncio.wait_for(

View File

@@ -2,54 +2,11 @@
from typing import Any
import httpx
from typing_extensions import Self
from crewai.mcp.transports.base import BaseTransport, TransportType
def _create_httpx_client_factory(
verify: bool | str,
) -> Any:
"""Create a custom httpx client factory with SSL verification settings.
This factory preserves MCP's default client settings (follow_redirects, timeout)
while allowing customization of SSL verification.
Args:
verify: SSL verification setting. True for default verification,
False to disable, or a path to a CA bundle file.
Returns:
A factory function compatible with MCP's McpHttpClientFactory protocol.
"""
def factory(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> httpx.AsyncClient:
kwargs: dict[str, Any] = {
"follow_redirects": True,
"verify": verify,
}
if timeout is None:
kwargs["timeout"] = httpx.Timeout(30.0)
else:
kwargs["timeout"] = timeout
if headers is not None:
kwargs["headers"] = headers
if auth is not None:
kwargs["auth"] = auth
return httpx.AsyncClient(**kwargs)
return factory
class SSETransport(BaseTransport):
"""SSE transport for connecting to remote MCP servers.
@@ -64,12 +21,6 @@ class SSETransport(BaseTransport):
)
async with transport:
# Use transport...
# With SSL verification disabled
transport = SSETransport(
url="https://internal-server.example.com/mcp/sse",
verify=False
)
```
"""
@@ -77,7 +28,6 @@ class SSETransport(BaseTransport):
self,
url: str,
headers: dict[str, str] | None = None,
verify: bool | str = True,
**kwargs: Any,
) -> None:
"""Initialize SSE transport.
@@ -85,14 +35,11 @@ class SSETransport(BaseTransport):
Args:
url: Server URL (e.g., "https://api.example.com/mcp/sse").
headers: Optional HTTP headers.
verify: SSL certificate verification. Set to False to disable,
or provide a path to a CA bundle file (default: True).
**kwargs: Additional transport options.
"""
super().__init__(**kwargs)
self.url = url
self.headers = headers or {}
self.verify = verify
self._transport_context: Any = None
@property
@@ -116,16 +63,10 @@ class SSETransport(BaseTransport):
try:
from mcp.client.sse import sse_client
client_kwargs: dict[str, Any] = {
"headers": self.headers if self.headers else None,
}
if self.verify is not True:
client_kwargs["httpx_client_factory"] = _create_httpx_client_factory(
self.verify
)
self._transport_context = sse_client(self.url, **client_kwargs)
self._transport_context = sse_client(
self.url,
headers=self.headers if self.headers else None,
)
read, write = await self._transport_context.__aenter__()

View File

@@ -1,73 +0,0 @@
"""Tests for HTTP transport."""
import pytest
from crewai.mcp.transports.http import HTTPTransport, _create_httpx_client_factory
def test_http_transport_verify_default():
"""Test HTTPTransport has verify=True by default."""
transport = HTTPTransport(url="http://localhost:9999/mcp")
assert transport.verify is True
def test_http_transport_verify_false():
"""Test HTTPTransport with verify=False."""
transport = HTTPTransport(
url="http://localhost:9999/mcp",
verify=False,
)
assert transport.verify is False
def test_http_transport_verify_ca_bundle():
"""Test HTTPTransport with custom CA bundle path."""
transport = HTTPTransport(
url="http://localhost:9999/mcp",
verify="/path/to/ca-bundle.crt",
)
assert transport.verify == "/path/to/ca-bundle.crt"
def test_http_transport_streamable_default():
"""Test HTTPTransport has streamable=True by default."""
transport = HTTPTransport(url="http://localhost:9999/mcp")
assert transport.streamable is True
def test_create_httpx_client_factory_returns_async_client():
"""Test _create_httpx_client_factory returns an AsyncClient."""
import httpx
factory = _create_httpx_client_factory(verify=False)
client = factory()
assert isinstance(client, httpx.AsyncClient)
def test_create_httpx_client_factory_preserves_mcp_defaults():
"""Test _create_httpx_client_factory preserves MCP default settings."""
factory = _create_httpx_client_factory(verify=False)
client = factory(headers={"X-Test": "value"})
assert client.follow_redirects is True
assert client.timeout.connect == 30.0
assert client.headers.get("X-Test") == "value"
def test_create_httpx_client_factory_with_custom_timeout():
"""Test _create_httpx_client_factory respects custom timeout."""
import httpx
factory = _create_httpx_client_factory(verify=False)
custom_timeout = httpx.Timeout(60.0)
client = factory(timeout=custom_timeout)
assert client.timeout.connect == 60.0
def test_create_httpx_client_factory_with_auth():
"""Test _create_httpx_client_factory passes auth parameter."""
import httpx
factory = _create_httpx_client_factory(verify=False)
auth = httpx.BasicAuth(username="user", password="pass")
client = factory(auth=auth)
assert client.auth is not None

View File

@@ -198,187 +198,3 @@ async def test_mcp_tool_execution_in_async_context(mock_tool_definitions):
assert result == "test result"
mock_client.call_tool.assert_called()
def test_http_mcp_config_verify_default():
"""Test MCPServerHTTP has verify=True by default."""
http_config = MCPServerHTTP(url="https://api.example.com/mcp")
assert http_config.verify is True
def test_http_mcp_config_verify_false():
"""Test MCPServerHTTP with verify=False."""
http_config = MCPServerHTTP(
url="https://api.example.com/mcp",
verify=False,
)
assert http_config.verify is False
def test_http_mcp_config_verify_ca_bundle():
"""Test MCPServerHTTP with custom CA bundle path."""
http_config = MCPServerHTTP(
url="https://api.example.com/mcp",
verify="/path/to/ca-bundle.crt",
)
assert http_config.verify == "/path/to/ca-bundle.crt"
def test_sse_mcp_config_verify_default():
"""Test MCPServerSSE has verify=True by default."""
sse_config = MCPServerSSE(url="https://api.example.com/mcp/sse")
assert sse_config.verify is True
def test_sse_mcp_config_verify_false():
"""Test MCPServerSSE with verify=False."""
sse_config = MCPServerSSE(
url="https://api.example.com/mcp/sse",
verify=False,
)
assert sse_config.verify is False
def test_sse_mcp_config_verify_ca_bundle():
"""Test MCPServerSSE with custom CA bundle path."""
sse_config = MCPServerSSE(
url="https://api.example.com/mcp/sse",
verify="/path/to/ca-bundle.crt",
)
assert sse_config.verify == "/path/to/ca-bundle.crt"
def test_agent_with_http_mcp_config_verify_false(mock_tool_definitions):
"""Test agent setup with MCPServerHTTP configuration with verify=False."""
http_config = MCPServerHTTP(
url="https://api.example.com/mcp",
headers={"Authorization": "Bearer test_token"},
verify=False,
)
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
mcps=[http_config],
)
with patch("crewai.agent.core.MCPClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
mock_client.connected = False
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client_class.return_value = mock_client
tools = agent.get_mcp_tools([http_config])
assert len(tools) == 2
assert all(isinstance(tool, BaseTool) for tool in tools)
mock_client_class.assert_called_once()
call_args = mock_client_class.call_args
transport = call_args.kwargs["transport"]
assert transport.url == "https://api.example.com/mcp"
assert transport.headers == {"Authorization": "Bearer test_token"}
assert transport.verify is False
def test_agent_with_http_mcp_config_verify_ca_bundle(mock_tool_definitions):
"""Test agent setup with MCPServerHTTP configuration with custom CA bundle."""
http_config = MCPServerHTTP(
url="https://api.example.com/mcp",
verify="/path/to/ca-bundle.crt",
)
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
mcps=[http_config],
)
with patch("crewai.agent.core.MCPClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
mock_client.connected = False
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client_class.return_value = mock_client
tools = agent.get_mcp_tools([http_config])
assert len(tools) == 2
mock_client_class.assert_called_once()
call_args = mock_client_class.call_args
transport = call_args.kwargs["transport"]
assert transport.verify == "/path/to/ca-bundle.crt"
def test_agent_with_sse_mcp_config_verify_false(mock_tool_definitions):
"""Test agent setup with MCPServerSSE configuration with verify=False."""
sse_config = MCPServerSSE(
url="https://api.example.com/mcp/sse",
headers={"Authorization": "Bearer test_token"},
verify=False,
)
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
mcps=[sse_config],
)
with patch("crewai.agent.core.MCPClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
mock_client.connected = False
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client_class.return_value = mock_client
tools = agent.get_mcp_tools([sse_config])
assert len(tools) == 2
assert all(isinstance(tool, BaseTool) for tool in tools)
mock_client_class.assert_called_once()
call_args = mock_client_class.call_args
transport = call_args.kwargs["transport"]
assert transport.url == "https://api.example.com/mcp/sse"
assert transport.headers == {"Authorization": "Bearer test_token"}
assert transport.verify is False
def test_agent_with_sse_mcp_config_verify_ca_bundle(mock_tool_definitions):
"""Test agent setup with MCPServerSSE configuration with custom CA bundle."""
sse_config = MCPServerSSE(
url="https://api.example.com/mcp/sse",
verify="/path/to/ca-bundle.crt",
)
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
mcps=[sse_config],
)
with patch("crewai.agent.core.MCPClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
mock_client.connected = False
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client_class.return_value = mock_client
tools = agent.get_mcp_tools([sse_config])
assert len(tools) == 2
mock_client_class.assert_called_once()
call_args = mock_client_class.call_args
transport = call_args.kwargs["transport"]
assert transport.verify == "/path/to/ca-bundle.crt"

View File

@@ -2,7 +2,7 @@
import pytest
from crewai.mcp.transports.sse import SSETransport, _create_httpx_client_factory
from crewai.mcp.transports.sse import SSETransport
@pytest.mark.asyncio
@@ -19,46 +19,4 @@ async def test_sse_transport_connect_does_not_pass_invalid_args():
with pytest.raises(ConnectionError) as exc_info:
await transport.connect()
assert "unexpected keyword argument" not in str(exc_info.value)
def test_sse_transport_verify_default():
"""Test SSETransport has verify=True by default."""
transport = SSETransport(url="http://localhost:9999/sse")
assert transport.verify is True
def test_sse_transport_verify_false():
"""Test SSETransport with verify=False."""
transport = SSETransport(
url="http://localhost:9999/sse",
verify=False,
)
assert transport.verify is False
def test_sse_transport_verify_ca_bundle():
"""Test SSETransport with custom CA bundle path."""
transport = SSETransport(
url="http://localhost:9999/sse",
verify="/path/to/ca-bundle.crt",
)
assert transport.verify == "/path/to/ca-bundle.crt"
def test_create_httpx_client_factory_returns_async_client():
"""Test _create_httpx_client_factory returns an AsyncClient."""
import httpx
factory = _create_httpx_client_factory(verify=False)
client = factory()
assert isinstance(client, httpx.AsyncClient)
def test_create_httpx_client_factory_preserves_mcp_defaults():
"""Test _create_httpx_client_factory preserves MCP default settings."""
factory = _create_httpx_client_factory(verify=False)
client = factory(headers={"X-Test": "value"})
assert client.follow_redirects is True
assert client.timeout.connect == 30.0
assert client.headers.get("X-Test") == "value"
assert "unexpected keyword argument" not in str(exc_info.value)

View File

@@ -705,7 +705,121 @@ def test_ollama_does_not_modify_when_last_is_user(ollama_llm):
formatted = ollama_llm._format_messages_for_provider(original_messages)
assert formatted == original_messages
# Ollama formatting should preserve user-only messages
assert len(formatted) == 1
assert formatted[0]["role"] == "user"
assert formatted[0]["content"] == "Tell me a joke."
def test_ollama_merges_system_message_into_first_user_message(ollama_llm):
"""Test that system messages are merged into the first user message for Ollama models.
This ensures that tool-format instructions in system messages are visible to
models that don't properly respect system messages (like Olmo, Nemotron-3-nano).
"""
original_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
]
formatted = ollama_llm._format_messages_for_provider(original_messages)
# System message should be merged into user message
assert len(formatted) == 1
assert formatted[0]["role"] == "user"
assert "You are a helpful assistant." in formatted[0]["content"]
assert "Hello!" in formatted[0]["content"]
def test_ollama_merges_multiple_system_messages(ollama_llm):
"""Test that multiple system messages are accumulated and merged."""
original_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "system", "content": "Use the following tools:"},
{"role": "user", "content": "What's the weather?"},
]
formatted = ollama_llm._format_messages_for_provider(original_messages)
# Both system messages should be merged into user message
assert len(formatted) == 1
assert formatted[0]["role"] == "user"
assert "You are a helpful assistant." in formatted[0]["content"]
assert "Use the following tools:" in formatted[0]["content"]
assert "What's the weather?" in formatted[0]["content"]
def test_ollama_handles_system_only_messages(ollama_llm):
"""Test that system-only messages are converted to user messages."""
original_messages = [
{"role": "system", "content": "You are a helpful assistant."},
]
formatted = ollama_llm._format_messages_for_provider(original_messages)
# System message should be converted to user message
assert len(formatted) == 1
assert formatted[0]["role"] == "user"
assert formatted[0]["content"] == "You are a helpful assistant."
def test_ollama_handles_system_then_assistant_messages(ollama_llm):
"""Test that system messages are prepended when first non-system is assistant."""
original_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "assistant", "content": "Hello!"},
]
formatted = ollama_llm._format_messages_for_provider(original_messages)
# System should be prepended as user, assistant kept, then empty user appended
assert len(formatted) == 3
assert formatted[0]["role"] == "user"
assert formatted[0]["content"] == "You are a helpful assistant."
assert formatted[1]["role"] == "assistant"
assert formatted[1]["content"] == "Hello!"
assert formatted[2]["role"] == "user"
assert formatted[2]["content"] == ""
def test_ollama_preserves_conversation_after_system_merge(ollama_llm):
"""Test that conversation history is preserved after system message merge."""
original_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "How are you?"},
]
formatted = ollama_llm._format_messages_for_provider(original_messages)
# System merged into first user, then rest of conversation preserved
assert len(formatted) == 3
assert formatted[0]["role"] == "user"
assert "You are a helpful assistant." in formatted[0]["content"]
assert "Hello!" in formatted[0]["content"]
assert formatted[1]["role"] == "assistant"
assert formatted[1]["content"] == "Hi there!"
assert formatted[2]["role"] == "user"
assert formatted[2]["content"] == "How are you?"
def test_non_ollama_model_preserves_system_messages():
"""Test that non-Ollama models preserve system messages as-is."""
llm = LLM(model="gpt-4o-mini", is_litellm=True)
original_messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
]
formatted = llm._format_messages_for_provider(original_messages)
# Non-Ollama models should preserve system messages
assert len(formatted) == 2
assert formatted[0]["role"] == "system"
assert formatted[0]["content"] == "You are a helpful assistant."
assert formatted[1]["role"] == "user"
assert formatted[1]["content"] == "Hello!"
def test_native_provider_raises_error_when_supported_but_fails():

8378
uv.lock generated

File diff suppressed because it is too large Load Diff