mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-03-22 11:48:16 +00:00
Compare commits
26 Commits
gl/feat/a2
...
feat/cli-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5aa229f72 | ||
|
|
f13d307534 | ||
|
|
8e427164ca | ||
|
|
6495aff528 | ||
|
|
f7de8b2d28 | ||
|
|
8886f11672 | ||
|
|
713fa7d01b | ||
|
|
929d756ae2 | ||
|
|
6b262f5a6d | ||
|
|
6a6adaf2da | ||
|
|
116182f708 | ||
|
|
9eed13b8a2 | ||
|
|
50b2c7d072 | ||
|
|
e9ba4932a0 | ||
|
|
0b07b4c45f | ||
|
|
6235810844 | ||
|
|
b95486c187 | ||
|
|
ead8e8d6e6 | ||
|
|
5bbf9c8e03 | ||
|
|
5053fae8a1 | ||
|
|
9facd96aad | ||
|
|
9acb327d9f | ||
|
|
aca0817421 | ||
|
|
4d21c6e4ad | ||
|
|
32d7b4a8d4 | ||
|
|
fb2323b3de |
29
conftest.py
29
conftest.py
@@ -43,6 +43,35 @@ def _patched_make_vcr_request(httpx_request: Any, **kwargs: Any) -> Any:
|
||||
httpx_stubs._make_vcr_request = _patched_make_vcr_request
|
||||
|
||||
|
||||
# Patch the response-side of VCR to fix httpx.ResponseNotRead errors.
|
||||
# VCR's _from_serialized_response mocks httpx.Response.read(), which prevents
|
||||
# the response's internal _content attribute from being properly initialized.
|
||||
# When OpenAI's client (using with_raw_response) accesses response.content,
|
||||
# httpx raises ResponseNotRead because read() was never actually called.
|
||||
# This patch ensures _content is explicitly set after response creation.
|
||||
_original_from_serialized_response = getattr(
|
||||
httpx_stubs, "_from_serialized_response", None
|
||||
)
|
||||
|
||||
if _original_from_serialized_response is not None:
|
||||
|
||||
def _patched_from_serialized_response(
|
||||
request: Any, serialized_response: Any, history: Any = None
|
||||
) -> Any:
|
||||
"""Patched version that ensures response._content is properly set."""
|
||||
response = _original_from_serialized_response(request, serialized_response, history)
|
||||
# Explicitly set _content to avoid ResponseNotRead errors
|
||||
# The content was passed to the constructor but the mocked read() prevents
|
||||
# proper initialization of the internal state
|
||||
body_content = serialized_response.get("body", {}).get("string", b"")
|
||||
if isinstance(body_content, str):
|
||||
body_content = body_content.encode("utf-8")
|
||||
response._content = body_content
|
||||
return response
|
||||
|
||||
httpx_stubs._from_serialized_response = _patched_from_serialized_response
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="function")
|
||||
def cleanup_event_handlers() -> Generator[None, Any, None]:
|
||||
"""Clean up event bus handlers after each test to prevent test pollution."""
|
||||
|
||||
1479
docs/docs.json
1479
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,70 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 18, 2026">
|
||||
## v1.11.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.11.0rc2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 17, 2026">
|
||||
## v1.11.0rc2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0rc2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Enhance LLM response handling and serialization.
|
||||
- Upgrade vulnerable transitive dependencies (authlib, PyJWT, snowflake-connector-python).
|
||||
- Replace `os.system` with `subprocess.run` in unsafe mode pip install.
|
||||
|
||||
### Documentation
|
||||
- Update Exa Search Tool page with improved naming, description, and configuration options.
|
||||
- Add Custom MCP Servers in How-To Guide.
|
||||
- Update OTEL collectors documentation.
|
||||
- Update MCP documentation.
|
||||
- Update changelog and version for v1.11.0rc1.
|
||||
|
||||
## Contributors
|
||||
|
||||
@10ishq, @greysonlalonde, @joaomdmoura, @lucasgomide, @mattatcha, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 15, 2026">
|
||||
## v1.11.0rc1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0rc1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add Plus API token authentication in a2a
|
||||
- Implement plan execute pattern
|
||||
|
||||
### Bug Fixes
|
||||
- Resolve code interpreter sandbox escape issue
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.10.2rc2
|
||||
|
||||
## Contributors
|
||||
|
||||
@Copilot, @greysonlalonde, @lorenzejay, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 14, 2026">
|
||||
## v1.10.2rc2
|
||||
|
||||
|
||||
@@ -196,12 +196,19 @@ CrewAI provides a wide range of events that you can listen for:
|
||||
- **CrewTrainStartedEvent**: Emitted when a Crew starts training
|
||||
- **CrewTrainCompletedEvent**: Emitted when a Crew completes training
|
||||
- **CrewTrainFailedEvent**: Emitted when a Crew fails to complete training
|
||||
- **CrewTestResultEvent**: Emitted when a Crew test result is available. Contains the quality score, execution duration, and model used.
|
||||
|
||||
### Agent Events
|
||||
|
||||
- **AgentExecutionStartedEvent**: Emitted when an Agent starts executing a task
|
||||
- **AgentExecutionCompletedEvent**: Emitted when an Agent completes executing a task
|
||||
- **AgentExecutionErrorEvent**: Emitted when an Agent encounters an error during execution
|
||||
- **LiteAgentExecutionStartedEvent**: Emitted when a LiteAgent starts executing. Contains the agent info, tools, and messages.
|
||||
- **LiteAgentExecutionCompletedEvent**: Emitted when a LiteAgent completes execution. Contains the agent info and output.
|
||||
- **LiteAgentExecutionErrorEvent**: Emitted when a LiteAgent encounters an error during execution. Contains the agent info and error message.
|
||||
- **AgentEvaluationStartedEvent**: Emitted when an agent evaluation starts. Contains the agent ID, agent role, optional task ID, and iteration number.
|
||||
- **AgentEvaluationCompletedEvent**: Emitted when an agent evaluation completes. Contains the agent ID, agent role, optional task ID, iteration number, metric category, and score.
|
||||
- **AgentEvaluationFailedEvent**: Emitted when an agent evaluation fails. Contains the agent ID, agent role, optional task ID, iteration number, and error message.
|
||||
|
||||
### Task Events
|
||||
|
||||
@@ -219,6 +226,16 @@ CrewAI provides a wide range of events that you can listen for:
|
||||
- **ToolExecutionErrorEvent**: Emitted when a tool execution encounters an error
|
||||
- **ToolSelectionErrorEvent**: Emitted when there's an error selecting a tool
|
||||
|
||||
### MCP Events
|
||||
|
||||
- **MCPConnectionStartedEvent**: Emitted when starting to connect to an MCP server. Contains the server name, URL, transport type, connection timeout, and whether it's a reconnection attempt.
|
||||
- **MCPConnectionCompletedEvent**: Emitted when successfully connected to an MCP server. Contains the server name, connection duration in milliseconds, and whether it was a reconnection.
|
||||
- **MCPConnectionFailedEvent**: Emitted when connection to an MCP server fails. Contains the server name, error message, and error type (`timeout`, `authentication`, `network`, etc.).
|
||||
- **MCPToolExecutionStartedEvent**: Emitted when starting to execute an MCP tool. Contains the server name, tool name, and tool arguments.
|
||||
- **MCPToolExecutionCompletedEvent**: Emitted when MCP tool execution completes successfully. Contains the server name, tool name, result, and execution duration in milliseconds.
|
||||
- **MCPToolExecutionFailedEvent**: Emitted when MCP tool execution fails. Contains the server name, tool name, error message, and error type (`timeout`, `validation`, `server_error`, etc.).
|
||||
- **MCPConfigFetchFailedEvent**: Emitted when fetching an MCP server configuration fails (e.g., the MCP is not connected in your account, API error, or connection failure after config was fetched). Contains the slug, error message, and error type (`not_connected`, `api_error`, `connection_failed`).
|
||||
|
||||
### Knowledge Events
|
||||
|
||||
- **KnowledgeRetrievalStartedEvent**: Emitted when a knowledge retrieval is started
|
||||
@@ -232,16 +249,26 @@ CrewAI provides a wide range of events that you can listen for:
|
||||
|
||||
- **LLMGuardrailStartedEvent**: Emitted when a guardrail validation starts. Contains details about the guardrail being applied and retry count.
|
||||
- **LLMGuardrailCompletedEvent**: Emitted when a guardrail validation completes. Contains details about validation success/failure, results, and error messages if any.
|
||||
- **LLMGuardrailFailedEvent**: Emitted when a guardrail validation fails. Contains the error message and retry count.
|
||||
|
||||
### Flow Events
|
||||
|
||||
- **FlowCreatedEvent**: Emitted when a Flow is created
|
||||
- **FlowStartedEvent**: Emitted when a Flow starts execution
|
||||
- **FlowFinishedEvent**: Emitted when a Flow completes execution
|
||||
- **FlowPausedEvent**: Emitted when a Flow is paused waiting for human feedback. Contains the flow name, flow ID, method name, current state, message shown when requesting feedback, and optional list of possible outcomes for routing.
|
||||
- **FlowPlotEvent**: Emitted when a Flow is plotted
|
||||
- **MethodExecutionStartedEvent**: Emitted when a Flow method starts execution
|
||||
- **MethodExecutionFinishedEvent**: Emitted when a Flow method completes execution
|
||||
- **MethodExecutionFailedEvent**: Emitted when a Flow method fails to complete execution
|
||||
- **MethodExecutionPausedEvent**: Emitted when a Flow method is paused waiting for human feedback. Contains the flow name, method name, current state, flow ID, message shown when requesting feedback, and optional list of possible outcomes for routing.
|
||||
|
||||
### Human In The Loop Events
|
||||
|
||||
- **FlowInputRequestedEvent**: Emitted when a Flow requests user input via `Flow.ask()`. Contains the flow name, method name, the question or prompt being shown to the user, and optional metadata (e.g., user ID, channel, session context).
|
||||
- **FlowInputReceivedEvent**: Emitted when user input is received after `Flow.ask()`. Contains the flow name, method name, the original question, the user's response (or `None` if timed out), optional request metadata, and optional response metadata from the provider (e.g., who responded, thread ID, timestamps).
|
||||
- **HumanFeedbackRequestedEvent**: Emitted when a `@human_feedback` decorated method requires input from a human reviewer. Contains the flow name, method name, the method output shown to the human for review, the message displayed when requesting feedback, and optional list of possible outcomes for routing.
|
||||
- **HumanFeedbackReceivedEvent**: Emitted when a human provides feedback in response to a `@human_feedback` decorated method. Contains the flow name, method name, the raw text feedback provided by the human, and the collapsed outcome string (if emit was specified).
|
||||
|
||||
### LLM Events
|
||||
|
||||
@@ -249,6 +276,7 @@ CrewAI provides a wide range of events that you can listen for:
|
||||
- **LLMCallCompletedEvent**: Emitted when an LLM call completes
|
||||
- **LLMCallFailedEvent**: Emitted when an LLM call fails
|
||||
- **LLMStreamChunkEvent**: Emitted for each chunk received during streaming LLM responses
|
||||
- **LLMThinkingChunkEvent**: Emitted when a thinking/reasoning chunk is received from a thinking model. Contains the chunk text and optional response ID.
|
||||
|
||||
### Memory Events
|
||||
|
||||
@@ -260,6 +288,79 @@ CrewAI provides a wide range of events that you can listen for:
|
||||
- **MemorySaveFailedEvent**: Emitted when a memory save operation fails. Contains the value, metadata, agent role, and error message.
|
||||
- **MemoryRetrievalStartedEvent**: Emitted when memory retrieval for a task prompt starts. Contains the optional task ID.
|
||||
- **MemoryRetrievalCompletedEvent**: Emitted when memory retrieval for a task prompt completes successfully. Contains the task ID, memory content, and retrieval execution time.
|
||||
- **MemoryRetrievalFailedEvent**: Emitted when memory retrieval for a task prompt fails. Contains the optional task ID and error message.
|
||||
|
||||
### Reasoning Events
|
||||
|
||||
- **AgentReasoningStartedEvent**: Emitted when an agent starts reasoning about a task. Contains the agent role, task ID, and attempt number.
|
||||
- **AgentReasoningCompletedEvent**: Emitted when an agent finishes its reasoning process. Contains the agent role, task ID, the plan produced, and whether the agent is ready to proceed.
|
||||
- **AgentReasoningFailedEvent**: Emitted when the reasoning process fails. Contains the agent role, task ID, and error message.
|
||||
|
||||
### Observation Events
|
||||
|
||||
- **StepObservationStartedEvent**: Emitted when the Planner begins observing a step's result. Fires after every step execution, before the observation LLM call. Contains the agent role, step number, and step description.
|
||||
- **StepObservationCompletedEvent**: Emitted when the Planner finishes observing a step's result. Contains whether the step completed successfully, key information learned, whether the remaining plan is still valid, whether a full replan is needed, and suggested refinements.
|
||||
- **StepObservationFailedEvent**: Emitted when the observation LLM call itself fails. The system defaults to continuing the plan. Contains the error message.
|
||||
- **PlanRefinementEvent**: Emitted when the Planner refines upcoming step descriptions without a full replan. Contains the number of refined steps and the refinements applied.
|
||||
- **PlanReplanTriggeredEvent**: Emitted when the Planner triggers a full replan because the remaining plan was deemed fundamentally wrong. Contains the replan reason, replan count, and number of completed steps preserved.
|
||||
- **GoalAchievedEarlyEvent**: Emitted when the Planner detects the goal was achieved early and remaining steps will be skipped. Contains the number of steps remaining and steps completed.
|
||||
|
||||
### A2A (Agent-to-Agent) Events
|
||||
|
||||
#### Delegation Events
|
||||
|
||||
- **A2ADelegationStartedEvent**: Emitted when A2A delegation starts. Contains the endpoint URL, task description, agent ID, context ID, whether it's multiturn, turn number, agent card metadata, protocol version, provider info, and optional skill ID.
|
||||
- **A2ADelegationCompletedEvent**: Emitted when A2A delegation completes. Contains the completion status (`completed`, `input_required`, `failed`, etc.), result, error message, context ID, and agent card metadata.
|
||||
- **A2AParallelDelegationStartedEvent**: Emitted when parallel delegation to multiple A2A agents begins. Contains the list of endpoints and the task description.
|
||||
- **A2AParallelDelegationCompletedEvent**: Emitted when parallel delegation to multiple A2A agents completes. Contains the list of endpoints, success count, failure count, and results summary.
|
||||
|
||||
#### Conversation Events
|
||||
|
||||
- **A2AConversationStartedEvent**: Emitted once at the beginning of a multiturn A2A conversation, before the first message exchange. Contains the agent ID, endpoint, context ID, agent card metadata, protocol version, and provider info.
|
||||
- **A2AMessageSentEvent**: Emitted when a message is sent to the A2A agent. Contains the message content, turn number, context ID, message ID, and whether it's multiturn.
|
||||
- **A2AResponseReceivedEvent**: Emitted when a response is received from the A2A agent. Contains the response content, turn number, context ID, message ID, status, and whether it's the final response.
|
||||
- **A2AConversationCompletedEvent**: Emitted once at the end of a multiturn A2A conversation. Contains the final status (`completed` or `failed`), final result, error message, context ID, and total number of turns.
|
||||
|
||||
#### Streaming Events
|
||||
|
||||
- **A2AStreamingStartedEvent**: Emitted when streaming mode begins for A2A delegation. Contains the task ID, context ID, endpoint, turn number, and whether it's multiturn.
|
||||
- **A2AStreamingChunkEvent**: Emitted when a streaming chunk is received. Contains the chunk text, chunk index, whether it's the final chunk, task ID, context ID, and turn number.
|
||||
|
||||
#### Polling & Push Notification Events
|
||||
|
||||
- **A2APollingStartedEvent**: Emitted when polling mode begins for A2A delegation. Contains the task ID, context ID, polling interval in seconds, and endpoint.
|
||||
- **A2APollingStatusEvent**: Emitted on each polling iteration. Contains the task ID, context ID, current task state, elapsed seconds, and poll count.
|
||||
- **A2APushNotificationRegisteredEvent**: Emitted when a push notification callback is registered. Contains the task ID, context ID, callback URL, and endpoint.
|
||||
- **A2APushNotificationReceivedEvent**: Emitted when a push notification is received from the remote A2A agent. Contains the task ID, context ID, and current state.
|
||||
- **A2APushNotificationSentEvent**: Emitted when a push notification is sent to a callback URL. Contains the task ID, context ID, callback URL, state, whether delivery succeeded, and optional error message.
|
||||
- **A2APushNotificationTimeoutEvent**: Emitted when push notification wait times out. Contains the task ID, context ID, and timeout duration in seconds.
|
||||
|
||||
#### Connection & Authentication Events
|
||||
|
||||
- **A2AAgentCardFetchedEvent**: Emitted when an agent card is successfully fetched. Contains the endpoint, agent name, agent card metadata, protocol version, provider info, whether it was cached, and fetch time in milliseconds.
|
||||
- **A2AAuthenticationFailedEvent**: Emitted when authentication to an A2A agent fails. Contains the endpoint, auth type attempted (e.g., `bearer`, `oauth2`, `api_key`), error message, and HTTP status code.
|
||||
- **A2AConnectionErrorEvent**: Emitted when a connection error occurs during A2A communication. Contains the endpoint, error message, error type (e.g., `timeout`, `connection_refused`, `dns_error`), HTTP status code, and the operation being attempted.
|
||||
- **A2ATransportNegotiatedEvent**: Emitted when transport protocol is negotiated with an A2A agent. Contains the negotiated transport, negotiated URL, selection source (`client_preferred`, `server_preferred`, `fallback`), and client/server supported transports.
|
||||
- **A2AContentTypeNegotiatedEvent**: Emitted when content types are negotiated with an A2A agent. Contains the client/server input/output modes, negotiated input/output modes, and whether negotiation succeeded.
|
||||
|
||||
#### Artifact Events
|
||||
|
||||
- **A2AArtifactReceivedEvent**: Emitted when an artifact is received from a remote A2A agent. Contains the task ID, artifact ID, artifact name, description, MIME type, size in bytes, and whether content should be appended.
|
||||
|
||||
#### Server Task Events
|
||||
|
||||
- **A2AServerTaskStartedEvent**: Emitted when an A2A server task execution starts. Contains the task ID and context ID.
|
||||
- **A2AServerTaskCompletedEvent**: Emitted when an A2A server task execution completes. Contains the task ID, context ID, and result.
|
||||
- **A2AServerTaskCanceledEvent**: Emitted when an A2A server task execution is canceled. Contains the task ID and context ID.
|
||||
- **A2AServerTaskFailedEvent**: Emitted when an A2A server task execution fails. Contains the task ID, context ID, and error message.
|
||||
|
||||
#### Context Lifecycle Events
|
||||
|
||||
- **A2AContextCreatedEvent**: Emitted when an A2A context is created. Contexts group related tasks in a conversation or workflow. Contains the context ID and creation timestamp.
|
||||
- **A2AContextExpiredEvent**: Emitted when an A2A context expires due to TTL. Contains the context ID, creation timestamp, age in seconds, and task count.
|
||||
- **A2AContextIdleEvent**: Emitted when an A2A context becomes idle (no activity for the configured threshold). Contains the context ID, idle time in seconds, and task count.
|
||||
- **A2AContextCompletedEvent**: Emitted when all tasks in an A2A context complete. Contains the context ID, total tasks, and duration in seconds.
|
||||
- **A2AContextPrunedEvent**: Emitted when an A2A context is pruned (deleted). Contains the context ID, task count, and age in seconds.
|
||||
|
||||
## Event Handler Structure
|
||||
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
---
|
||||
title: "Open Telemetry Logs"
|
||||
description: "Understand how to capture telemetry logs from your CrewAI AMP deployments"
|
||||
title: "OpenTelemetry Export"
|
||||
description: "Export traces and logs from your CrewAI AMP deployments to your own OpenTelemetry collector"
|
||||
icon: "magnifying-glass-chart"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
CrewAI AMP provides a powerful way to capture telemetry logs from your deployments. This allows you to monitor the performance of your agents and workflows, and to debug issues that may arise.
|
||||
CrewAI AMP can export OpenTelemetry **traces** and **logs** from your deployments directly to your own collector. This lets you monitor agent performance, track LLM calls, and debug issues using your existing observability stack.
|
||||
|
||||
Telemetry data follows the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) plus additional CrewAI-specific attributes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="ENTERPRISE OTEL SETUP enabled" icon="users">
|
||||
Your organization should have ENTERPRISE OTEL SETUP enabled
|
||||
<Card title="CrewAI AMP account" icon="users">
|
||||
Your organization must have an active CrewAI AMP account.
|
||||
</Card>
|
||||
<Card title="OTEL collector setup" icon="server">
|
||||
Your organization should have an OTEL collector setup or a provider like
|
||||
Datadog log intake setup
|
||||
<Card title="OpenTelemetry collector" icon="server">
|
||||
You need an OpenTelemetry-compatible collector endpoint (e.g., your own OTel Collector, Datadog, Grafana, or any OTLP-compatible backend).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## How to capture telemetry logs
|
||||
## Setting up a collector
|
||||
|
||||
1. Go to settings/organization tab
|
||||
2. Configure your OTEL collector setup
|
||||
3. Save
|
||||
1. In CrewAI AMP, go to **Settings** > **OpenTelemetry Collectors**.
|
||||
2. Click **Add Collector**.
|
||||
3. Select an integration type — **OpenTelemetry Traces** or **OpenTelemetry Logs**.
|
||||
4. Configure the connection:
|
||||
- **Endpoint** — Your collector's OTLP endpoint (e.g., `https://otel-collector.example.com:4317`).
|
||||
- **Service Name** — A name to identify this service in your observability platform.
|
||||
- **Custom Headers** *(optional)* — Add authentication or routing headers as key-value pairs.
|
||||
- **Certificate** *(optional)* — Provide a TLS certificate if your collector requires one.
|
||||
5. Click **Save**.
|
||||
|
||||
Example to setup OTEL log collection capture to Datadog.
|
||||
<Frame></Frame>
|
||||
|
||||
<Frame></Frame>
|
||||
<Tip>
|
||||
You can add multiple collectors — for example, one for traces and another for logs, or send to different backends for different purposes.
|
||||
</Tip>
|
||||
|
||||
136
docs/en/enterprise/guides/custom-mcp-server.mdx
Normal file
136
docs/en/enterprise/guides/custom-mcp-server.mdx
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: "Custom MCP Servers"
|
||||
description: "Connect your own MCP servers to CrewAI AMP with public access, API key authentication, or OAuth 2.0"
|
||||
icon: "plug"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
CrewAI AMP supports connecting to any MCP server that implements the [Model Context Protocol](https://modelcontextprotocol.io/). You can bring public servers that require no authentication, servers protected by an API key or bearer token, and servers that use OAuth 2.0 for secure delegated access.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CrewAI AMP Account" icon="user">
|
||||
You need an active [CrewAI AMP](https://app.crewai.com) account.
|
||||
</Card>
|
||||
<Card title="MCP Server URL" icon="link">
|
||||
The URL of the MCP server you want to connect. The server must be accessible from the internet and support Streamable HTTP transport.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Adding a Custom MCP Server
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Tools & Integrations">
|
||||
Navigate to **Tools & Integrations** in the left sidebar of CrewAI AMP, then select the **Connections** tab.
|
||||
</Step>
|
||||
|
||||
<Step title="Start adding a Custom MCP Server">
|
||||
Click the **Add Custom MCP Server** button. A dialog will appear with the configuration form.
|
||||
</Step>
|
||||
|
||||
<Step title="Fill in the basic information">
|
||||
- **Name** (required): A descriptive name for your MCP server (e.g., "My Internal Tools Server").
|
||||
- **Description**: An optional summary of what this MCP server provides.
|
||||
- **Server URL** (required): The full URL to your MCP server endpoint (e.g., `https://my-server.example.com/mcp`).
|
||||
</Step>
|
||||
|
||||
<Step title="Choose an authentication method">
|
||||
Select one of the three available authentication methods based on how your MCP server is secured. See the sections below for details on each method.
|
||||
</Step>
|
||||
|
||||
<Step title="Add custom headers (optional)">
|
||||
If your MCP server requires additional headers on every request (e.g., tenant identifiers or routing headers), click **+ Add Header** and provide the header name and value. You can add multiple custom headers.
|
||||
</Step>
|
||||
|
||||
<Step title="Create the connection">
|
||||
Click **Create MCP Server** to save the connection. Your custom MCP server will now appear in the Connections list and its tools will be available for use in your crews.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### No Authentication
|
||||
|
||||
Choose this option when your MCP server is publicly accessible and does not require any credentials. This is common for open-source or internal servers running behind a VPN.
|
||||
|
||||
### Authentication Token
|
||||
|
||||
Use this method when your MCP server is protected by an API key or bearer token.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/custom-mcp-auth-token.png" alt="Custom MCP Server with Authentication Token" />
|
||||
</Frame>
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| **Header Name** | Yes | The name of the HTTP header that carries the token (e.g., `X-API-Key`, `Authorization`). |
|
||||
| **Value** | Yes | Your API key or bearer token. |
|
||||
| **Add to** | No | Where to attach the credential — **Header** (default) or **Query parameter**. |
|
||||
|
||||
<Tip>
|
||||
If your server expects a `Bearer` token in the `Authorization` header, set the Header Name to `Authorization` and the Value to `Bearer <your-token>`.
|
||||
</Tip>
|
||||
|
||||
### OAuth 2.0
|
||||
|
||||
Use this method for MCP servers that require OAuth 2.0 authorization. CrewAI will handle the full OAuth flow, including token refresh.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/custom-mcp-oauth.png" alt="Custom MCP Server with OAuth 2.0" />
|
||||
</Frame>
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| **Redirect URI** | — | Pre-filled and read-only. Copy this URI and register it as an authorized redirect URI in your OAuth provider. |
|
||||
| **Authorization Endpoint** | Yes | The URL where users are sent to authorize access (e.g., `https://auth.example.com/oauth/authorize`). |
|
||||
| **Token Endpoint** | Yes | The URL used to exchange the authorization code for an access token (e.g., `https://auth.example.com/oauth/token`). |
|
||||
| **Client ID** | Yes | The OAuth client ID issued by your provider. |
|
||||
| **Client Secret** | No | The OAuth client secret. Not required for public clients using PKCE. |
|
||||
| **Scopes** | No | Space-separated list of scopes to request (e.g., `read write`). |
|
||||
| **Token Auth Method** | No | How the client credentials are sent when exchanging tokens — **Standard (POST body)** or **Basic Auth (header)**. Defaults to Standard. |
|
||||
| **PKCE Supported** | No | Enable if your OAuth provider supports Proof Key for Code Exchange. Recommended for improved security. |
|
||||
|
||||
<Info>
|
||||
**Discover OAuth Config**: If your OAuth provider supports OpenID Connect Discovery, click the **Discover OAuth Config** link to auto-populate the authorization and token endpoints from the provider's `/.well-known/openid-configuration` URL.
|
||||
</Info>
|
||||
|
||||
#### Setting Up OAuth 2.0 Step by Step
|
||||
|
||||
<Steps>
|
||||
<Step title="Register the redirect URI">
|
||||
Copy the **Redirect URI** shown in the form and add it as an authorized redirect URI in your OAuth provider's application settings.
|
||||
</Step>
|
||||
|
||||
<Step title="Enter endpoints and credentials">
|
||||
Fill in the **Authorization Endpoint**, **Token Endpoint**, **Client ID**, and optionally the **Client Secret** and **Scopes**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure token exchange method">
|
||||
Select the appropriate **Token Auth Method**. Most providers use the default **Standard (POST body)**. Some older providers require **Basic Auth (header)**.
|
||||
</Step>
|
||||
|
||||
<Step title="Enable PKCE (recommended)">
|
||||
Check **PKCE Supported** if your provider supports it. PKCE adds an extra layer of security to the authorization code flow and is recommended for all new integrations.
|
||||
</Step>
|
||||
|
||||
<Step title="Create and authorize">
|
||||
Click **Create MCP Server**. You will be redirected to your OAuth provider to authorize access. Once authorized, CrewAI will store the tokens and automatically refresh them as needed.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Using Your Custom MCP Server
|
||||
|
||||
Once connected, your custom MCP server's tools appear alongside built-in connections on the **Tools & Integrations** page. You can:
|
||||
|
||||
- **Assign tools to agents** in your crews just like any other CrewAI tool.
|
||||
- **Manage visibility** to control which team members can use the server.
|
||||
- **Edit or remove** the connection at any time from the Connections list.
|
||||
|
||||
<Warning>
|
||||
If your MCP server becomes unreachable or the credentials expire, tool calls using that server will fail. Make sure the server URL is stable and credentials are kept up to date.
|
||||
</Warning>
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for assistance with custom MCP server configuration or troubleshooting.
|
||||
</Card>
|
||||
244
docs/en/guides/tools/publish-custom-tools.mdx
Normal file
244
docs/en/guides/tools/publish-custom-tools.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: Publish Custom Tools
|
||||
description: How to build, package, and publish your own CrewAI-compatible tools to PyPI so any CrewAI user can install and use them.
|
||||
icon: box-open
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CrewAI's tool system is designed to be extended. If you've built a tool that could benefit others, you can package it as a standalone Python library, publish it to PyPI, and make it available to any CrewAI user — no PR to the CrewAI repo required.
|
||||
|
||||
This guide walks through the full process: implementing the tools contract, structuring your package, and publishing to PyPI.
|
||||
|
||||
<Note type="info" title="Not looking to publish?">
|
||||
If you just need a custom tool for your own project, see the [Create Custom Tools](/en/learn/create-custom-tools) guide instead.
|
||||
</Note>
|
||||
|
||||
## The Tools Contract
|
||||
|
||||
Every CrewAI tool must satisfy one of two interfaces:
|
||||
|
||||
### Option 1: Subclass `BaseTool`
|
||||
|
||||
Subclass `crewai.tools.BaseTool` and implement the `_run` method. Define `name`, `description`, and optionally an `args_schema` for input validation.
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateInput(BaseModel):
|
||||
"""Input schema for GeolocateTool."""
|
||||
address: str = Field(..., description="The street address to geolocate.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
args_schema: type[BaseModel] = GeolocateInput
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# Your implementation here
|
||||
return f"40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### Option 2: Use the `@tool` Decorator
|
||||
|
||||
For simpler tools, the `@tool` decorator turns a function into a CrewAI tool. The function **must** have a docstring (used as the tool description) and type annotations.
|
||||
|
||||
```python
|
||||
from crewai.tools import tool
|
||||
|
||||
|
||||
@tool("Geolocate")
|
||||
def geolocate(address: str) -> str:
|
||||
"""Converts a street address into latitude/longitude coordinates."""
|
||||
return "40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### Key Requirements
|
||||
|
||||
Regardless of which approach you use, your tool must:
|
||||
|
||||
- Have a **`name`** — a short, descriptive identifier.
|
||||
- Have a **`description`** — tells the agent when and how to use the tool. This directly affects how well agents use your tool, so be clear and specific.
|
||||
- Implement **`_run`** (BaseTool) or provide a **function body** (@tool) — the synchronous execution logic.
|
||||
- Use **type annotations** on all parameters and return values.
|
||||
- Return a **string** result (or something that can be meaningfully converted to one).
|
||||
|
||||
### Optional: Async Support
|
||||
|
||||
If your tool performs I/O-bound work, implement `_arun` for async execution:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# Sync implementation
|
||||
...
|
||||
|
||||
async def _arun(self, address: str) -> str:
|
||||
# Async implementation
|
||||
...
|
||||
```
|
||||
|
||||
### Optional: Input Validation with `args_schema`
|
||||
|
||||
Define a Pydantic model as your `args_schema` to get automatic input validation and clear error messages. If you don't provide one, CrewAI will infer it from your `_run` method's signature.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TranslateInput(BaseModel):
|
||||
"""Input schema for TranslateTool."""
|
||||
text: str = Field(..., description="The text to translate.")
|
||||
target_language: str = Field(
|
||||
default="en",
|
||||
description="ISO 639-1 language code for the target language.",
|
||||
)
|
||||
```
|
||||
|
||||
Explicit schemas are recommended for published tools — they produce better agent behavior and clearer documentation for your users.
|
||||
|
||||
### Optional: Environment Variables
|
||||
|
||||
If your tool requires API keys or other configuration, declare them with `env_vars` so users know what to set:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
env_vars: list[EnvVar] = [
|
||||
EnvVar(
|
||||
name="GEOCODING_API_KEY",
|
||||
description="API key for the geocoding service.",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
## Package Structure
|
||||
|
||||
Structure your project as a standard Python package. Here's a recommended layout:
|
||||
|
||||
```
|
||||
crewai-geolocate/
|
||||
├── pyproject.toml
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── src/
|
||||
└── crewai_geolocate/
|
||||
├── __init__.py
|
||||
└── tools.py
|
||||
```
|
||||
|
||||
### `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "crewai-geolocate"
|
||||
version = "0.1.0"
|
||||
description = "A CrewAI tool for geolocating street addresses."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"crewai",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
```
|
||||
|
||||
Declare `crewai` as a dependency so users get a compatible version automatically.
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
Re-export your tool classes so users can import them directly:
|
||||
|
||||
```python
|
||||
from crewai_geolocate.tools import GeolocateTool
|
||||
|
||||
__all__ = ["GeolocateTool"]
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Package name**: Use the prefix `crewai-` (e.g., `crewai-geolocate`). This makes your tool discoverable when users search PyPI.
|
||||
- **Module name**: Use underscores (e.g., `crewai_geolocate`).
|
||||
- **Tool class name**: Use PascalCase ending in `Tool` (e.g., `GeolocateTool`).
|
||||
|
||||
## Testing Your Tool
|
||||
|
||||
Before publishing, verify your tool works within a crew:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
goal="Find coordinates for given addresses.",
|
||||
backstory="An expert in geospatial data.",
|
||||
tools=[GeolocateTool()],
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Find the coordinates of 1600 Pennsylvania Avenue, Washington, DC.",
|
||||
expected_output="The latitude and longitude of the address.",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Publishing to PyPI
|
||||
|
||||
Once your tool is tested and ready:
|
||||
|
||||
```bash
|
||||
# Build the package
|
||||
uv build
|
||||
|
||||
# Publish to PyPI
|
||||
uv publish
|
||||
```
|
||||
|
||||
If this is your first time publishing, you'll need a [PyPI account](https://pypi.org/account/register/) and an [API token](https://pypi.org/help/#apitoken).
|
||||
|
||||
### After Publishing
|
||||
|
||||
Users can install your tool with:
|
||||
|
||||
```bash
|
||||
pip install crewai-geolocate
|
||||
```
|
||||
|
||||
Or with uv:
|
||||
|
||||
```bash
|
||||
uv add crewai-geolocate
|
||||
```
|
||||
|
||||
Then use it in their crews:
|
||||
|
||||
```python
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
@@ -1,344 +0,0 @@
|
||||
---
|
||||
title: Agent-to-UI (A2UI) Protocol
|
||||
description: Enable agents to generate declarative UI surfaces for rich client rendering via the A2UI extension.
|
||||
icon: window-restore
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## A2UI Overview
|
||||
|
||||
A2UI is a declarative UI protocol extension for [A2A](/en/learn/a2a-agent-delegation) that lets agents emit structured JSON messages describing interactive surfaces. Clients receive these messages and render them as rich UI components — forms, cards, lists, modals, and more — without the agent needing to know anything about the client's rendering stack.
|
||||
|
||||
A2UI is built on the A2A extension mechanism and identified by the URI `https://a2ui.org/a2a-extension/a2ui/v0.8`.
|
||||
|
||||
<Note>
|
||||
A2UI requires the `a2a-sdk` package. Install with: `uv add 'crewai[a2a]'` or `pip install 'crewai[a2a]'`
|
||||
</Note>
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The **server extension** scans agent output for A2UI JSON objects
|
||||
2. Valid messages are wrapped as `DataPart` entries with the `application/json+a2ui` MIME type
|
||||
3. The **client extension** augments the agent's system prompt with A2UI instructions and the component catalog
|
||||
4. The client tracks surface state (active surfaces and data models) across conversation turns
|
||||
|
||||
## Server Setup
|
||||
|
||||
Add `A2UIServerExtension` to your `A2AServerConfig` to enable A2UI output:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.a2a import A2AServerConfig
|
||||
from crewai.a2a.extensions.a2ui import A2UIServerExtension
|
||||
|
||||
agent = Agent(
|
||||
role="Dashboard Agent",
|
||||
goal="Present data through interactive UI surfaces",
|
||||
backstory="Expert at building clear, actionable dashboards",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AServerConfig(
|
||||
url="https://your-server.com",
|
||||
extensions=[A2UIServerExtension()],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Server Extension Options
|
||||
|
||||
<ParamField path="catalog_ids" type="list[str] | None" default="None">
|
||||
Component catalog identifiers the server supports. When set, only these catalogs are advertised to clients.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="accept_inline_catalogs" type="bool" default="False">
|
||||
Whether to accept inline catalog definitions from clients in addition to named catalogs.
|
||||
</ParamField>
|
||||
|
||||
## Client Setup
|
||||
|
||||
Add `A2UIClientExtension` to your `A2AClientConfig` to enable A2UI rendering:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.extensions.a2ui import A2UIClientExtension
|
||||
|
||||
agent = Agent(
|
||||
role="UI Coordinator",
|
||||
goal="Coordinate tasks and render agent responses as rich UI",
|
||||
backstory="Expert at presenting agent output in interactive formats",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://dashboard-agent.example.com/.well-known/agent-card.json",
|
||||
extensions=[A2UIClientExtension()],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Client Extension Options
|
||||
|
||||
<ParamField path="catalog_id" type="str | None" default="None">
|
||||
Preferred component catalog identifier. Defaults to `"standard (v0.8)"` when not set.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="allowed_components" type="list[str] | None" default="None">
|
||||
Restrict which components the agent may use. When `None`, all 18 standard catalog components are available.
|
||||
</ParamField>
|
||||
|
||||
## Message Types
|
||||
|
||||
A2UI defines four server-to-client message types. Each message targets a **surface** identified by `surfaceId`.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="beginRendering">
|
||||
Initializes a new surface with a root component and optional styles.
|
||||
|
||||
```json
|
||||
{
|
||||
"beginRendering": {
|
||||
"surfaceId": "dashboard-1",
|
||||
"root": "main-column",
|
||||
"catalogId": "standard (v0.8)",
|
||||
"styles": {
|
||||
"primaryColor": "#EB6658"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="surfaceUpdate">
|
||||
Sends or updates one or more components on an existing surface.
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "dashboard-1",
|
||||
"components": [
|
||||
{
|
||||
"id": "main-column",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": { "explicitList": ["title", "content"] },
|
||||
"alignment": "start"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "title",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "literalString": "Dashboard" },
|
||||
"usageHint": "h1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="dataModelUpdate">
|
||||
Updates the data model bound to a surface, enabling dynamic content.
|
||||
|
||||
```json
|
||||
{
|
||||
"dataModelUpdate": {
|
||||
"surfaceId": "dashboard-1",
|
||||
"path": "/data/model",
|
||||
"contents": [
|
||||
{
|
||||
"key": "userName",
|
||||
"valueString": "Alice"
|
||||
},
|
||||
{
|
||||
"key": "score",
|
||||
"valueNumber": 42
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="deleteSurface">
|
||||
Removes a surface and all its components.
|
||||
|
||||
```json
|
||||
{
|
||||
"deleteSurface": {
|
||||
"surfaceId": "dashboard-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Component Catalog
|
||||
|
||||
A2UI ships with 18 standard components organized into three categories:
|
||||
|
||||
### Content
|
||||
|
||||
| Component | Description | Required Fields |
|
||||
|-----------|-------------|-----------------|
|
||||
| **Text** | Renders text with optional heading/body hints | `text` (StringBinding) |
|
||||
| **Image** | Displays an image with fit and size options | `url` (StringBinding) |
|
||||
| **Icon** | Renders a named icon from a set of 47 icons | `name` (IconBinding) |
|
||||
| **Video** | Embeds a video player | `url` (StringBinding) |
|
||||
| **AudioPlayer** | Embeds an audio player with optional description | `url` (StringBinding) |
|
||||
|
||||
### Layout
|
||||
|
||||
| Component | Description | Required Fields |
|
||||
|-----------|-------------|-----------------|
|
||||
| **Row** | Horizontal flex container | `children` (ChildrenDef) |
|
||||
| **Column** | Vertical flex container | `children` (ChildrenDef) |
|
||||
| **List** | Scrollable list (vertical or horizontal) | `children` (ChildrenDef) |
|
||||
| **Card** | Elevated container for a single child | `child` (str) |
|
||||
| **Tabs** | Tabbed container | `tabItems` (list of TabItem) |
|
||||
| **Divider** | Visual separator (horizontal or vertical) | — |
|
||||
| **Modal** | Overlay triggered by an entry point | `entryPointChild`, `contentChild` (str) |
|
||||
|
||||
### Interactive
|
||||
|
||||
| Component | Description | Required Fields |
|
||||
|-----------|-------------|-----------------|
|
||||
| **Button** | Clickable button that triggers an action | `child` (str), `action` (Action) |
|
||||
| **CheckBox** | Boolean toggle with a label | `label` (StringBinding), `value` (BooleanBinding) |
|
||||
| **TextField** | Text input with type and validation options | `label` (StringBinding) |
|
||||
| **DateTimeInput** | Date and/or time picker | `value` (StringBinding) |
|
||||
| **MultipleChoice** | Selection from a list of options | `selections` (ArrayBinding), `options` (list) |
|
||||
| **Slider** | Numeric range slider | `value` (NumberBinding) |
|
||||
|
||||
## Data Binding
|
||||
|
||||
Components reference values through **bindings** rather than raw literals. This allows surfaces to update dynamically when the data model changes.
|
||||
|
||||
There are two ways to bind a value:
|
||||
|
||||
- **Literal values** — hardcoded directly in the component definition
|
||||
- **Path references** — point to a key in the surface's data model
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "profile-1",
|
||||
"components": [
|
||||
{
|
||||
"id": "greeting",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "path": "/data/model/userName" },
|
||||
"usageHint": "h2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "status",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "literalString": "Online" },
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, `greeting` reads the user's name from the data model (updated via `dataModelUpdate`), while `status` uses a hardcoded literal.
|
||||
|
||||
## Handling User Actions
|
||||
|
||||
Interactive components like `Button` trigger `userAction` events that flow back to the server. Each action includes a `name`, the originating `surfaceId` and `sourceComponentId`, and an optional `context` with key-value pairs.
|
||||
|
||||
```json
|
||||
{
|
||||
"userAction": {
|
||||
"name": "submitForm",
|
||||
"surfaceId": "form-1",
|
||||
"sourceComponentId": "submit-btn",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
"context": {
|
||||
"selectedOption": "optionA"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Action context values can also use path bindings to send current data model values back to the server:
|
||||
|
||||
```json
|
||||
{
|
||||
"Button": {
|
||||
"child": "confirm-label",
|
||||
"action": {
|
||||
"name": "confirm",
|
||||
"context": [
|
||||
{
|
||||
"key": "currentScore",
|
||||
"value": { "path": "/data/model/score" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Use `validate_a2ui_message` to validate server-to-client messages and `validate_a2ui_event` for client-to-server events:
|
||||
|
||||
```python Code
|
||||
from crewai.a2a.extensions.a2ui import validate_a2ui_message
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
validate_a2ui_event,
|
||||
A2UIValidationError,
|
||||
)
|
||||
|
||||
# Validate a server message
|
||||
try:
|
||||
msg = validate_a2ui_message({"beginRendering": {"surfaceId": "s1", "root": "r1"}})
|
||||
except A2UIValidationError as exc:
|
||||
print(exc.errors)
|
||||
|
||||
# Validate a client event
|
||||
try:
|
||||
event = validate_a2ui_event({
|
||||
"userAction": {
|
||||
"name": "click",
|
||||
"surfaceId": "s1",
|
||||
"sourceComponentId": "btn-1",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
}
|
||||
})
|
||||
except A2UIValidationError as exc:
|
||||
print(exc.errors)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Start Simple" icon="play">
|
||||
Begin with a `beginRendering` message and a single `surfaceUpdate`. Add data binding and interactivity once the basic flow works.
|
||||
</Card>
|
||||
|
||||
<Card title="Use Data Binding for Dynamic Content" icon="arrows-rotate">
|
||||
Prefer path bindings over literal values for content that changes. Use `dataModelUpdate` to push new values without resending the full component tree.
|
||||
</Card>
|
||||
|
||||
<Card title="Filter Components" icon="filter">
|
||||
Use the `allowed_components` option on `A2UIClientExtension` to restrict which components the agent may emit, reducing prompt size and keeping output predictable.
|
||||
</Card>
|
||||
|
||||
<Card title="Validate Messages" icon="check">
|
||||
Use `validate_a2ui_message` and `validate_a2ui_event` to catch malformed payloads early, especially when building custom integrations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Learn More
|
||||
|
||||
- [A2A Agent Delegation](/en/learn/a2a-agent-delegation) — configure agents for remote delegation via the A2A protocol
|
||||
- [A2A Protocol Documentation](https://a2a-protocol.org) — official protocol specification
|
||||
@@ -11,6 +11,10 @@ This guide provides detailed instructions on creating custom tools for the CrewA
|
||||
incorporating the latest functionalities such as tool delegation, error handling, and dynamic tool calling. It also highlights the importance of collaboration tools,
|
||||
enabling agents to perform a wide range of actions.
|
||||
|
||||
<Tip>
|
||||
**Want to publish your tool for the community?** If you're building a tool that others could benefit from, check out the [Publish Custom Tools](/en/guides/tools/publish-custom-tools) guide to learn how to package and distribute your tool on PyPI.
|
||||
</Tip>
|
||||
|
||||
### Subclassing `BaseTool`
|
||||
|
||||
To create a personalized tool, inherit from `BaseTool` and define the necessary attributes, including the `args_schema` for input validation, and the `_run` method.
|
||||
|
||||
@@ -62,22 +62,22 @@ Use the `#` syntax to select specific tools from a server:
|
||||
"https://mcp.exa.ai/mcp?api_key=your_key#web_search_exa"
|
||||
```
|
||||
|
||||
### CrewAI AMP Marketplace
|
||||
### Connected MCP Integrations
|
||||
|
||||
Access tools from the CrewAI AMP marketplace:
|
||||
Connect MCP servers from the CrewAI catalog or bring your own. Once connected in your account, reference them by slug:
|
||||
|
||||
```python
|
||||
# Full service with all tools
|
||||
"crewai-amp:financial-data"
|
||||
# Connected MCP with all tools
|
||||
"snowflake"
|
||||
|
||||
# Specific tool from AMP service
|
||||
"crewai-amp:research-tools#pubmed_search"
|
||||
# Specific tool from a connected MCP
|
||||
"stripe#list_invoices"
|
||||
|
||||
# Multiple AMP services
|
||||
# Multiple connected MCPs
|
||||
mcps=[
|
||||
"crewai-amp:weather-insights",
|
||||
"crewai-amp:market-analysis",
|
||||
"crewai-amp:social-media-monitoring"
|
||||
"snowflake",
|
||||
"stripe",
|
||||
"github"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -99,10 +99,10 @@ multi_source_agent = Agent(
|
||||
"https://mcp.exa.ai/mcp?api_key=your_exa_key&profile=research",
|
||||
"https://weather.api.com/mcp#get_current_conditions",
|
||||
|
||||
# CrewAI AMP marketplace
|
||||
"crewai-amp:financial-insights",
|
||||
"crewai-amp:academic-research#pubmed_search",
|
||||
"crewai-amp:market-intelligence#competitor_analysis"
|
||||
# Connected MCPs from catalog
|
||||
"snowflake",
|
||||
"stripe#list_invoices",
|
||||
"github#search_repositories"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -147,7 +147,7 @@ agent = Agent(
|
||||
mcps=[
|
||||
"https://mcp.exa.ai/mcp?api_key=key", # Tools: mcp_exa_ai_*
|
||||
"https://weather.service.com/mcp", # Tools: weather_service_com_*
|
||||
"crewai-amp:financial-data" # Tools: financial_data_*
|
||||
"snowflake" # Tools: snowflake_*
|
||||
]
|
||||
)
|
||||
|
||||
@@ -170,7 +170,7 @@ agent = Agent(
|
||||
"https://primary-server.com/mcp", # Primary data source
|
||||
"https://backup-server.com/mcp", # Backup if primary fails
|
||||
"https://unreachable-server.com/mcp", # Will be skipped with warning
|
||||
"crewai-amp:reliable-service" # Reliable AMP service
|
||||
"snowflake" # Connected MCP from catalog
|
||||
]
|
||||
)
|
||||
|
||||
@@ -254,7 +254,7 @@ agent = Agent(
|
||||
apps=["gmail", "slack"], # Platform integrations
|
||||
mcps=[ # MCP servers
|
||||
"https://mcp.exa.ai/mcp?api_key=key",
|
||||
"crewai-amp:research-tools"
|
||||
"snowflake"
|
||||
],
|
||||
|
||||
verbose=True,
|
||||
@@ -298,7 +298,7 @@ agent = Agent(
|
||||
mcps=[
|
||||
"https://primary-api.com/mcp", # Primary choice
|
||||
"https://backup-api.com/mcp", # Backup option
|
||||
"crewai-amp:reliable-service" # AMP fallback
|
||||
"snowflake" # Connected MCP fallback
|
||||
]
|
||||
```
|
||||
|
||||
@@ -311,7 +311,7 @@ agent = Agent(
|
||||
backstory="Financial analyst with access to weather data for agricultural market insights",
|
||||
mcps=[
|
||||
"https://weather.service.com/mcp#get_forecast",
|
||||
"crewai-amp:financial-data#stock_analysis"
|
||||
"stripe#list_invoices"
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ Use the `mcps` field directly on agents for seamless MCP tool integration. The D
|
||||
|
||||
#### String-Based References (Quick Setup)
|
||||
|
||||
Perfect for remote HTTPS servers and CrewAI AMP marketplace:
|
||||
Perfect for remote HTTPS servers and connected MCP integrations from the CrewAI catalog:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
@@ -29,8 +29,8 @@ agent = Agent(
|
||||
mcps=[
|
||||
"https://mcp.exa.ai/mcp?api_key=your_key", # External MCP server
|
||||
"https://api.weather.com/mcp#get_forecast", # Specific tool from server
|
||||
"crewai-amp:financial-data", # CrewAI AMP marketplace
|
||||
"crewai-amp:research-tools#pubmed_search" # Specific AMP tool
|
||||
"snowflake", # Connected MCP from catalog
|
||||
"stripe#list_invoices" # Specific tool from connected MCP
|
||||
]
|
||||
)
|
||||
# MCP tools are now automatically available to your agent!
|
||||
@@ -127,7 +127,7 @@ research_agent = Agent(
|
||||
backstory="Expert researcher with access to multiple data sources",
|
||||
mcps=[
|
||||
"https://mcp.exa.ai/mcp?api_key=your_key&profile=your_profile",
|
||||
"crewai-amp:weather-service#current_conditions"
|
||||
"snowflake#run_query"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -204,19 +204,22 @@ mcps=[
|
||||
]
|
||||
```
|
||||
|
||||
#### CrewAI AMP Marketplace
|
||||
#### Connected MCP Integrations
|
||||
|
||||
Connect MCP servers from the CrewAI catalog or bring your own. Once connected in your account, reference them by slug:
|
||||
|
||||
```python
|
||||
mcps=[
|
||||
# Full AMP MCP service - get all available tools
|
||||
"crewai-amp:financial-data",
|
||||
# Connected MCP - get all available tools
|
||||
"snowflake",
|
||||
|
||||
# Specific tool from AMP service using # syntax
|
||||
"crewai-amp:research-tools#pubmed_search",
|
||||
# Specific tool from a connected MCP using # syntax
|
||||
"stripe#list_invoices",
|
||||
|
||||
# Multiple AMP services
|
||||
"crewai-amp:weather-service",
|
||||
"crewai-amp:market-analysis"
|
||||
# Multiple connected MCPs
|
||||
"snowflake",
|
||||
"stripe",
|
||||
"github"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -299,7 +302,7 @@ from crewai.mcp import MCPServerStdio, MCPServerHTTP
|
||||
mcps=[
|
||||
# String references
|
||||
"https://external-api.com/mcp", # External server
|
||||
"crewai-amp:financial-insights", # AMP service
|
||||
"snowflake", # Connected MCP from catalog
|
||||
|
||||
# Structured configurations
|
||||
MCPServerStdio(
|
||||
@@ -409,7 +412,7 @@ agent = Agent(
|
||||
# String references
|
||||
"https://reliable-server.com/mcp", # Will work
|
||||
"https://unreachable-server.com/mcp", # Will be skipped gracefully
|
||||
"crewai-amp:working-service", # Will work
|
||||
"snowflake", # Connected MCP from catalog
|
||||
|
||||
# Structured configs
|
||||
MCPServerStdio(
|
||||
|
||||
@@ -1,53 +1,110 @@
|
||||
---
|
||||
title: EXA Search Web Loader
|
||||
description: The `EXASearchTool` is designed to perform a semantic search for a specified query from a text's content across the internet.
|
||||
icon: globe-pointer
|
||||
title: "Exa Search Tool"
|
||||
description: "Search the web using the Exa Search API to find the most relevant results for any query, with options for full page content, highlights, and summaries."
|
||||
icon: "magnifying-glass"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# `EXASearchTool`
|
||||
|
||||
## Description
|
||||
|
||||
The EXASearchTool is designed to perform a semantic search for a specified query from a text's content across the internet.
|
||||
It utilizes the [exa.ai](https://exa.ai/) API to fetch and display the most relevant search results based on the query provided by the user.
|
||||
The `EXASearchTool` lets CrewAI agents search the web using the [Exa](https://exa.ai/) search API. It returns the most relevant results for any query, with options for full page content and AI-generated summaries.
|
||||
|
||||
## Installation
|
||||
|
||||
To incorporate this tool into your project, follow the installation instructions below:
|
||||
Install the CrewAI tools package:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Example
|
||||
## Environment Variables
|
||||
|
||||
The following example demonstrates how to initialize the tool and execute a search with a given query:
|
||||
Set your Exa API key as an environment variable:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import EXASearchTool
|
||||
|
||||
# Initialize the tool for internet searching capabilities
|
||||
tool = EXASearchTool()
|
||||
```bash
|
||||
export EXA_API_KEY='your_exa_api_key'
|
||||
```
|
||||
|
||||
## Steps to Get Started
|
||||
Get an API key from the [Exa dashboard](https://dashboard.exa.ai/api-keys).
|
||||
|
||||
To effectively use the EXASearchTool, follow these steps:
|
||||
## Example Usage
|
||||
|
||||
<Steps>
|
||||
<Step title="Package Installation">
|
||||
Confirm that the `crewai[tools]` package is installed in your Python environment.
|
||||
</Step>
|
||||
<Step title="API Key Acquisition">
|
||||
Acquire a [exa.ai](https://exa.ai/) API key by registering for a free account at [exa.ai](https://exa.ai/).
|
||||
</Step>
|
||||
<Step title="Environment Configuration">
|
||||
Store your obtained API key in an environment variable named `EXA_API_KEY` to facilitate its use by the tool.
|
||||
</Step>
|
||||
</Steps>
|
||||
Here's how to use the `EXASearchTool` within a CrewAI agent:
|
||||
|
||||
## Conclusion
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai_tools import EXASearchTool
|
||||
|
||||
By integrating the `EXASearchTool` into Python projects, users gain the ability to conduct real-time, relevant searches across the internet directly from their applications.
|
||||
By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward.
|
||||
# Initialize the tool
|
||||
exa_tool = EXASearchTool()
|
||||
|
||||
# Create an agent that uses the tool
|
||||
researcher = Agent(
|
||||
role='Research Analyst',
|
||||
goal='Find the latest information on any topic',
|
||||
backstory='An expert researcher who finds the most relevant and up-to-date information.',
|
||||
tools=[exa_tool],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Create a task for the agent
|
||||
research_task = Task(
|
||||
description='Find the top 3 recent breakthroughs in quantum computing.',
|
||||
expected_output='A summary of the top 3 breakthroughs with source URLs.',
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
# Form the crew and kick it off
|
||||
crew = Crew(
|
||||
agents=[researcher],
|
||||
tasks=[research_task],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `EXASearchTool` accepts the following parameters during initialization:
|
||||
|
||||
- `type` (str, optional): The search type to use. Defaults to `"auto"`. Options: `"auto"`, `"instant"`, `"fast"`, `"deep"`.
|
||||
- `content` (bool, optional): Whether to include full page content in results. Defaults to `False`.
|
||||
- `summary` (bool, optional): Whether to include AI-generated summaries of each result. Requires `content=True`. Defaults to `False`.
|
||||
- `api_key` (str, optional): Your Exa API key. Falls back to the `EXA_API_KEY` environment variable if not provided.
|
||||
- `base_url` (str, optional): Custom API server URL. Falls back to the `EXA_BASE_URL` environment variable if not provided.
|
||||
|
||||
When calling the tool (or when an agent invokes it), the following search parameters are available:
|
||||
|
||||
- `search_query` (str): **Required**. The search query string.
|
||||
- `start_published_date` (str, optional): Filter results published after this date (ISO 8601 format, e.g. `"2024-01-01"`).
|
||||
- `end_published_date` (str, optional): Filter results published before this date (ISO 8601 format).
|
||||
- `include_domains` (list[str], optional): A list of domains to restrict the search to.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
You can configure the tool with custom parameters for richer results:
|
||||
|
||||
```python
|
||||
# Get full page content with AI summaries
|
||||
exa_tool = EXASearchTool(
|
||||
content=True,
|
||||
summary=True,
|
||||
type="deep"
|
||||
)
|
||||
|
||||
# Use it in an agent
|
||||
agent = Agent(
|
||||
role="Deep Researcher",
|
||||
goal="Conduct thorough research with full content and summaries",
|
||||
tools=[exa_tool]
|
||||
)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Semantic Search**: Find results based on meaning, not just keywords
|
||||
- **Full Content Retrieval**: Get the full text of web pages alongside search results
|
||||
- **AI Summaries**: Get concise, AI-generated summaries of each result
|
||||
- **Date Filtering**: Limit results to specific time periods with published date filters
|
||||
- **Domain Filtering**: Restrict searches to specific domains
|
||||
|
||||
BIN
docs/images/crewai-otel-collector-config.png
Normal file
BIN
docs/images/crewai-otel-collector-config.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 317 KiB |
BIN
docs/images/enterprise/custom-mcp-auth-token.png
Normal file
BIN
docs/images/enterprise/custom-mcp-auth-token.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/images/enterprise/custom-mcp-oauth.png
Normal file
BIN
docs/images/enterprise/custom-mcp-oauth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -4,6 +4,70 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 18일">
|
||||
## v1.11.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- v1.11.0rc2에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 17일">
|
||||
## v1.11.0rc2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0rc2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- LLM 응답 처리 및 직렬화 개선.
|
||||
- 취약한 전이 종속성(authlib, PyJWT, snowflake-connector-python) 업그레이드.
|
||||
- 안전하지 않은 모드에서 pip 설치 시 `os.system`을 `subprocess.run`으로 교체.
|
||||
|
||||
### 문서
|
||||
- 개선된 이름, 설명 및 구성 옵션으로 Exa 검색 도구 페이지 업데이트.
|
||||
- 사용 방법 가이드에 사용자 지정 MCP 서버 추가.
|
||||
- OTEL 수집기 문서 업데이트.
|
||||
- MCP 문서 업데이트.
|
||||
- v1.11.0rc1에 대한 변경 로그 및 버전 업데이트.
|
||||
|
||||
## 기여자
|
||||
|
||||
@10ishq, @greysonlalonde, @joaomdmoura, @lucasgomide, @mattatcha, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 15일">
|
||||
## v1.11.0rc1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0rc1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Plus API 토큰 인증 추가
|
||||
- 에서 계획 실행 패턴 구현
|
||||
|
||||
### 버그 수정
|
||||
- 코드 인터프리터 샌드박스 탈출 문제 해결
|
||||
|
||||
### 문서
|
||||
- v1.10.2rc2의 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@Copilot, @greysonlalonde, @lorenzejay, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 14일">
|
||||
## v1.10.2rc2
|
||||
|
||||
|
||||
@@ -195,12 +195,19 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
|
||||
- **CrewTrainStartedEvent**: Crew가 훈련을 시작할 때 발생
|
||||
- **CrewTrainCompletedEvent**: Crew가 훈련을 완료할 때 발생
|
||||
- **CrewTrainFailedEvent**: Crew가 훈련을 완료하지 못할 때 발생
|
||||
- **CrewTestResultEvent**: Crew 테스트 결과가 사용 가능할 때 발생합니다. 품질 점수, 실행 시간, 사용된 모델을 포함합니다.
|
||||
|
||||
### 에이전트 이벤트
|
||||
|
||||
- **AgentExecutionStartedEvent**: 에이전트가 작업 실행을 시작할 때 발생함
|
||||
- **AgentExecutionCompletedEvent**: 에이전트가 작업 실행을 완료할 때 발생함
|
||||
- **AgentExecutionErrorEvent**: 에이전트가 실행 도중 오류를 만날 때 발생함
|
||||
- **LiteAgentExecutionStartedEvent**: LiteAgent가 실행을 시작할 때 발생합니다. 에이전트 정보, 도구, 메시지를 포함합니다.
|
||||
- **LiteAgentExecutionCompletedEvent**: LiteAgent가 실행을 완료할 때 발생합니다. 에이전트 정보와 출력을 포함합니다.
|
||||
- **LiteAgentExecutionErrorEvent**: LiteAgent가 실행 중 오류를 만날 때 발생합니다. 에이전트 정보와 오류 메시지를 포함합니다.
|
||||
- **AgentEvaluationStartedEvent**: 에이전트 평가가 시작될 때 발생합니다. 에이전트 ID, 에이전트 역할, 선택적 태스크 ID, 반복 횟수를 포함합니다.
|
||||
- **AgentEvaluationCompletedEvent**: 에이전트 평가가 완료될 때 발생합니다. 에이전트 ID, 에이전트 역할, 선택적 태스크 ID, 반복 횟수, 메트릭 카테고리, 점수를 포함합니다.
|
||||
- **AgentEvaluationFailedEvent**: 에이전트 평가가 실패할 때 발생합니다. 에이전트 ID, 에이전트 역할, 선택적 태스크 ID, 반복 횟수, 오류 메시지를 포함합니다.
|
||||
|
||||
### 작업 이벤트
|
||||
|
||||
@@ -218,6 +225,16 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
|
||||
- **ToolExecutionErrorEvent**: 도구 실행 중 오류가 발생할 때 발생함
|
||||
- **ToolSelectionErrorEvent**: 도구 선택 시 오류가 발생할 때 발생함
|
||||
|
||||
### MCP 이벤트
|
||||
|
||||
- **MCPConnectionStartedEvent**: MCP 서버 연결을 시작할 때 발생합니다. 서버 이름, URL, 전송 유형, 연결 시간 초과, 재연결 시도 여부를 포함합니다.
|
||||
- **MCPConnectionCompletedEvent**: MCP 서버에 성공적으로 연결될 때 발생합니다. 서버 이름, 연결 시간(밀리초), 재연결 여부를 포함합니다.
|
||||
- **MCPConnectionFailedEvent**: MCP 서버 연결이 실패할 때 발생합니다. 서버 이름, 오류 메시지, 오류 유형(`timeout`, `authentication`, `network` 등)을 포함합니다.
|
||||
- **MCPToolExecutionStartedEvent**: MCP 도구 실행을 시작할 때 발생합니다. 서버 이름, 도구 이름, 도구 인수를 포함합니다.
|
||||
- **MCPToolExecutionCompletedEvent**: MCP 도구 실행이 성공적으로 완료될 때 발생합니다. 서버 이름, 도구 이름, 결과, 실행 시간(밀리초)을 포함합니다.
|
||||
- **MCPToolExecutionFailedEvent**: MCP 도구 실행이 실패할 때 발생합니다. 서버 이름, 도구 이름, 오류 메시지, 오류 유형(`timeout`, `validation`, `server_error` 등)을 포함합니다.
|
||||
- **MCPConfigFetchFailedEvent**: MCP 서버 구성을 가져오는 데 실패할 때 발생합니다(예: 계정에서 MCP가 연결되지 않았거나, API 오류, 구성을 가져온 후 연결 실패). slug, 오류 메시지, 오류 유형(`not_connected`, `api_error`, `connection_failed`)을 포함합니다.
|
||||
|
||||
### 지식 이벤트
|
||||
|
||||
- **KnowledgeRetrievalStartedEvent**: 지식 검색이 시작될 때 발생
|
||||
@@ -231,16 +248,26 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
|
||||
|
||||
- **LLMGuardrailStartedEvent**: 가드레일 검증이 시작될 때 발생합니다. 적용되는 가드레일에 대한 세부 정보와 재시도 횟수를 포함합니다.
|
||||
- **LLMGuardrailCompletedEvent**: 가드레일 검증이 완료될 때 발생합니다. 검증의 성공/실패, 결과 및 오류 메시지(있는 경우)에 대한 세부 정보를 포함합니다.
|
||||
- **LLMGuardrailFailedEvent**: 가드레일 검증이 실패할 때 발생합니다. 오류 메시지와 재시도 횟수를 포함합니다.
|
||||
|
||||
### Flow 이벤트
|
||||
|
||||
- **FlowCreatedEvent**: Flow가 생성될 때 발생
|
||||
- **FlowStartedEvent**: Flow가 실행을 시작할 때 발생
|
||||
- **FlowFinishedEvent**: Flow가 실행을 완료할 때 발생
|
||||
- **FlowPausedEvent**: 사람의 피드백을 기다리며 Flow가 일시 중지될 때 발생합니다. Flow 이름, Flow ID, 메서드 이름, 현재 상태, 피드백 요청 시 표시되는 메시지, 라우팅을 위한 선택적 결과 목록을 포함합니다.
|
||||
- **FlowPlotEvent**: Flow가 플롯될 때 발생
|
||||
- **MethodExecutionStartedEvent**: Flow 메서드가 실행을 시작할 때 발생
|
||||
- **MethodExecutionFinishedEvent**: Flow 메서드가 실행을 완료할 때 발생
|
||||
- **MethodExecutionFailedEvent**: Flow 메서드가 실행을 완료하지 못할 때 발생
|
||||
- **MethodExecutionPausedEvent**: 사람의 피드백을 기다리며 Flow 메서드가 일시 중지될 때 발생합니다. Flow 이름, 메서드 이름, 현재 상태, Flow ID, 피드백 요청 시 표시되는 메시지, 라우팅을 위한 선택적 결과 목록을 포함합니다.
|
||||
|
||||
### Human In The Loop 이벤트
|
||||
|
||||
- **FlowInputRequestedEvent**: `Flow.ask()`를 통해 Flow가 사용자 입력을 요청할 때 발생합니다. Flow 이름, 메서드 이름, 사용자에게 표시되는 질문 또는 프롬프트, 선택적 메타데이터(예: 사용자 ID, 채널, 세션 컨텍스트)를 포함합니다.
|
||||
- **FlowInputReceivedEvent**: `Flow.ask()` 이후 사용자 입력이 수신될 때 발생합니다. Flow 이름, 메서드 이름, 원래 질문, 사용자의 응답(시간 초과 시 `None`), 선택적 요청 메타데이터, 프로바이더의 선택적 응답 메타데이터(예: 응답자, 스레드 ID, 타임스탬프)를 포함합니다.
|
||||
- **HumanFeedbackRequestedEvent**: `@human_feedback` 데코레이터가 적용된 메서드가 사람 리뷰어의 입력을 필요로 할 때 발생합니다. Flow 이름, 메서드 이름, 사람에게 검토를 위해 표시되는 메서드 출력, 피드백 요청 시 표시되는 메시지, 라우팅을 위한 선택적 결과 목록을 포함합니다.
|
||||
- **HumanFeedbackReceivedEvent**: `@human_feedback` 데코레이터가 적용된 메서드에 대해 사람이 피드백을 제공할 때 발생합니다. Flow 이름, 메서드 이름, 사람이 제공한 원본 텍스트 피드백, 축약된 결과 문자열(emit이 지정된 경우)을 포함합니다.
|
||||
|
||||
### LLM 이벤트
|
||||
|
||||
@@ -248,6 +275,7 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
|
||||
- **LLMCallCompletedEvent**: LLM 호출이 완료될 때 발생
|
||||
- **LLMCallFailedEvent**: LLM 호출이 실패할 때 발생
|
||||
- **LLMStreamChunkEvent**: 스트리밍 LLM 응답 중 각 청크를 받을 때마다 발생
|
||||
- **LLMThinkingChunkEvent**: thinking 모델에서 사고/추론 청크가 수신될 때 발생합니다. 청크 텍스트와 선택적 응답 ID를 포함합니다.
|
||||
|
||||
### 메모리 이벤트
|
||||
|
||||
@@ -259,6 +287,79 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
|
||||
- **MemorySaveFailedEvent**: 메모리 저장 작업에 실패할 때 발생합니다. 값, 메타데이터, agent 역할, 오류 메시지를 포함합니다.
|
||||
- **MemoryRetrievalStartedEvent**: 태스크 프롬프트를 위한 메모리 검색이 시작될 때 발생합니다. 선택적 태스크 ID를 포함합니다.
|
||||
- **MemoryRetrievalCompletedEvent**: 태스크 프롬프트를 위한 메모리 검색이 성공적으로 완료될 때 발생합니다. 태스크 ID, 메모리 내용, 검색 실행 시간을 포함합니다.
|
||||
- **MemoryRetrievalFailedEvent**: 태스크 프롬프트를 위한 메모리 검색이 실패할 때 발생합니다. 선택적 태스크 ID와 오류 메시지를 포함합니다.
|
||||
|
||||
### 추론 이벤트
|
||||
|
||||
- **AgentReasoningStartedEvent**: 에이전트가 태스크에 대한 추론을 시작할 때 발생합니다. 에이전트 역할, 태스크 ID, 시도 횟수를 포함합니다.
|
||||
- **AgentReasoningCompletedEvent**: 에이전트가 추론 과정을 마칠 때 발생합니다. 에이전트 역할, 태스크 ID, 생성된 계획, 에이전트가 진행할 준비가 되었는지 여부를 포함합니다.
|
||||
- **AgentReasoningFailedEvent**: 추론 과정이 실패할 때 발생합니다. 에이전트 역할, 태스크 ID, 오류 메시지를 포함합니다.
|
||||
|
||||
### 관찰 이벤트
|
||||
|
||||
- **StepObservationStartedEvent**: Planner가 단계 결과를 관찰하기 시작할 때 발생합니다. 매 단계 실행 후, 관찰 LLM 호출 전에 발생합니다. 에이전트 역할, 단계 번호, 단계 설명을 포함합니다.
|
||||
- **StepObservationCompletedEvent**: Planner가 단계 결과 관찰을 마칠 때 발생합니다. 단계 성공 여부, 학습된 핵심 정보, 남은 계획의 유효성, 전체 재계획 필요 여부, 제안된 개선 사항을 포함합니다.
|
||||
- **StepObservationFailedEvent**: 관찰 LLM 호출 자체가 실패할 때 발생합니다. 시스템은 기본적으로 계획을 계속 진행합니다. 오류 메시지를 포함합니다.
|
||||
- **PlanRefinementEvent**: Planner가 전체 재계획 없이 다음 단계 설명을 개선할 때 발생합니다. 개선된 단계 수와 적용된 개선 사항을 포함합니다.
|
||||
- **PlanReplanTriggeredEvent**: 남은 계획이 근본적으로 잘못된 것으로 판단되어 Planner가 전체 재계획을 트리거할 때 발생합니다. 재계획 이유, 재계획 횟수, 보존된 완료 단계 수를 포함합니다.
|
||||
- **GoalAchievedEarlyEvent**: Planner가 목표가 조기에 달성되었음을 감지하고 나머지 단계를 건너뛸 때 발생합니다. 남은 단계 수와 완료된 단계 수를 포함합니다.
|
||||
|
||||
### A2A (Agent-to-Agent) 이벤트
|
||||
|
||||
#### 위임 이벤트
|
||||
|
||||
- **A2ADelegationStartedEvent**: A2A 위임이 시작될 때 발생합니다. 엔드포인트 URL, 태스크 설명, 에이전트 ID, 컨텍스트 ID, 멀티턴 여부, 턴 번호, agent card 메타데이터, 프로토콜 버전, 프로바이더 정보, 선택적 skill ID를 포함합니다.
|
||||
- **A2ADelegationCompletedEvent**: A2A 위임이 완료될 때 발생합니다. 완료 상태(`completed`, `input_required`, `failed` 등), 결과, 오류 메시지, 컨텍스트 ID, agent card 메타데이터를 포함합니다.
|
||||
- **A2AParallelDelegationStartedEvent**: 여러 A2A 에이전트로의 병렬 위임이 시작될 때 발생합니다. 엔드포인트 목록과 태스크 설명을 포함합니다.
|
||||
- **A2AParallelDelegationCompletedEvent**: 여러 A2A 에이전트로의 병렬 위임이 완료될 때 발생합니다. 엔드포인트 목록, 성공 수, 실패 수, 결과 요약을 포함합니다.
|
||||
|
||||
#### 대화 이벤트
|
||||
|
||||
- **A2AConversationStartedEvent**: 멀티턴 A2A 대화 시작 시 한 번 발생합니다. 첫 번째 메시지 교환 전에 발생합니다. 에이전트 ID, 엔드포인트, 컨텍스트 ID, agent card 메타데이터, 프로토콜 버전, 프로바이더 정보를 포함합니다.
|
||||
- **A2AMessageSentEvent**: A2A 에이전트에 메시지가 전송될 때 발생합니다. 메시지 내용, 턴 번호, 컨텍스트 ID, 메시지 ID, 멀티턴 여부를 포함합니다.
|
||||
- **A2AResponseReceivedEvent**: A2A 에이전트로부터 응답이 수신될 때 발생합니다. 응답 내용, 턴 번호, 컨텍스트 ID, 메시지 ID, 상태, 최종 응답 여부를 포함합니다.
|
||||
- **A2AConversationCompletedEvent**: 멀티턴 A2A 대화 종료 시 한 번 발생합니다. 최종 상태(`completed` 또는 `failed`), 최종 결과, 오류 메시지, 컨텍스트 ID, 총 턴 수를 포함합니다.
|
||||
|
||||
#### 스트리밍 이벤트
|
||||
|
||||
- **A2AStreamingStartedEvent**: A2A 위임을 위한 스트리밍 모드가 시작될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 엔드포인트, 턴 번호, 멀티턴 여부를 포함합니다.
|
||||
- **A2AStreamingChunkEvent**: 스트리밍 청크가 수신될 때 발생합니다. 청크 텍스트, 청크 인덱스, 최종 청크 여부, 태스크 ID, 컨텍스트 ID, 턴 번호를 포함합니다.
|
||||
|
||||
#### 폴링 및 푸시 알림 이벤트
|
||||
|
||||
- **A2APollingStartedEvent**: A2A 위임을 위한 폴링 모드가 시작될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 폴링 간격(초), 엔드포인트를 포함합니다.
|
||||
- **A2APollingStatusEvent**: 각 폴링 반복 시 발생합니다. 태스크 ID, 컨텍스트 ID, 현재 태스크 상태, 경과 시간, 폴링 횟수를 포함합니다.
|
||||
- **A2APushNotificationRegisteredEvent**: 푸시 알림 콜백이 등록될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 콜백 URL, 엔드포인트를 포함합니다.
|
||||
- **A2APushNotificationReceivedEvent**: 원격 A2A 에이전트로부터 푸시 알림이 수신될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 현재 상태를 포함합니다.
|
||||
- **A2APushNotificationSentEvent**: 콜백 URL로 푸시 알림이 전송될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 콜백 URL, 상태, 전달 성공 여부, 선택적 오류 메시지를 포함합니다.
|
||||
- **A2APushNotificationTimeoutEvent**: 푸시 알림 대기가 시간 초과될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 시간 초과 시간(초)을 포함합니다.
|
||||
|
||||
#### 연결 및 인증 이벤트
|
||||
|
||||
- **A2AAgentCardFetchedEvent**: agent card가 성공적으로 가져올 때 발생합니다. 엔드포인트, 에이전트 이름, agent card 메타데이터, 프로토콜 버전, 프로바이더 정보, 캐시 여부, 가져오기 시간(밀리초)을 포함합니다.
|
||||
- **A2AAuthenticationFailedEvent**: A2A 에이전트 인증이 실패할 때 발생합니다. 엔드포인트, 시도된 인증 유형(예: `bearer`, `oauth2`, `api_key`), 오류 메시지, HTTP 상태 코드를 포함합니다.
|
||||
- **A2AConnectionErrorEvent**: A2A 통신 중 연결 오류가 발생할 때 발생합니다. 엔드포인트, 오류 메시지, 오류 유형(예: `timeout`, `connection_refused`, `dns_error`), HTTP 상태 코드, 시도 중인 작업을 포함합니다.
|
||||
- **A2ATransportNegotiatedEvent**: A2A 에이전트와 전송 프로토콜이 협상될 때 발생합니다. 협상된 전송, 협상된 URL, 선택 소스(`client_preferred`, `server_preferred`, `fallback`), 클라이언트/서버 지원 전송을 포함합니다.
|
||||
- **A2AContentTypeNegotiatedEvent**: A2A 에이전트와 콘텐츠 유형이 협상될 때 발생합니다. 클라이언트/서버 입출력 모드, 협상된 입출력 모드, 협상 성공 여부를 포함합니다.
|
||||
|
||||
#### 아티팩트 이벤트
|
||||
|
||||
- **A2AArtifactReceivedEvent**: 원격 A2A 에이전트로부터 아티팩트가 수신될 때 발생합니다. 태스크 ID, 아티팩트 ID, 아티팩트 이름, 설명, MIME 유형, 크기(바이트), 콘텐츠 추가 여부를 포함합니다.
|
||||
|
||||
#### 서버 태스크 이벤트
|
||||
|
||||
- **A2AServerTaskStartedEvent**: A2A 서버 태스크 실행이 시작될 때 발생합니다. 태스크 ID와 컨텍스트 ID를 포함합니다.
|
||||
- **A2AServerTaskCompletedEvent**: A2A 서버 태스크 실행이 완료될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 결과를 포함합니다.
|
||||
- **A2AServerTaskCanceledEvent**: A2A 서버 태스크 실행이 취소될 때 발생합니다. 태스크 ID와 컨텍스트 ID를 포함합니다.
|
||||
- **A2AServerTaskFailedEvent**: A2A 서버 태스크 실행이 실패할 때 발생합니다. 태스크 ID, 컨텍스트 ID, 오류 메시지를 포함합니다.
|
||||
|
||||
#### 컨텍스트 수명 주기 이벤트
|
||||
|
||||
- **A2AContextCreatedEvent**: A2A 컨텍스트가 생성될 때 발생합니다. 컨텍스트는 대화 또는 워크플로우에서 관련 태스크를 그룹화합니다. 컨텍스트 ID와 생성 타임스탬프를 포함합니다.
|
||||
- **A2AContextExpiredEvent**: TTL로 인해 A2A 컨텍스트가 만료될 때 발생합니다. 컨텍스트 ID, 생성 타임스탬프, 수명(초), 태스크 수를 포함합니다.
|
||||
- **A2AContextIdleEvent**: A2A 컨텍스트가 유휴 상태가 될 때(설정된 임계값 동안 활동 없음) 발생합니다. 컨텍스트 ID, 유휴 시간(초), 태스크 수를 포함합니다.
|
||||
- **A2AContextCompletedEvent**: A2A 컨텍스트의 모든 태스크가 완료될 때 발생합니다. 컨텍스트 ID, 총 태스크 수, 지속 시간(초)을 포함합니다.
|
||||
- **A2AContextPrunedEvent**: A2A 컨텍스트가 정리(삭제)될 때 발생합니다. 컨텍스트 ID, 태스크 수, 수명(초)을 포함합니다.
|
||||
|
||||
## 이벤트 핸들러 구조
|
||||
|
||||
|
||||
39
docs/ko/enterprise/guides/capture_telemetry_logs.mdx
Normal file
39
docs/ko/enterprise/guides/capture_telemetry_logs.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "OpenTelemetry 내보내기"
|
||||
description: "CrewAI AMP 배포에서 자체 OpenTelemetry 수집기로 트레이스와 로그를 내보내기"
|
||||
icon: "magnifying-glass-chart"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
CrewAI AMP는 배포에서 OpenTelemetry **트레이스**와 **로그**를 자체 수집기로 직접 내보낼 수 있습니다. 이를 통해 기존 관측 가능성 스택을 사용하여 에이전트 성능을 모니터링하고, LLM 호출을 추적하고, 문제를 디버깅할 수 있습니다.
|
||||
|
||||
텔레메트리 데이터는 [OpenTelemetry GenAI 시맨틱 규칙](https://opentelemetry.io/docs/specs/semconv/gen-ai/)과 추가적인 CrewAI 전용 속성을 따릅니다.
|
||||
|
||||
## 사전 요구 사항
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CrewAI AMP 계정" icon="users">
|
||||
조직에 활성 CrewAI AMP 계정이 있어야 합니다.
|
||||
</Card>
|
||||
<Card title="OpenTelemetry 수집기" icon="server">
|
||||
OpenTelemetry 호환 수집기 엔드포인트가 필요합니다 (예: 자체 OTel Collector, Datadog, Grafana 또는 OTLP 호환 백엔드).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 수집기 설정
|
||||
|
||||
1. CrewAI AMP에서 **Settings** > **OpenTelemetry Collectors**로 이동합니다.
|
||||
2. **Add Collector**를 클릭합니다.
|
||||
3. 통합 유형을 선택합니다 — **OpenTelemetry Traces** 또는 **OpenTelemetry Logs**.
|
||||
4. 연결을 구성합니다:
|
||||
- **Endpoint** — 수집기의 OTLP 엔드포인트 (예: `https://otel-collector.example.com:4317`).
|
||||
- **Service Name** — 관측 가능성 플랫폼에서 이 서비스를 식별하기 위한 이름.
|
||||
- **Custom Headers** *(선택 사항)* — 인증 또는 라우팅 헤더를 키-값 쌍으로 추가합니다.
|
||||
- **Certificate** *(선택 사항)* — 수집기에서 TLS 인증서가 필요한 경우 제공합니다.
|
||||
5. **Save**를 클릭합니다.
|
||||
|
||||
<Frame></Frame>
|
||||
|
||||
<Tip>
|
||||
여러 수집기를 추가할 수 있습니다 — 예를 들어, 트레이스용 하나와 로그용 하나를 추가하거나, 다른 목적을 위해 다른 백엔드로 전송할 수 있습니다.
|
||||
</Tip>
|
||||
136
docs/ko/enterprise/guides/custom-mcp-server.mdx
Normal file
136
docs/ko/enterprise/guides/custom-mcp-server.mdx
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: "커스텀 MCP 서버"
|
||||
description: "공개 액세스, API 키 인증 또는 OAuth 2.0을 사용하여 자체 MCP 서버를 CrewAI AMP에 연결하세요"
|
||||
icon: "plug"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
CrewAI AMP는 [Model Context Protocol](https://modelcontextprotocol.io/)을 구현하는 모든 MCP 서버에 연결할 수 있습니다. 인증이 필요 없는 공개 서버, API 키 또는 Bearer 토큰으로 보호되는 서버, OAuth 2.0을 사용하는 서버를 연결할 수 있습니다.
|
||||
|
||||
## 사전 요구사항
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CrewAI AMP 계정" icon="user">
|
||||
활성화된 [CrewAI AMP](https://app.crewai.com) 계정이 필요합니다.
|
||||
</Card>
|
||||
<Card title="MCP 서버 URL" icon="link">
|
||||
연결하려는 MCP 서버의 URL입니다. 서버는 인터넷에서 접근 가능해야 하며 Streamable HTTP 전송을 지원해야 합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 커스텀 MCP 서버 추가하기
|
||||
|
||||
<Steps>
|
||||
<Step title="Tools & Integrations 열기">
|
||||
CrewAI AMP 왼쪽 사이드바에서 **Tools & Integrations**로 이동한 후 **Connections** 탭을 선택합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="커스텀 MCP 서버 추가 시작">
|
||||
**Add Custom MCP Server** 버튼을 클릭합니다. 구성 양식이 포함된 대화 상자가 나타납니다.
|
||||
</Step>
|
||||
|
||||
<Step title="기본 정보 입력">
|
||||
- **Name** (필수): MCP 서버의 설명적 이름 (예: "내부 도구 서버").
|
||||
- **Description**: 이 MCP 서버가 제공하는 기능에 대한 선택적 요약.
|
||||
- **Server URL** (필수): MCP 서버 엔드포인트의 전체 URL (예: `https://my-server.example.com/mcp`).
|
||||
</Step>
|
||||
|
||||
<Step title="인증 방법 선택">
|
||||
MCP 서버의 보안 방식에 따라 세 가지 인증 방법 중 하나를 선택합니다. 각 방법에 대한 자세한 내용은 아래 섹션을 참조하세요.
|
||||
</Step>
|
||||
|
||||
<Step title="커스텀 헤더 추가 (선택사항)">
|
||||
MCP 서버가 모든 요청에 추가 헤더를 요구하는 경우 (예: 테넌트 식별자 또는 라우팅 헤더), **+ Add Header**를 클릭하고 헤더 이름과 값을 입력합니다. 여러 커스텀 헤더를 추가할 수 있습니다.
|
||||
</Step>
|
||||
|
||||
<Step title="연결 생성">
|
||||
**Create MCP Server**를 클릭하여 연결을 저장합니다. 커스텀 MCP 서버가 Connections 목록에 나타나고 해당 도구를 crew에서 사용할 수 있게 됩니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 인증 방법
|
||||
|
||||
### 인증 없음
|
||||
|
||||
MCP 서버가 공개적으로 접근 가능하고 자격 증명이 필요 없을 때 이 옵션을 선택합니다. 오픈 소스 서버나 VPN 뒤에서 실행되는 내부 서버에 일반적입니다.
|
||||
|
||||
### 인증 토큰
|
||||
|
||||
MCP 서버가 API 키 또는 Bearer 토큰으로 보호되는 경우 이 방법을 사용합니다.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/custom-mcp-auth-token.png" alt="인증 토큰을 사용하는 커스텀 MCP 서버" />
|
||||
</Frame>
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
|------|------|------|
|
||||
| **Header Name** | 예 | 토큰을 전달하는 HTTP 헤더 이름 (예: `X-API-Key`, `Authorization`). |
|
||||
| **Value** | 예 | API 키 또는 Bearer 토큰. |
|
||||
| **Add to** | 아니오 | 자격 증명을 첨부할 위치 — **Header** (기본값) 또는 **Query parameter**. |
|
||||
|
||||
<Tip>
|
||||
서버가 `Authorization` 헤더에 `Bearer` 토큰을 예상하는 경우, Header Name을 `Authorization`으로, Value를 `Bearer <토큰>`으로 설정하세요.
|
||||
</Tip>
|
||||
|
||||
### OAuth 2.0
|
||||
|
||||
OAuth 2.0 인증이 필요한 MCP 서버에 이 방법을 사용합니다. CrewAI가 토큰 갱신을 포함한 전체 OAuth 흐름을 처리합니다.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/custom-mcp-oauth.png" alt="OAuth 2.0을 사용하는 커스텀 MCP 서버" />
|
||||
</Frame>
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
|------|------|------|
|
||||
| **Redirect URI** | — | 자동으로 채워지며 읽기 전용입니다. 이 URI를 복사하여 OAuth 제공자에 승인된 리디렉션 URI로 등록하세요. |
|
||||
| **Authorization Endpoint** | 예 | 사용자가 접근을 승인하기 위해 이동하는 URL (예: `https://auth.example.com/oauth/authorize`). |
|
||||
| **Token Endpoint** | 예 | 인증 코드를 액세스 토큰으로 교환하는 데 사용되는 URL (예: `https://auth.example.com/oauth/token`). |
|
||||
| **Client ID** | 예 | OAuth 제공자가 발급한 클라이언트 ID. |
|
||||
| **Client Secret** | 아니오 | OAuth 클라이언트 시크릿. PKCE를 사용하는 공개 클라이언트에는 필요하지 않습니다. |
|
||||
| **Scopes** | 아니오 | 요청할 스코프의 공백으로 구분된 목록 (예: `read write`). |
|
||||
| **Token Auth Method** | 아니오 | 토큰 교환 시 클라이언트 자격 증명을 보내는 방법 — **Standard (POST body)** 또는 **Basic Auth (header)**. 기본값은 Standard입니다. |
|
||||
| **PKCE Supported** | 아니오 | OAuth 제공자가 Proof Key for Code Exchange를 지원하는 경우 활성화합니다. 보안 강화를 위해 권장됩니다. |
|
||||
|
||||
<Info>
|
||||
**Discover OAuth Config**: OAuth 제공자가 OpenID Connect Discovery를 지원하는 경우, **Discover OAuth Config** 링크를 클릭하여 제공자의 `/.well-known/openid-configuration` URL에서 인증 및 토큰 엔드포인트를 자동으로 채울 수 있습니다.
|
||||
</Info>
|
||||
|
||||
#### OAuth 2.0 단계별 설정
|
||||
|
||||
<Steps>
|
||||
<Step title="리디렉션 URI 등록">
|
||||
양식에 표시된 **Redirect URI**를 복사하여 OAuth 제공자의 애플리케이션 설정에서 승인된 리디렉션 URI로 추가합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="엔드포인트 및 자격 증명 입력">
|
||||
**Authorization Endpoint**, **Token Endpoint**, **Client ID**를 입력하고, 선택적으로 **Client Secret**과 **Scopes**를 입력합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="토큰 교환 방법 구성">
|
||||
적절한 **Token Auth Method**를 선택합니다. 대부분의 제공자는 기본값인 **Standard (POST body)**를 사용합니다. 일부 오래된 제공자는 **Basic Auth (header)**를 요구합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="PKCE 활성화 (권장)">
|
||||
제공자가 지원하는 경우 **PKCE Supported**를 체크합니다. PKCE는 인증 코드 흐름에 추가 보안 계층을 제공하며 모든 새 통합에 권장됩니다.
|
||||
</Step>
|
||||
|
||||
<Step title="생성 및 인증">
|
||||
**Create MCP Server**를 클릭합니다. OAuth 제공자로 리디렉션되어 접근을 인증합니다. 인증 완료 후 CrewAI가 토큰을 저장하고 필요에 따라 자동으로 갱신합니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 커스텀 MCP 서버 사용하기
|
||||
|
||||
연결이 완료되면 커스텀 MCP 서버의 도구가 **Tools & Integrations** 페이지에서 기본 제공 연결과 함께 표시됩니다. 다음을 수행할 수 있습니다:
|
||||
|
||||
- 다른 CrewAI 도구와 마찬가지로 crew의 **에이전트에 도구를 할당**합니다.
|
||||
- **가시성을 관리**하여 어떤 팀원이 서버를 사용할 수 있는지 제어합니다.
|
||||
- Connections 목록에서 언제든지 연결을 **편집하거나 제거**합니다.
|
||||
|
||||
<Warning>
|
||||
MCP 서버에 접근할 수 없거나 자격 증명이 만료되면 해당 서버를 사용하는 도구 호출이 실패합니다. 서버 URL이 안정적이고 자격 증명이 최신 상태인지 확인하세요.
|
||||
</Warning>
|
||||
|
||||
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
|
||||
커스텀 MCP 서버 구성 또는 문제 해결에 대한 도움이 필요하면 지원팀에 문의하세요.
|
||||
</Card>
|
||||
61
docs/ko/guides/coding-tools/agents-md.mdx
Normal file
61
docs/ko/guides/coding-tools/agents-md.mdx
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: 코딩 도구
|
||||
description: AGENTS.md를 사용하여 CrewAI 프로젝트 전반에서 코딩 에이전트와 IDE를 안내합니다.
|
||||
icon: terminal
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## AGENTS.md를 사용하는 이유
|
||||
|
||||
`AGENTS.md`는 가벼운 저장소 로컬 지침 파일로, 코딩 에이전트에게 일관되고 프로젝트별 안내를 제공합니다. 프로젝트 루트에 배치하고 어시스턴트가 작업하는 방식(컨벤션, 명령어, 아키텍처 노트, 가드레일)에 대한 신뢰할 수 있는 소스로 활용하세요.
|
||||
|
||||
## CLI로 프로젝트 생성
|
||||
|
||||
CrewAI CLI를 사용하여 프로젝트를 스캐폴딩하면, `AGENTS.md`가 루트에 자동으로 추가됩니다.
|
||||
|
||||
```bash
|
||||
# Crew
|
||||
crewai create crew my_crew
|
||||
|
||||
# Flow
|
||||
crewai create flow my_flow
|
||||
|
||||
# Tool repository
|
||||
crewai tool create my_tool
|
||||
```
|
||||
|
||||
## 도구 설정: 어시스턴트에 AGENTS.md 연결
|
||||
|
||||
### Codex
|
||||
|
||||
Codex는 저장소에 배치된 `AGENTS.md` 파일로 안내할 수 있습니다. 컨벤션, 명령어, 워크플로우 기대치 등 지속적인 프로젝트 컨텍스트를 제공하는 데 사용하세요.
|
||||
|
||||
### Claude Code
|
||||
|
||||
Claude Code는 프로젝트 메모리를 `CLAUDE.md`에 저장합니다. `/init`으로 부트스트랩하고 `/memory`로 편집할 수 있습니다. Claude Code는 `CLAUDE.md` 내에서 임포트도 지원하므로, `@AGENTS.md`와 같은 한 줄을 추가하여 공유 지침을 중복 없이 가져올 수 있습니다.
|
||||
|
||||
간단하게 다음과 같이 사용할 수 있습니다:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md CLAUDE.md
|
||||
```
|
||||
|
||||
### Gemini CLI와 Google Antigravity
|
||||
|
||||
Gemini CLI와 Antigravity는 저장소 루트 및 상위 디렉토리에서 프로젝트 컨텍스트 파일(기본값: `GEMINI.md`)을 로드합니다. Gemini CLI 설정에서 `context.fileName`을 설정하여 `AGENTS.md`를 대신(또는 추가로) 읽도록 구성할 수 있습니다. 예를 들어, `AGENTS.md`만 설정하거나 각 도구의 형식을 유지하고 싶다면 `AGENTS.md`와 `GEMINI.md`를 모두 포함할 수 있습니다.
|
||||
|
||||
간단하게 다음과 같이 사용할 수 있습니다:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md GEMINI.md
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
Cursor는 `AGENTS.md`를 프로젝트 지침 파일로 지원합니다. 프로젝트 루트에 배치하여 Cursor의 코딩 어시스턴트에 안내를 제공하세요.
|
||||
|
||||
### Windsurf
|
||||
|
||||
Claude Code는 Windsurf와의 공식 통합을 제공합니다. Windsurf 내에서 Claude Code를 사용하는 경우, 위의 Claude Code 안내를 따르고 `CLAUDE.md`에서 `AGENTS.md`를 임포트하세요.
|
||||
|
||||
Windsurf의 네이티브 어시스턴트를 사용하는 경우, 프로젝트 규칙 또는 지침 기능(사용 가능한 경우)을 구성하여 `AGENTS.md`에서 읽거나 내용을 직접 붙여넣으세요.
|
||||
244
docs/ko/guides/tools/publish-custom-tools.mdx
Normal file
244
docs/ko/guides/tools/publish-custom-tools.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: 커스텀 도구 배포하기
|
||||
description: PyPI에 게시할 수 있는 CrewAI 호환 도구를 빌드, 패키징, 배포하는 방법을 안내합니다.
|
||||
icon: box-open
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
CrewAI의 도구 시스템은 확장 가능하도록 설계되었습니다. 다른 사용자에게도 유용한 도구를 만들었다면, 독립적인 Python 라이브러리로 패키징하여 PyPI에 게시하고 모든 CrewAI 사용자가 사용할 수 있도록 할 수 있습니다. CrewAI 저장소에 PR을 보낼 필요가 없습니다.
|
||||
|
||||
이 가이드에서는 도구 계약 구현, 패키지 구조화, PyPI 게시까지의 전체 과정을 안내합니다.
|
||||
|
||||
<Note type="info" title="배포할 계획이 없으신가요?">
|
||||
프로젝트 내에서만 사용할 커스텀 도구가 필요하다면 [커스텀 도구 생성](/ko/learn/create-custom-tools) 가이드를 참고하세요.
|
||||
</Note>
|
||||
|
||||
## 도구 계약
|
||||
|
||||
모든 CrewAI 도구는 다음 두 가지 인터페이스 중 하나를 충족해야 합니다:
|
||||
|
||||
### 옵션 1: `BaseTool` 서브클래싱
|
||||
|
||||
`crewai.tools.BaseTool`을 서브클래싱하고 `_run` 메서드를 구현합니다. `name`, `description`, 그리고 선택적으로 입력 검증을 위한 `args_schema`를 정의합니다.
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateInput(BaseModel):
|
||||
"""GeolocateTool의 입력 스키마."""
|
||||
address: str = Field(..., description="지오코딩할 도로명 주소.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
|
||||
args_schema: type[BaseModel] = GeolocateInput
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# 구현 로직
|
||||
return f"40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### 옵션 2: `@tool` 데코레이터 사용
|
||||
|
||||
간단한 도구의 경우, `@tool` 데코레이터로 함수를 CrewAI 도구로 변환할 수 있습니다. 함수에는 반드시 독스트링(도구 설명으로 사용됨)과 타입 어노테이션이 있어야 합니다.
|
||||
|
||||
```python
|
||||
from crewai.tools import tool
|
||||
|
||||
|
||||
@tool("Geolocate")
|
||||
def geolocate(address: str) -> str:
|
||||
"""도로명 주소를 위도/경도 좌표로 변환합니다."""
|
||||
return "40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### 핵심 요구사항
|
||||
|
||||
어떤 방식을 사용하든, 도구는 다음을 충족해야 합니다:
|
||||
|
||||
- **`name`** — 짧고 설명적인 식별자.
|
||||
- **`description`** — 에이전트에게 도구를 언제, 어떻게 사용할지 알려줍니다. 에이전트가 도구를 얼마나 잘 활용하는지에 직접적으로 영향을 미치므로 명확하고 구체적으로 작성하세요.
|
||||
- **`_run`** (BaseTool) 또는 **함수 본문** (@tool) 구현 — 동기 실행 로직.
|
||||
- 모든 매개변수와 반환 값에 **타입 어노테이션** 사용.
|
||||
- **문자열** 결과를 반환 (또는 의미 있게 문자열로 변환 가능한 값).
|
||||
|
||||
### 선택사항: 비동기 지원
|
||||
|
||||
I/O 바운드 작업을 수행하는 도구의 경우 비동기 실행을 위해 `_arun`을 구현합니다:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# 동기 구현
|
||||
...
|
||||
|
||||
async def _arun(self, address: str) -> str:
|
||||
# 비동기 구현
|
||||
...
|
||||
```
|
||||
|
||||
### 선택사항: `args_schema`를 통한 입력 검증
|
||||
|
||||
Pydantic 모델을 `args_schema`로 정의하면 자동 입력 검증과 명확한 에러 메시지를 받을 수 있습니다. 제공하지 않으면 CrewAI가 `_run` 메서드의 시그니처에서 추론합니다.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TranslateInput(BaseModel):
|
||||
"""TranslateTool의 입력 스키마."""
|
||||
text: str = Field(..., description="번역할 텍스트.")
|
||||
target_language: str = Field(
|
||||
default="en",
|
||||
description="대상 언어의 ISO 639-1 언어 코드.",
|
||||
)
|
||||
```
|
||||
|
||||
배포용 도구에는 명시적 스키마를 권장합니다 — 에이전트 동작이 개선되고 사용자에게 더 명확한 문서를 제공합니다.
|
||||
|
||||
### 선택사항: 환경 변수
|
||||
|
||||
도구에 API 키나 기타 설정이 필요한 경우, `env_vars`로 선언하여 사용자가 무엇을 설정해야 하는지 알 수 있도록 합니다:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
|
||||
env_vars: list[EnvVar] = [
|
||||
EnvVar(
|
||||
name="GEOCODING_API_KEY",
|
||||
description="지오코딩 서비스 API 키.",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
## 패키지 구조
|
||||
|
||||
프로젝트를 표준 Python 패키지로 구성합니다. 권장 레이아웃:
|
||||
|
||||
```
|
||||
crewai-geolocate/
|
||||
├── pyproject.toml
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── src/
|
||||
└── crewai_geolocate/
|
||||
├── __init__.py
|
||||
└── tools.py
|
||||
```
|
||||
|
||||
### `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "crewai-geolocate"
|
||||
version = "0.1.0"
|
||||
description = "도로명 주소를 지오코딩하는 CrewAI 도구."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"crewai",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
```
|
||||
|
||||
사용자가 자동으로 호환 버전을 받을 수 있도록 `crewai`를 의존성으로 선언합니다.
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
사용자가 직접 import할 수 있도록 도구 클래스를 re-export합니다:
|
||||
|
||||
```python
|
||||
from crewai_geolocate.tools import GeolocateTool
|
||||
|
||||
__all__ = ["GeolocateTool"]
|
||||
```
|
||||
|
||||
### 명명 규칙
|
||||
|
||||
- **패키지 이름**: `crewai-` 접두사를 사용합니다 (예: `crewai-geolocate`). PyPI에서 검색할 때 도구를 쉽게 찾을 수 있습니다.
|
||||
- **모듈 이름**: 밑줄을 사용합니다 (예: `crewai_geolocate`).
|
||||
- **도구 클래스 이름**: `Tool`로 끝나는 PascalCase를 사용합니다 (예: `GeolocateTool`).
|
||||
|
||||
## 도구 테스트
|
||||
|
||||
게시 전에 도구가 크루 내에서 작동하는지 확인합니다:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
goal="주어진 주소의 좌표를 찾습니다.",
|
||||
backstory="지리공간 데이터 전문가.",
|
||||
tools=[GeolocateTool()],
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="1600 Pennsylvania Avenue, Washington, DC의 좌표를 찾으세요.",
|
||||
expected_output="해당 주소의 위도와 경도.",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## PyPI에 게시하기
|
||||
|
||||
도구 테스트를 완료하고 준비가 되면:
|
||||
|
||||
```bash
|
||||
# 패키지 빌드
|
||||
uv build
|
||||
|
||||
# PyPI에 게시
|
||||
uv publish
|
||||
```
|
||||
|
||||
처음 게시하는 경우 [PyPI 계정](https://pypi.org/account/register/)과 [API 토큰](https://pypi.org/help/#apitoken)이 필요합니다.
|
||||
|
||||
### 게시 후
|
||||
|
||||
사용자는 다음과 같이 도구를 설치할 수 있습니다:
|
||||
|
||||
```bash
|
||||
pip install crewai-geolocate
|
||||
```
|
||||
|
||||
또는 uv를 사용하여:
|
||||
|
||||
```bash
|
||||
uv add crewai-geolocate
|
||||
```
|
||||
|
||||
그런 다음 크루에서 사용합니다:
|
||||
|
||||
```python
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
@@ -9,6 +9,10 @@ mode: "wide"
|
||||
|
||||
이 가이드는 CrewAI 프레임워크를 위한 커스텀 툴을 생성하는 방법과 최신 기능(툴 위임, 오류 처리, 동적 툴 호출 등)을 통합하여 이러한 툴을 효율적으로 관리하고 활용하는 방법에 대해 자세히 안내합니다. 또한 협업 툴의 중요성을 강조하며, 에이전트가 다양한 작업을 수행할 수 있도록 지원합니다.
|
||||
|
||||
<Tip>
|
||||
**커뮤니티에 도구를 배포하고 싶으신가요?** 다른 사용자에게도 유용한 도구를 만들고 있다면, [커스텀 도구 배포하기](/ko/guides/tools/publish-custom-tools) 가이드에서 도구를 패키징하고 PyPI에 배포하는 방법을 알아보세요.
|
||||
</Tip>
|
||||
|
||||
### `BaseTool` 서브클래싱
|
||||
|
||||
개인화된 툴을 생성하려면 `BaseTool`을 상속받고, 입력 검증을 위한 `args_schema`와 `_run` 메서드를 포함한 필요한 속성들을 정의해야 합니다.
|
||||
|
||||
@@ -62,22 +62,22 @@ agent = Agent(
|
||||
"https://mcp.exa.ai/mcp?api_key=your_key#web_search_exa"
|
||||
```
|
||||
|
||||
### CrewAI AMP 마켓플레이스
|
||||
### 연결된 MCP 통합
|
||||
|
||||
CrewAI AMP 마켓플레이스의 도구에 액세스하세요:
|
||||
CrewAI 카탈로그에서 MCP 서버를 연결하거나 직접 가져올 수 있습니다. 계정에 연결한 후 슬러그로 참조하세요:
|
||||
|
||||
```python
|
||||
# 모든 도구가 포함된 전체 서비스
|
||||
"crewai-amp:financial-data"
|
||||
# 모든 도구가 포함된 연결된 MCP
|
||||
"snowflake"
|
||||
|
||||
# AMP 서비스의 특정 도구
|
||||
"crewai-amp:research-tools#pubmed_search"
|
||||
# 연결된 MCP의 특정 도구
|
||||
"stripe#list_invoices"
|
||||
|
||||
# 다중 AMP 서비스
|
||||
# 여러 연결된 MCP
|
||||
mcps=[
|
||||
"crewai-amp:weather-insights",
|
||||
"crewai-amp:market-analysis",
|
||||
"crewai-amp:social-media-monitoring"
|
||||
"snowflake",
|
||||
"stripe",
|
||||
"github"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -99,10 +99,10 @@ multi_source_agent = Agent(
|
||||
"https://mcp.exa.ai/mcp?api_key=your_exa_key&profile=research",
|
||||
"https://weather.api.com/mcp#get_current_conditions",
|
||||
|
||||
# CrewAI AMP 마켓플레이스
|
||||
"crewai-amp:financial-insights",
|
||||
"crewai-amp:academic-research#pubmed_search",
|
||||
"crewai-amp:market-intelligence#competitor_analysis"
|
||||
# 카탈로그에서 연결된 MCP
|
||||
"snowflake",
|
||||
"stripe#list_invoices",
|
||||
"github#search_repositories"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -154,7 +154,7 @@ agent = Agent(
|
||||
"https://reliable-server.com/mcp", # 작동할 것
|
||||
"https://unreachable-server.com/mcp", # 우아하게 건너뛸 것
|
||||
"https://slow-server.com/mcp", # 우아하게 타임아웃될 것
|
||||
"crewai-amp:working-service" # 작동할 것
|
||||
"snowflake" # 카탈로그에서 연결된 MCP
|
||||
]
|
||||
)
|
||||
# 에이전트는 작동하는 서버의 도구를 사용하고 실패한 서버에 대한 경고를 로그에 남깁니다
|
||||
@@ -229,6 +229,6 @@ agent = Agent(
|
||||
mcps=[
|
||||
"https://primary-api.com/mcp", # 주요 선택
|
||||
"https://backup-api.com/mcp", # 백업 옵션
|
||||
"crewai-amp:reliable-service" # AMP 폴백
|
||||
"snowflake" # 연결된 MCP 폴백
|
||||
]
|
||||
```
|
||||
|
||||
@@ -25,8 +25,8 @@ agent = Agent(
|
||||
mcps=[
|
||||
"https://mcp.exa.ai/mcp?api_key=your_key", # 외부 MCP 서버
|
||||
"https://api.weather.com/mcp#get_forecast", # 서버의 특정 도구
|
||||
"crewai-amp:financial-data", # CrewAI AMP 마켓플레이스
|
||||
"crewai-amp:research-tools#pubmed_search" # 특정 AMP 도구
|
||||
"snowflake", # 카탈로그에서 연결된 MCP
|
||||
"stripe#list_invoices" # 연결된 MCP의 특정 도구
|
||||
]
|
||||
)
|
||||
# MCP 도구들이 이제 자동으로 에이전트에서 사용 가능합니다!
|
||||
|
||||
@@ -4,6 +4,70 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="18 mar 2026">
|
||||
## v1.11.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.11.0rc2
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="17 mar 2026">
|
||||
## v1.11.0rc2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0rc2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Aprimorar o manuseio e a serialização das respostas do LLM.
|
||||
- Atualizar dependências transitivas vulneráveis (authlib, PyJWT, snowflake-connector-python).
|
||||
- Substituir `os.system` por `subprocess.run` na instalação do pip em modo inseguro.
|
||||
|
||||
### Documentação
|
||||
- Atualizar a página da Ferramenta de Pesquisa Exa com nomes, descrições e opções de configuração aprimoradas.
|
||||
- Adicionar Servidores MCP Personalizados no Guia de Como Fazer.
|
||||
- Atualizar a documentação dos coletores OTEL.
|
||||
- Atualizar a documentação do MCP.
|
||||
- Atualizar o changelog e a versão para v1.11.0rc1.
|
||||
|
||||
## Contributors
|
||||
|
||||
@10ishq, @greysonlalonde, @joaomdmoura, @lucasgomide, @mattatcha, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="15 mar 2026">
|
||||
## v1.11.0rc1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.11.0rc1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar autenticação de token da API Plus
|
||||
- Implementar padrão de execução de plano
|
||||
|
||||
### Correções de Bugs
|
||||
- Resolver problema de escape do sandbox do interpretador de código
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.10.2rc2
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@Copilot, @greysonlalonde, @lorenzejay, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="14 mar 2026">
|
||||
## v1.10.2rc2
|
||||
|
||||
|
||||
@@ -196,12 +196,19 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
|
||||
- **CrewTrainStartedEvent**: Emitido ao iniciar o treinamento de um Crew
|
||||
- **CrewTrainCompletedEvent**: Emitido ao concluir o treinamento de um Crew
|
||||
- **CrewTrainFailedEvent**: Emitido ao falhar no treinamento de um Crew
|
||||
- **CrewTestResultEvent**: Emitido quando um resultado de teste de Crew está disponível. Contém a pontuação de qualidade, duração da execução e modelo utilizado.
|
||||
|
||||
### Eventos de Agent
|
||||
|
||||
- **AgentExecutionStartedEvent**: Emitido quando um Agent inicia a execução de uma tarefa
|
||||
- **AgentExecutionCompletedEvent**: Emitido quando um Agent conclui a execução de uma tarefa
|
||||
- **AgentExecutionErrorEvent**: Emitido quando um Agent encontra um erro durante a execução
|
||||
- **LiteAgentExecutionStartedEvent**: Emitido quando um LiteAgent inicia a execução. Contém as informações do agente, ferramentas e mensagens.
|
||||
- **LiteAgentExecutionCompletedEvent**: Emitido quando um LiteAgent conclui a execução. Contém as informações do agente e a saída.
|
||||
- **LiteAgentExecutionErrorEvent**: Emitido quando um LiteAgent encontra um erro durante a execução. Contém as informações do agente e a mensagem de erro.
|
||||
- **AgentEvaluationStartedEvent**: Emitido quando uma avaliação de agente é iniciada. Contém o ID do agente, papel do agente, ID da tarefa opcional e número da iteração.
|
||||
- **AgentEvaluationCompletedEvent**: Emitido quando uma avaliação de agente é concluída. Contém o ID do agente, papel do agente, ID da tarefa opcional, número da iteração, categoria da métrica e pontuação.
|
||||
- **AgentEvaluationFailedEvent**: Emitido quando uma avaliação de agente falha. Contém o ID do agente, papel do agente, ID da tarefa opcional, número da iteração e mensagem de erro.
|
||||
|
||||
### Eventos de Task
|
||||
|
||||
@@ -219,6 +226,16 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
|
||||
- **ToolExecutionErrorEvent**: Emitido quando ocorre erro na execução de uma ferramenta
|
||||
- **ToolSelectionErrorEvent**: Emitido ao ocorrer erro na seleção de uma ferramenta
|
||||
|
||||
### Eventos de MCP
|
||||
|
||||
- **MCPConnectionStartedEvent**: Emitido ao iniciar a conexão com um servidor MCP. Contém o nome do servidor, URL, tipo de transporte, timeout de conexão e se é uma tentativa de reconexão.
|
||||
- **MCPConnectionCompletedEvent**: Emitido ao conectar com sucesso a um servidor MCP. Contém o nome do servidor, duração da conexão em milissegundos e se foi uma reconexão.
|
||||
- **MCPConnectionFailedEvent**: Emitido quando a conexão com um servidor MCP falha. Contém o nome do servidor, mensagem de erro e tipo de erro (`timeout`, `authentication`, `network`, etc.).
|
||||
- **MCPToolExecutionStartedEvent**: Emitido ao iniciar a execução de uma ferramenta MCP. Contém o nome do servidor, nome da ferramenta e argumentos da ferramenta.
|
||||
- **MCPToolExecutionCompletedEvent**: Emitido quando a execução de uma ferramenta MCP é concluída com sucesso. Contém o nome do servidor, nome da ferramenta, resultado e duração da execução em milissegundos.
|
||||
- **MCPToolExecutionFailedEvent**: Emitido quando a execução de uma ferramenta MCP falha. Contém o nome do servidor, nome da ferramenta, mensagem de erro e tipo de erro (`timeout`, `validation`, `server_error`, etc.).
|
||||
- **MCPConfigFetchFailedEvent**: Emitido quando a obtenção da configuração de um servidor MCP falha (ex.: o MCP não está conectado na sua conta, erro de API ou falha de conexão após a configuração ser obtida). Contém o slug, mensagem de erro e tipo de erro (`not_connected`, `api_error`, `connection_failed`).
|
||||
|
||||
### Eventos de Knowledge
|
||||
|
||||
- **KnowledgeRetrievalStartedEvent**: Emitido ao iniciar recuperação de conhecimento
|
||||
@@ -232,16 +249,26 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
|
||||
|
||||
- **LLMGuardrailStartedEvent**: Emitido ao iniciar validação dos guardrails. Contém detalhes do guardrail aplicado e tentativas.
|
||||
- **LLMGuardrailCompletedEvent**: Emitido ao concluir validação dos guardrails. Contém detalhes sobre sucesso/falha na validação, resultados e mensagens de erro, se houver.
|
||||
- **LLMGuardrailFailedEvent**: Emitido quando a validação do guardrail falha. Contém a mensagem de erro e o número de tentativas.
|
||||
|
||||
### Eventos de Flow
|
||||
|
||||
- **FlowCreatedEvent**: Emitido ao criar um Flow
|
||||
- **FlowStartedEvent**: Emitido ao iniciar a execução de um Flow
|
||||
- **FlowFinishedEvent**: Emitido ao concluir a execução de um Flow
|
||||
- **FlowPausedEvent**: Emitido quando um Flow é pausado aguardando feedback humano. Contém o nome do flow, ID do flow, nome do método, estado atual, mensagem exibida ao solicitar feedback e lista opcional de resultados possíveis para roteamento.
|
||||
- **FlowPlotEvent**: Emitido ao plotar um Flow
|
||||
- **MethodExecutionStartedEvent**: Emitido ao iniciar a execução de um método do Flow
|
||||
- **MethodExecutionFinishedEvent**: Emitido ao concluir a execução de um método do Flow
|
||||
- **MethodExecutionFailedEvent**: Emitido ao falhar na execução de um método do Flow
|
||||
- **MethodExecutionPausedEvent**: Emitido quando um método do Flow é pausado aguardando feedback humano. Contém o nome do flow, nome do método, estado atual, ID do flow, mensagem exibida ao solicitar feedback e lista opcional de resultados possíveis para roteamento.
|
||||
|
||||
### Eventos de Human In The Loop
|
||||
|
||||
- **FlowInputRequestedEvent**: Emitido quando um Flow solicita entrada do usuário via `Flow.ask()`. Contém o nome do flow, nome do método, a pergunta ou prompt exibido ao usuário e metadados opcionais (ex.: ID do usuário, canal, contexto da sessão).
|
||||
- **FlowInputReceivedEvent**: Emitido quando a entrada do usuário é recebida após `Flow.ask()`. Contém o nome do flow, nome do método, a pergunta original, a resposta do usuário (ou `None` se expirou), metadados opcionais da solicitação e metadados opcionais da resposta do provedor (ex.: quem respondeu, ID do thread, timestamps).
|
||||
- **HumanFeedbackRequestedEvent**: Emitido quando um método decorado com `@human_feedback` requer entrada de um revisor humano. Contém o nome do flow, nome do método, a saída do método exibida ao humano para revisão, a mensagem exibida ao solicitar feedback e lista opcional de resultados possíveis para roteamento.
|
||||
- **HumanFeedbackReceivedEvent**: Emitido quando um humano fornece feedback em resposta a um método decorado com `@human_feedback`. Contém o nome do flow, nome do método, o texto bruto do feedback fornecido pelo humano e a string de resultado consolidada (se emit foi especificado).
|
||||
|
||||
### Eventos de LLM
|
||||
|
||||
@@ -249,6 +276,91 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
|
||||
- **LLMCallCompletedEvent**: Emitido ao concluir uma chamada LLM
|
||||
- **LLMCallFailedEvent**: Emitido ao falhar uma chamada LLM
|
||||
- **LLMStreamChunkEvent**: Emitido para cada chunk recebido durante respostas em streaming do LLM
|
||||
- **LLMThinkingChunkEvent**: Emitido quando um chunk de pensamento/raciocínio é recebido de um modelo de pensamento. Contém o texto do chunk e ID de resposta opcional.
|
||||
|
||||
### Eventos de Memória
|
||||
|
||||
- **MemoryQueryStartedEvent**: Emitido quando uma consulta de memória é iniciada. Contém a consulta, limite e threshold de pontuação opcional.
|
||||
- **MemoryQueryCompletedEvent**: Emitido quando uma consulta de memória é concluída com sucesso. Contém a consulta, resultados, limite, threshold de pontuação e tempo de execução da consulta.
|
||||
- **MemoryQueryFailedEvent**: Emitido quando uma consulta de memória falha. Contém a consulta, limite, threshold de pontuação e mensagem de erro.
|
||||
- **MemorySaveStartedEvent**: Emitido quando uma operação de salvamento de memória é iniciada. Contém o valor a ser salvo, metadados e papel do agente opcional.
|
||||
- **MemorySaveCompletedEvent**: Emitido quando uma operação de salvamento de memória é concluída com sucesso. Contém o valor salvo, metadados, papel do agente e tempo de salvamento.
|
||||
- **MemorySaveFailedEvent**: Emitido quando uma operação de salvamento de memória falha. Contém o valor, metadados, papel do agente e mensagem de erro.
|
||||
- **MemoryRetrievalStartedEvent**: Emitido quando a recuperação de memória para um prompt de tarefa é iniciada. Contém o ID da tarefa opcional.
|
||||
- **MemoryRetrievalCompletedEvent**: Emitido quando a recuperação de memória para um prompt de tarefa é concluída com sucesso. Contém o ID da tarefa, conteúdo da memória e tempo de execução da recuperação.
|
||||
- **MemoryRetrievalFailedEvent**: Emitido quando a recuperação de memória para um prompt de tarefa falha. Contém o ID da tarefa opcional e mensagem de erro.
|
||||
|
||||
### Eventos de Raciocínio
|
||||
|
||||
- **AgentReasoningStartedEvent**: Emitido quando um agente começa a raciocinar sobre uma tarefa. Contém o papel do agente, ID da tarefa e número da tentativa.
|
||||
- **AgentReasoningCompletedEvent**: Emitido quando um agente finaliza seu processo de raciocínio. Contém o papel do agente, ID da tarefa, o plano produzido e se o agente está pronto para prosseguir.
|
||||
- **AgentReasoningFailedEvent**: Emitido quando o processo de raciocínio falha. Contém o papel do agente, ID da tarefa e mensagem de erro.
|
||||
|
||||
### Eventos de Observação
|
||||
|
||||
- **StepObservationStartedEvent**: Emitido quando o Planner começa a observar o resultado de um passo. Disparado após cada execução de passo, antes da chamada LLM de observação. Contém o papel do agente, número do passo e descrição do passo.
|
||||
- **StepObservationCompletedEvent**: Emitido quando o Planner finaliza a observação do resultado de um passo. Contém se o passo foi concluído com sucesso, informações-chave aprendidas, se o plano restante ainda é válido, se é necessário um replanejamento completo e refinamentos sugeridos.
|
||||
- **StepObservationFailedEvent**: Emitido quando a chamada LLM de observação falha. O sistema continua o plano por padrão. Contém a mensagem de erro.
|
||||
- **PlanRefinementEvent**: Emitido quando o Planner refina descrições de passos futuros sem replanejamento completo. Contém o número de passos refinados e os refinamentos aplicados.
|
||||
- **PlanReplanTriggeredEvent**: Emitido quando o Planner dispara um replanejamento completo porque o plano restante foi considerado fundamentalmente incorreto. Contém o motivo do replanejamento, contagem de replanejamentos e número de passos concluídos preservados.
|
||||
- **GoalAchievedEarlyEvent**: Emitido quando o Planner detecta que o objetivo foi alcançado antecipadamente e os passos restantes serão ignorados. Contém o número de passos restantes e passos concluídos.
|
||||
|
||||
### Eventos A2A (Agent-to-Agent)
|
||||
|
||||
#### Eventos de Delegação
|
||||
|
||||
- **A2ADelegationStartedEvent**: Emitido quando a delegação A2A é iniciada. Contém a URL do endpoint, descrição da tarefa, ID do agente, ID do contexto, se é multiturn, número do turno, metadados do agent card, versão do protocolo, informações do provedor e ID da skill opcional.
|
||||
- **A2ADelegationCompletedEvent**: Emitido quando a delegação A2A é concluída. Contém o status de conclusão (`completed`, `input_required`, `failed`, etc.), resultado, mensagem de erro, ID do contexto e metadados do agent card.
|
||||
- **A2AParallelDelegationStartedEvent**: Emitido quando a delegação paralela para múltiplos agentes A2A é iniciada. Contém a lista de endpoints e a descrição da tarefa.
|
||||
- **A2AParallelDelegationCompletedEvent**: Emitido quando a delegação paralela para múltiplos agentes A2A é concluída. Contém a lista de endpoints, contagem de sucessos, contagem de falhas e resumo dos resultados.
|
||||
|
||||
#### Eventos de Conversação
|
||||
|
||||
- **A2AConversationStartedEvent**: Emitido uma vez no início de uma conversação multiturn A2A, antes da primeira troca de mensagens. Contém o ID do agente, endpoint, ID do contexto, metadados do agent card, versão do protocolo e informações do provedor.
|
||||
- **A2AMessageSentEvent**: Emitido quando uma mensagem é enviada ao agente A2A. Contém o conteúdo da mensagem, número do turno, ID do contexto, ID da mensagem e se é multiturn.
|
||||
- **A2AResponseReceivedEvent**: Emitido quando uma resposta é recebida do agente A2A. Contém o conteúdo da resposta, número do turno, ID do contexto, ID da mensagem, status e se é a resposta final.
|
||||
- **A2AConversationCompletedEvent**: Emitido uma vez ao final de uma conversação multiturn A2A. Contém o status final (`completed` ou `failed`), resultado final, mensagem de erro, ID do contexto e número total de turnos.
|
||||
|
||||
#### Eventos de Streaming
|
||||
|
||||
- **A2AStreamingStartedEvent**: Emitido quando o modo streaming é iniciado para delegação A2A. Contém o ID da tarefa, ID do contexto, endpoint, número do turno e se é multiturn.
|
||||
- **A2AStreamingChunkEvent**: Emitido quando um chunk de streaming é recebido. Contém o texto do chunk, índice do chunk, se é o chunk final, ID da tarefa, ID do contexto e número do turno.
|
||||
|
||||
#### Eventos de Polling e Push Notification
|
||||
|
||||
- **A2APollingStartedEvent**: Emitido quando o modo polling é iniciado para delegação A2A. Contém o ID da tarefa, ID do contexto, intervalo de polling em segundos e endpoint.
|
||||
- **A2APollingStatusEvent**: Emitido em cada iteração de polling. Contém o ID da tarefa, ID do contexto, estado atual da tarefa, segundos decorridos e contagem de polls.
|
||||
- **A2APushNotificationRegisteredEvent**: Emitido quando um callback de push notification é registrado. Contém o ID da tarefa, ID do contexto, URL do callback e endpoint.
|
||||
- **A2APushNotificationReceivedEvent**: Emitido quando uma push notification é recebida do agente A2A remoto. Contém o ID da tarefa, ID do contexto e estado atual.
|
||||
- **A2APushNotificationSentEvent**: Emitido quando uma push notification é enviada para uma URL de callback. Contém o ID da tarefa, ID do contexto, URL do callback, estado, se a entrega foi bem-sucedida e mensagem de erro opcional.
|
||||
- **A2APushNotificationTimeoutEvent**: Emitido quando a espera por push notification expira. Contém o ID da tarefa, ID do contexto e duração do timeout em segundos.
|
||||
|
||||
#### Eventos de Conexão e Autenticação
|
||||
|
||||
- **A2AAgentCardFetchedEvent**: Emitido quando um agent card é obtido com sucesso. Contém o endpoint, nome do agente, metadados do agent card, versão do protocolo, informações do provedor, se foi do cache e tempo de busca em milissegundos.
|
||||
- **A2AAuthenticationFailedEvent**: Emitido quando a autenticação com um agente A2A falha. Contém o endpoint, tipo de autenticação tentada (ex.: `bearer`, `oauth2`, `api_key`), mensagem de erro e código de status HTTP.
|
||||
- **A2AConnectionErrorEvent**: Emitido quando ocorre um erro de conexão durante a comunicação A2A. Contém o endpoint, mensagem de erro, tipo de erro (ex.: `timeout`, `connection_refused`, `dns_error`), código de status HTTP e a operação sendo tentada.
|
||||
- **A2ATransportNegotiatedEvent**: Emitido quando o protocolo de transporte é negociado com um agente A2A. Contém o transporte negociado, URL negociada, fonte de seleção (`client_preferred`, `server_preferred`, `fallback`) e transportes suportados pelo cliente/servidor.
|
||||
- **A2AContentTypeNegotiatedEvent**: Emitido quando os tipos de conteúdo são negociados com um agente A2A. Contém os modos de entrada/saída do cliente/servidor, modos de entrada/saída negociados e se a negociação foi bem-sucedida.
|
||||
|
||||
#### Eventos de Artefatos
|
||||
|
||||
- **A2AArtifactReceivedEvent**: Emitido quando um artefato é recebido de um agente A2A remoto. Contém o ID da tarefa, ID do artefato, nome do artefato, descrição, tipo MIME, tamanho em bytes e se o conteúdo deve ser concatenado.
|
||||
|
||||
#### Eventos de Tarefa do Servidor
|
||||
|
||||
- **A2AServerTaskStartedEvent**: Emitido quando a execução de uma tarefa do servidor A2A é iniciada. Contém o ID da tarefa e ID do contexto.
|
||||
- **A2AServerTaskCompletedEvent**: Emitido quando a execução de uma tarefa do servidor A2A é concluída. Contém o ID da tarefa, ID do contexto e resultado.
|
||||
- **A2AServerTaskCanceledEvent**: Emitido quando a execução de uma tarefa do servidor A2A é cancelada. Contém o ID da tarefa e ID do contexto.
|
||||
- **A2AServerTaskFailedEvent**: Emitido quando a execução de uma tarefa do servidor A2A falha. Contém o ID da tarefa, ID do contexto e mensagem de erro.
|
||||
|
||||
#### Eventos de Ciclo de Vida do Contexto
|
||||
|
||||
- **A2AContextCreatedEvent**: Emitido quando um contexto A2A é criado. Contextos agrupam tarefas relacionadas em uma conversação ou workflow. Contém o ID do contexto e timestamp de criação.
|
||||
- **A2AContextExpiredEvent**: Emitido quando um contexto A2A expira devido ao TTL. Contém o ID do contexto, timestamp de criação, idade em segundos e contagem de tarefas.
|
||||
- **A2AContextIdleEvent**: Emitido quando um contexto A2A fica inativo (sem atividade pelo threshold configurado). Contém o ID do contexto, tempo de inatividade em segundos e contagem de tarefas.
|
||||
- **A2AContextCompletedEvent**: Emitido quando todas as tarefas em um contexto A2A são concluídas. Contém o ID do contexto, total de tarefas e duração em segundos.
|
||||
- **A2AContextPrunedEvent**: Emitido quando um contexto A2A é podado (deletado). Contém o ID do contexto, contagem de tarefas e idade em segundos.
|
||||
|
||||
## Estrutura dos Handlers de Evento
|
||||
|
||||
|
||||
39
docs/pt-BR/enterprise/guides/capture_telemetry_logs.mdx
Normal file
39
docs/pt-BR/enterprise/guides/capture_telemetry_logs.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "Exportação OpenTelemetry"
|
||||
description: "Exporte traces e logs das suas implantações CrewAI AMP para seu próprio coletor OpenTelemetry"
|
||||
icon: "magnifying-glass-chart"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
O CrewAI AMP pode exportar **traces** e **logs** do OpenTelemetry das suas implantações diretamente para seu próprio coletor. Isso permite que você monitore o desempenho dos agentes, rastreie chamadas de LLM e depure problemas usando sua stack de observabilidade existente.
|
||||
|
||||
Os dados de telemetria seguem as [convenções semânticas GenAI do OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/gen-ai/) além de atributos adicionais específicos do CrewAI.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Conta CrewAI AMP" icon="users">
|
||||
Sua organização deve ter uma conta CrewAI AMP ativa.
|
||||
</Card>
|
||||
<Card title="Coletor OpenTelemetry" icon="server">
|
||||
Você precisa de um endpoint de coletor compatível com OpenTelemetry (por exemplo, seu próprio OTel Collector, Datadog, Grafana ou qualquer backend compatível com OTLP).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Configurando um coletor
|
||||
|
||||
1. No CrewAI AMP, vá para **Settings** > **OpenTelemetry Collectors**.
|
||||
2. Clique em **Add Collector**.
|
||||
3. Selecione um tipo de integração — **OpenTelemetry Traces** ou **OpenTelemetry Logs**.
|
||||
4. Configure a conexão:
|
||||
- **Endpoint** — O endpoint OTLP do seu coletor (por exemplo, `https://otel-collector.example.com:4317`).
|
||||
- **Service Name** — Um nome para identificar este serviço na sua plataforma de observabilidade.
|
||||
- **Custom Headers** *(opcional)* — Adicione headers de autenticação ou roteamento como pares chave-valor.
|
||||
- **Certificate** *(opcional)* — Forneça um certificado TLS se o seu coletor exigir um.
|
||||
5. Clique em **Save**.
|
||||
|
||||
<Frame></Frame>
|
||||
|
||||
<Tip>
|
||||
Você pode adicionar múltiplos coletores — por exemplo, um para traces e outro para logs, ou enviar para diferentes backends para diferentes propósitos.
|
||||
</Tip>
|
||||
136
docs/pt-BR/enterprise/guides/custom-mcp-server.mdx
Normal file
136
docs/pt-BR/enterprise/guides/custom-mcp-server.mdx
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: "Servidores MCP Personalizados"
|
||||
description: "Conecte seus próprios servidores MCP ao CrewAI AMP com acesso público, autenticação por token ou OAuth 2.0"
|
||||
icon: "plug"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
O CrewAI AMP suporta a conexão com qualquer servidor MCP que implemente o [Model Context Protocol](https://modelcontextprotocol.io/). Você pode conectar servidores públicos que não exigem autenticação, servidores protegidos por chave de API ou token bearer, e servidores que utilizam OAuth 2.0 para acesso delegado seguro.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Conta CrewAI AMP" icon="user">
|
||||
Você precisa de uma conta ativa no [CrewAI AMP](https://app.crewai.com).
|
||||
</Card>
|
||||
<Card title="URL do Servidor MCP" icon="link">
|
||||
A URL do servidor MCP que você deseja conectar. O servidor deve ser acessível pela internet e suportar transporte Streamable HTTP.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Adicionando um Servidor MCP Personalizado
|
||||
|
||||
<Steps>
|
||||
<Step title="Acesse Tools & Integrations">
|
||||
Navegue até **Tools & Integrations** no menu lateral esquerdo do CrewAI AMP e selecione a aba **Connections**.
|
||||
</Step>
|
||||
|
||||
<Step title="Inicie a adição de um Servidor MCP Personalizado">
|
||||
Clique no botão **Add Custom MCP Server**. Um diálogo aparecerá com o formulário de configuração.
|
||||
</Step>
|
||||
|
||||
<Step title="Preencha as informações básicas">
|
||||
- **Name** (obrigatório): Um nome descritivo para seu servidor MCP (ex.: "Meu Servidor de Ferramentas Internas").
|
||||
- **Description**: Um resumo opcional do que este servidor MCP fornece.
|
||||
- **Server URL** (obrigatório): A URL completa do endpoint do seu servidor MCP (ex.: `https://my-server.example.com/mcp`).
|
||||
</Step>
|
||||
|
||||
<Step title="Escolha um método de autenticação">
|
||||
Selecione um dos três métodos de autenticação disponíveis com base em como seu servidor MCP está protegido. Veja as seções abaixo para detalhes sobre cada método.
|
||||
</Step>
|
||||
|
||||
<Step title="Adicione headers personalizados (opcional)">
|
||||
Se seu servidor MCP requer headers adicionais em cada requisição (ex.: identificadores de tenant ou headers de roteamento), clique em **+ Add Header** e forneça o nome e valor do header. Você pode adicionar múltiplos headers personalizados.
|
||||
</Step>
|
||||
|
||||
<Step title="Crie a conexão">
|
||||
Clique em **Create MCP Server** para salvar a conexão. Seu servidor MCP personalizado aparecerá na lista de Connections e suas ferramentas estarão disponíveis para uso nas suas crews.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Métodos de Autenticação
|
||||
|
||||
### Sem Autenticação
|
||||
|
||||
Escolha esta opção quando seu servidor MCP é publicamente acessível e não requer nenhuma credencial. Isso é comum para servidores open-source ou servidores internos rodando atrás de uma VPN.
|
||||
|
||||
### Token de Autenticação
|
||||
|
||||
Use este método quando seu servidor MCP é protegido por uma chave de API ou token bearer.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/custom-mcp-auth-token.png" alt="Servidor MCP Personalizado com Token de Autenticação" />
|
||||
</Frame>
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
|-------|-------------|-----------|
|
||||
| **Header Name** | Sim | O nome do header HTTP que carrega o token (ex.: `X-API-Key`, `Authorization`). |
|
||||
| **Value** | Sim | Sua chave de API ou token bearer. |
|
||||
| **Add to** | Não | Onde anexar a credencial — **Header** (padrão) ou **Query parameter**. |
|
||||
|
||||
<Tip>
|
||||
Se seu servidor espera um token `Bearer` no header `Authorization`, defina o Header Name como `Authorization` e o Value como `Bearer <seu-token>`.
|
||||
</Tip>
|
||||
|
||||
### OAuth 2.0
|
||||
|
||||
Use este método para servidores MCP que requerem autorização OAuth 2.0. O CrewAI gerenciará todo o fluxo OAuth, incluindo a renovação de tokens.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/custom-mcp-oauth.png" alt="Servidor MCP Personalizado com OAuth 2.0" />
|
||||
</Frame>
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
|-------|-------------|-----------|
|
||||
| **Redirect URI** | — | Preenchido automaticamente e somente leitura. Copie esta URI e registre-a como URI de redirecionamento autorizada no seu provedor OAuth. |
|
||||
| **Authorization Endpoint** | Sim | A URL para onde os usuários são enviados para autorizar o acesso (ex.: `https://auth.example.com/oauth/authorize`). |
|
||||
| **Token Endpoint** | Sim | A URL usada para trocar o código de autorização por um token de acesso (ex.: `https://auth.example.com/oauth/token`). |
|
||||
| **Client ID** | Sim | O Client ID OAuth emitido pelo seu provedor. |
|
||||
| **Client Secret** | Não | O Client Secret OAuth. Não é necessário para clientes públicos usando PKCE. |
|
||||
| **Scopes** | Não | Lista de escopos separados por espaço a solicitar (ex.: `read write`). |
|
||||
| **Token Auth Method** | Não | Como as credenciais do cliente são enviadas ao trocar tokens — **Standard (POST body)** ou **Basic Auth (header)**. Padrão é Standard. |
|
||||
| **PKCE Supported** | Não | Ative se seu provedor OAuth suporta Proof Key for Code Exchange. Recomendado para maior segurança. |
|
||||
|
||||
<Info>
|
||||
**Discover OAuth Config**: Se seu provedor OAuth suporta OpenID Connect Discovery, clique no link **Discover OAuth Config** para preencher automaticamente os endpoints de autorização e token a partir da URL `/.well-known/openid-configuration` do provedor.
|
||||
</Info>
|
||||
|
||||
#### Configurando OAuth 2.0 Passo a Passo
|
||||
|
||||
<Steps>
|
||||
<Step title="Registre a URI de redirecionamento">
|
||||
Copie a **Redirect URI** exibida no formulário e adicione-a como URI de redirecionamento autorizada nas configurações do seu provedor OAuth.
|
||||
</Step>
|
||||
|
||||
<Step title="Insira os endpoints e credenciais">
|
||||
Preencha o **Authorization Endpoint**, **Token Endpoint**, **Client ID** e, opcionalmente, o **Client Secret** e **Scopes**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure o método de troca de tokens">
|
||||
Selecione o **Token Auth Method** apropriado. A maioria dos provedores usa o padrão **Standard (POST body)**. Alguns provedores mais antigos requerem **Basic Auth (header)**.
|
||||
</Step>
|
||||
|
||||
<Step title="Ative o PKCE (recomendado)">
|
||||
Marque **PKCE Supported** se seu provedor suporta. O PKCE adiciona uma camada extra de segurança ao fluxo de código de autorização e é recomendado para todas as novas integrações.
|
||||
</Step>
|
||||
|
||||
<Step title="Crie e autorize">
|
||||
Clique em **Create MCP Server**. Você será redirecionado ao seu provedor OAuth para autorizar o acesso. Uma vez autorizado, o CrewAI armazenará os tokens e os renovará automaticamente conforme necessário.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Usando Seu Servidor MCP Personalizado
|
||||
|
||||
Uma vez conectado, as ferramentas do seu servidor MCP personalizado aparecem junto com as conexões integradas na página **Tools & Integrations**. Você pode:
|
||||
|
||||
- **Atribuir ferramentas a agentes** nas suas crews, assim como qualquer outra ferramenta CrewAI.
|
||||
- **Gerenciar visibilidade** para controlar quais membros da equipe podem usar o servidor.
|
||||
- **Editar ou remover** a conexão a qualquer momento na lista de Connections.
|
||||
|
||||
<Warning>
|
||||
Se seu servidor MCP ficar inacessível ou as credenciais expirarem, as chamadas de ferramentas usando esse servidor falharão. Certifique-se de que a URL do servidor seja estável e as credenciais estejam atualizadas.
|
||||
</Warning>
|
||||
|
||||
<Card title="Precisa de Ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nossa equipe de suporte para assistência com configuração ou resolução de problemas de servidores MCP personalizados.
|
||||
</Card>
|
||||
61
docs/pt-BR/guides/coding-tools/agents-md.mdx
Normal file
61
docs/pt-BR/guides/coding-tools/agents-md.mdx
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Ferramentas de Codificação
|
||||
description: Use o AGENTS.md para guiar agentes de codificação e IDEs em seus projetos CrewAI.
|
||||
icon: terminal
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Por que AGENTS.md
|
||||
|
||||
`AGENTS.md` é um arquivo de instruções leve e local do repositório que fornece aos agentes de codificação orientações consistentes e específicas do projeto. Mantenha-o na raiz do projeto e trate-o como a fonte da verdade para como você deseja que os assistentes trabalhem: convenções, comandos, notas de arquitetura e proteções.
|
||||
|
||||
## Criar um Projeto com o CLI
|
||||
|
||||
Use o CLI do CrewAI para criar a estrutura de um projeto, e o `AGENTS.md` será automaticamente adicionado na raiz.
|
||||
|
||||
```bash
|
||||
# Crew
|
||||
crewai create crew my_crew
|
||||
|
||||
# Flow
|
||||
crewai create flow my_flow
|
||||
|
||||
# Tool repository
|
||||
crewai tool create my_tool
|
||||
```
|
||||
|
||||
## Configuração de Ferramentas: Direcione Assistentes para o AGENTS.md
|
||||
|
||||
### Codex
|
||||
|
||||
O Codex pode ser guiado por arquivos `AGENTS.md` colocados no seu repositório. Use-os para fornecer contexto persistente do projeto, como convenções, comandos e expectativas de fluxo de trabalho.
|
||||
|
||||
### Claude Code
|
||||
|
||||
O Claude Code armazena a memória do projeto em `CLAUDE.md`. Você pode inicializá-lo com `/init` e editá-lo usando `/memory`. O Claude Code também suporta importações dentro do `CLAUDE.md`, então você pode adicionar uma única linha como `@AGENTS.md` para incluir as instruções compartilhadas sem duplicá-las.
|
||||
|
||||
Você pode simplesmente usar:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md CLAUDE.md
|
||||
```
|
||||
|
||||
### Gemini CLI e Google Antigravity
|
||||
|
||||
O Gemini CLI e o Antigravity carregam um arquivo de contexto do projeto (padrão: `GEMINI.md`) da raiz do repositório e diretórios pais. Você pode configurá-lo para ler o `AGENTS.md` em vez disso (ou além) definindo `context.fileName` nas configurações do Gemini CLI. Por exemplo, defina apenas para `AGENTS.md`, ou inclua tanto `AGENTS.md` quanto `GEMINI.md` se quiser manter o formato de cada ferramenta.
|
||||
|
||||
Você pode simplesmente usar:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md GEMINI.md
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
O Cursor suporta `AGENTS.md` como arquivo de instruções do projeto. Coloque-o na raiz do projeto para fornecer orientação ao assistente de codificação do Cursor.
|
||||
|
||||
### Windsurf
|
||||
|
||||
O Claude Code fornece uma integração oficial com o Windsurf. Se você usa o Claude Code dentro do Windsurf, siga a orientação do Claude Code acima e importe o `AGENTS.md` a partir do `CLAUDE.md`.
|
||||
|
||||
Se você está usando o assistente nativo do Windsurf, configure o recurso de regras ou instruções do projeto (se disponível) para ler o `AGENTS.md` ou cole o conteúdo diretamente.
|
||||
244
docs/pt-BR/guides/tools/publish-custom-tools.mdx
Normal file
244
docs/pt-BR/guides/tools/publish-custom-tools.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: Publicar Ferramentas Personalizadas
|
||||
description: Como construir, empacotar e publicar suas próprias ferramentas compatíveis com CrewAI no PyPI para que qualquer usuário do CrewAI possa instalá-las e usá-las.
|
||||
icon: box-open
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O sistema de ferramentas do CrewAI foi projetado para ser extensível. Se você construiu uma ferramenta que pode beneficiar outros, pode empacotá-la como uma biblioteca Python independente, publicá-la no PyPI e disponibilizá-la para qualquer usuário do CrewAI — sem necessidade de PR para o repositório do CrewAI.
|
||||
|
||||
Este guia percorre todo o processo: implementação do contrato de ferramentas, estruturação do pacote e publicação no PyPI.
|
||||
|
||||
<Note type="info" title="Não pretende publicar?">
|
||||
Se você precisa apenas de uma ferramenta personalizada para seu próprio projeto, consulte o guia [Criar Ferramentas Personalizadas](/pt-BR/learn/create-custom-tools).
|
||||
</Note>
|
||||
|
||||
## O Contrato de Ferramentas
|
||||
|
||||
Toda ferramenta CrewAI deve satisfazer uma das duas interfaces:
|
||||
|
||||
### Opção 1: Subclassificar `BaseTool`
|
||||
|
||||
Subclassifique `crewai.tools.BaseTool` e implemente o método `_run`. Defina `name`, `description` e, opcionalmente, um `args_schema` para validação de entrada.
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateInput(BaseModel):
|
||||
"""Esquema de entrada para GeolocateTool."""
|
||||
address: str = Field(..., description="O endereço para geolocalizar.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converte um endereço em coordenadas de latitude/longitude."
|
||||
args_schema: type[BaseModel] = GeolocateInput
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# Sua implementação aqui
|
||||
return f"40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### Opção 2: Usar o Decorador `@tool`
|
||||
|
||||
Para ferramentas mais simples, o decorador `@tool` transforma uma função em uma ferramenta CrewAI. A função **deve** ter uma docstring (usada como descrição da ferramenta) e anotações de tipo.
|
||||
|
||||
```python
|
||||
from crewai.tools import tool
|
||||
|
||||
|
||||
@tool("Geolocate")
|
||||
def geolocate(address: str) -> str:
|
||||
"""Converte um endereço em coordenadas de latitude/longitude."""
|
||||
return "40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### Requisitos Essenciais
|
||||
|
||||
Independentemente da abordagem escolhida, sua ferramenta deve:
|
||||
|
||||
- Ter um **`name`** — um identificador curto e descritivo.
|
||||
- Ter uma **`description`** — informa ao agente quando e como usar a ferramenta. Isso afeta diretamente a qualidade do uso da ferramenta pelo agente, então seja claro e específico.
|
||||
- Implementar **`_run`** (BaseTool) ou fornecer um **corpo de função** (@tool) — a lógica de execução síncrona.
|
||||
- Usar **anotações de tipo** em todos os parâmetros e valores de retorno.
|
||||
- Retornar um resultado em **string** (ou algo que possa ser convertido de forma significativa).
|
||||
|
||||
### Opcional: Suporte Assíncrono
|
||||
|
||||
Se sua ferramenta realiza operações de I/O, implemente `_arun` para execução assíncrona:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converte um endereço em coordenadas de latitude/longitude."
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# Implementação síncrona
|
||||
...
|
||||
|
||||
async def _arun(self, address: str) -> str:
|
||||
# Implementação assíncrona
|
||||
...
|
||||
```
|
||||
|
||||
### Opcional: Validação de Entrada com `args_schema`
|
||||
|
||||
Defina um modelo Pydantic como seu `args_schema` para obter validação automática de entrada e mensagens de erro claras. Se não fornecer um, o CrewAI irá inferi-lo da assinatura do seu método `_run`.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TranslateInput(BaseModel):
|
||||
"""Esquema de entrada para TranslateTool."""
|
||||
text: str = Field(..., description="O texto a ser traduzido.")
|
||||
target_language: str = Field(
|
||||
default="en",
|
||||
description="Código de idioma ISO 639-1 para o idioma de destino.",
|
||||
)
|
||||
```
|
||||
|
||||
Esquemas explícitos são recomendados para ferramentas publicadas — produzem melhor comportamento do agente e documentação mais clara para seus usuários.
|
||||
|
||||
### Opcional: Variáveis de Ambiente
|
||||
|
||||
Se sua ferramenta requer chaves de API ou outra configuração, declare-as com `env_vars` para que os usuários saibam o que configurar:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converte um endereço em coordenadas de latitude/longitude."
|
||||
env_vars: list[EnvVar] = [
|
||||
EnvVar(
|
||||
name="GEOCODING_API_KEY",
|
||||
description="Chave de API para o serviço de geocodificação.",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
## Estrutura do Pacote
|
||||
|
||||
Estruture seu projeto como um pacote Python padrão. Layout recomendado:
|
||||
|
||||
```
|
||||
crewai-geolocate/
|
||||
├── pyproject.toml
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── src/
|
||||
└── crewai_geolocate/
|
||||
├── __init__.py
|
||||
└── tools.py
|
||||
```
|
||||
|
||||
### `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "crewai-geolocate"
|
||||
version = "0.1.0"
|
||||
description = "Uma ferramenta CrewAI para geolocalizar endereços."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"crewai",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
```
|
||||
|
||||
Declare `crewai` como dependência para que os usuários obtenham automaticamente uma versão compatível.
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
Re-exporte suas classes de ferramenta para que os usuários possam importá-las diretamente:
|
||||
|
||||
```python
|
||||
from crewai_geolocate.tools import GeolocateTool
|
||||
|
||||
__all__ = ["GeolocateTool"]
|
||||
```
|
||||
|
||||
### Convenções de Nomenclatura
|
||||
|
||||
- **Nome do pacote**: Use o prefixo `crewai-` (ex.: `crewai-geolocate`). Isso torna sua ferramenta fácil de encontrar no PyPI.
|
||||
- **Nome do módulo**: Use underscores (ex.: `crewai_geolocate`).
|
||||
- **Nome da classe da ferramenta**: Use PascalCase terminando em `Tool` (ex.: `GeolocateTool`).
|
||||
|
||||
## Testando sua Ferramenta
|
||||
|
||||
Antes de publicar, verifique se sua ferramenta funciona dentro de uma crew:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Analista de Localização",
|
||||
goal="Encontrar coordenadas para os endereços fornecidos.",
|
||||
backstory="Um especialista em dados geoespaciais.",
|
||||
tools=[GeolocateTool()],
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Encontre as coordenadas de 1600 Pennsylvania Avenue, Washington, DC.",
|
||||
expected_output="A latitude e longitude do endereço.",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Publicando no PyPI
|
||||
|
||||
Quando sua ferramenta estiver testada e pronta:
|
||||
|
||||
```bash
|
||||
# Construir o pacote
|
||||
uv build
|
||||
|
||||
# Publicar no PyPI
|
||||
uv publish
|
||||
```
|
||||
|
||||
Se é sua primeira vez publicando, você precisará de uma [conta no PyPI](https://pypi.org/account/register/) e um [token de API](https://pypi.org/help/#apitoken).
|
||||
|
||||
### Após a Publicação
|
||||
|
||||
Os usuários podem instalar sua ferramenta com:
|
||||
|
||||
```bash
|
||||
pip install crewai-geolocate
|
||||
```
|
||||
|
||||
Ou com uv:
|
||||
|
||||
```bash
|
||||
uv add crewai-geolocate
|
||||
```
|
||||
|
||||
E então usá-la em suas crews:
|
||||
|
||||
```python
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Analista de Localização",
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
@@ -11,6 +11,10 @@ Este guia traz instruções detalhadas sobre como criar ferramentas personalizad
|
||||
incorporando funcionalidades recentes, como delegação de ferramentas, tratamento de erros e chamada dinâmica de ferramentas. Destaca também a importância de ferramentas de colaboração,
|
||||
permitindo que agentes executem uma ampla gama de ações.
|
||||
|
||||
<Tip>
|
||||
**Quer publicar sua ferramenta para a comunidade?** Se você está construindo uma ferramenta que pode beneficiar outros, confira o guia [Publicar Ferramentas Personalizadas](/pt-BR/guides/tools/publish-custom-tools) para aprender como empacotar e distribuir sua ferramenta no PyPI.
|
||||
</Tip>
|
||||
|
||||
### Subclassificando `BaseTool`
|
||||
|
||||
Para criar uma ferramenta personalizada, herde de `BaseTool` e defina os atributos necessários, incluindo o `args_schema` para validação de entrada e o método `_run`.
|
||||
|
||||
@@ -62,22 +62,22 @@ Use a sintaxe `#` para selecionar ferramentas específicas de um servidor:
|
||||
"https://mcp.exa.ai/mcp?api_key=sua_chave#web_search_exa"
|
||||
```
|
||||
|
||||
### Marketplace CrewAI AMP
|
||||
### Integrações MCP Conectadas
|
||||
|
||||
Acesse ferramentas do marketplace CrewAI AMP:
|
||||
Conecte servidores MCP do catálogo CrewAI ou traga os seus próprios. Uma vez conectados em sua conta, referencie-os pelo slug:
|
||||
|
||||
```python
|
||||
# Serviço completo com todas as ferramentas
|
||||
"crewai-amp:financial-data"
|
||||
# MCP conectado com todas as ferramentas
|
||||
"snowflake"
|
||||
|
||||
# Ferramenta específica do serviço AMP
|
||||
"crewai-amp:research-tools#pubmed_search"
|
||||
# Ferramenta específica de um MCP conectado
|
||||
"stripe#list_invoices"
|
||||
|
||||
# Múltiplos serviços AMP
|
||||
# Múltiplos MCPs conectados
|
||||
mcps=[
|
||||
"crewai-amp:weather-insights",
|
||||
"crewai-amp:market-analysis",
|
||||
"crewai-amp:social-media-monitoring"
|
||||
"snowflake",
|
||||
"stripe",
|
||||
"github"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -99,10 +99,10 @@ agente_multi_fonte = Agent(
|
||||
"https://mcp.exa.ai/mcp?api_key=sua_chave_exa&profile=pesquisa",
|
||||
"https://weather.api.com/mcp#get_current_conditions",
|
||||
|
||||
# Marketplace CrewAI AMP
|
||||
"crewai-amp:financial-insights",
|
||||
"crewai-amp:academic-research#pubmed_search",
|
||||
"crewai-amp:market-intelligence#competitor_analysis"
|
||||
# MCPs conectados do catálogo
|
||||
"snowflake",
|
||||
"stripe#list_invoices",
|
||||
"github#search_repositories"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -154,7 +154,7 @@ agente = Agent(
|
||||
"https://servidor-confiavel.com/mcp", # Vai funcionar
|
||||
"https://servidor-inalcancavel.com/mcp", # Será ignorado graciosamente
|
||||
"https://servidor-lento.com/mcp", # Timeout gracioso
|
||||
"crewai-amp:servico-funcionando" # Vai funcionar
|
||||
"snowflake" # MCP conectado do catálogo
|
||||
]
|
||||
)
|
||||
# O agente usará ferramentas de servidores funcionais e registrará avisos para os que falharem
|
||||
@@ -229,6 +229,6 @@ agente = Agent(
|
||||
mcps=[
|
||||
"https://api-principal.com/mcp", # Escolha principal
|
||||
"https://api-backup.com/mcp", # Opção de backup
|
||||
"crewai-amp:servico-confiavel" # Fallback AMP
|
||||
"snowflake" # Fallback MCP conectado
|
||||
]
|
||||
```
|
||||
|
||||
@@ -25,8 +25,8 @@ agent = Agent(
|
||||
mcps=[
|
||||
"https://mcp.exa.ai/mcp?api_key=sua_chave", # Servidor MCP externo
|
||||
"https://api.weather.com/mcp#get_forecast", # Ferramenta específica do servidor
|
||||
"crewai-amp:financial-data", # Marketplace CrewAI AMP
|
||||
"crewai-amp:research-tools#pubmed_search" # Ferramenta AMP específica
|
||||
"snowflake", # MCP conectado do catálogo
|
||||
"stripe#list_invoices" # Ferramenta específica de MCP conectado
|
||||
]
|
||||
)
|
||||
# Ferramentas MCP agora estão automaticamente disponíveis para seu agente!
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.10.2rc2"
|
||||
__version__ = "1.11.0"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.10.2rc2",
|
||||
"crewai==1.11.0",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.10.2rc2"
|
||||
__version__ = "1.11.0"
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
# CodeInterpreterTool
|
||||
|
||||
## Description
|
||||
This tool is used to give the Agent the ability to run code (Python3) from the code generated by the Agent itself. The code is executed in a sandboxed environment, so it is safe to run any code.
|
||||
This tool is used to give the Agent the ability to run code (Python3) from the code generated by the Agent itself. The code is executed in a Docker container for secure isolation.
|
||||
|
||||
It is incredible useful since it allows the Agent to generate code, run it in the same environment, get the result and use it to make decisions.
|
||||
It is incredibly useful since it allows the Agent to generate code, run it in an isolated environment, get the result and use it to make decisions.
|
||||
|
||||
## ⚠️ Security Requirements
|
||||
|
||||
**Docker is REQUIRED** for safe code execution. The tool will refuse to execute code without Docker to prevent security vulnerabilities.
|
||||
|
||||
### Why Docker is Required
|
||||
|
||||
Previous versions included a "restricted sandbox" fallback when Docker was unavailable. This has been **removed** due to critical security vulnerabilities:
|
||||
|
||||
- The Python-based sandbox could be escaped via object introspection
|
||||
- Attackers could recover the original `__import__` function and access any module
|
||||
- This allowed arbitrary command execution on the host system
|
||||
|
||||
**Docker provides real process isolation** and is the only secure way to execute untrusted code.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker
|
||||
- **Docker (REQUIRED)** - Install from [docker.com](https://docs.docker.com/get-docker/)
|
||||
|
||||
## Installation
|
||||
Install the crewai_tools package
|
||||
@@ -17,7 +31,9 @@ pip install 'crewai[tools]'
|
||||
|
||||
## Example
|
||||
|
||||
Remember that when using this tool, the code must be generated by the Agent itself. The code must be a Python3 code. And it will take some time for the first time to run because it needs to build the Docker image.
|
||||
Remember that when using this tool, the code must be generated by the Agent itself. The code must be Python3 code. It will take some time the first time to run because it needs to build the Docker image.
|
||||
|
||||
### Basic Usage (Docker Container - Recommended)
|
||||
|
||||
```python
|
||||
from crewai_tools import CodeInterpreterTool
|
||||
@@ -28,7 +44,9 @@ Agent(
|
||||
)
|
||||
```
|
||||
|
||||
Or if you need to pass your own Dockerfile just do this
|
||||
### Custom Dockerfile
|
||||
|
||||
If you need to pass your own Dockerfile:
|
||||
|
||||
```python
|
||||
from crewai_tools import CodeInterpreterTool
|
||||
@@ -39,15 +57,39 @@ Agent(
|
||||
)
|
||||
```
|
||||
|
||||
If it is difficult to connect to docker daemon automatically (especially for macOS users), you can do this to setup docker host manually
|
||||
### Manual Docker Host Configuration
|
||||
|
||||
If it is difficult to connect to the Docker daemon automatically (especially for macOS users), you can set up the Docker host manually:
|
||||
|
||||
```python
|
||||
from crewai_tools import CodeInterpreterTool
|
||||
|
||||
Agent(
|
||||
...
|
||||
tools=[CodeInterpreterTool(user_docker_base_url="<Docker Host Base Url>",
|
||||
user_dockerfile_path="<Dockerfile_path>")],
|
||||
tools=[CodeInterpreterTool(
|
||||
user_docker_base_url="<Docker Host Base Url>",
|
||||
user_dockerfile_path="<Dockerfile_path>"
|
||||
)],
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### Unsafe Mode (NOT RECOMMENDED)
|
||||
|
||||
If you absolutely cannot use Docker and **fully trust the code source**, you can use unsafe mode:
|
||||
|
||||
```python
|
||||
from crewai_tools import CodeInterpreterTool
|
||||
|
||||
# WARNING: Only use with fully trusted code!
|
||||
Agent(
|
||||
...
|
||||
tools=[CodeInterpreterTool(unsafe_mode=True)],
|
||||
)
|
||||
```
|
||||
|
||||
**⚠️ SECURITY WARNING:** `unsafe_mode=True` executes code directly on the host without any isolation. Only use this if:
|
||||
- You completely trust the code being executed
|
||||
- You understand the security risks
|
||||
- You cannot install Docker in your environment
|
||||
|
||||
For production use, **always use Docker** (the default mode).
|
||||
|
||||
@@ -8,6 +8,7 @@ potentially unsafe operations and importing restricted modules.
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any, ClassVar, TypedDict
|
||||
|
||||
@@ -50,11 +51,16 @@ class CodeInterpreterSchema(BaseModel):
|
||||
|
||||
|
||||
class SandboxPython:
|
||||
"""A restricted Python execution environment for running code safely.
|
||||
"""INSECURE: A restricted Python execution environment with known vulnerabilities.
|
||||
|
||||
This class provides methods to safely execute Python code by restricting access to
|
||||
potentially dangerous modules and built-in functions. It creates a sandboxed
|
||||
environment where harmful operations are blocked.
|
||||
WARNING: This class does NOT provide real security isolation and is vulnerable to
|
||||
sandbox escape attacks via Python object introspection. Attackers can recover the
|
||||
original __import__ function and bypass all restrictions.
|
||||
|
||||
DO NOT USE for untrusted code execution. Use Docker containers instead.
|
||||
|
||||
This class attempts to restrict access to dangerous modules and built-in functions
|
||||
but provides no real security boundary against a motivated attacker.
|
||||
"""
|
||||
|
||||
BLOCKED_MODULES: ClassVar[set[str]] = {
|
||||
@@ -299,8 +305,8 @@ class CodeInterpreterTool(BaseTool):
|
||||
def run_code_safety(self, code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs code in the safest available environment.
|
||||
|
||||
Attempts to run code in Docker if available, falls back to a restricted
|
||||
sandbox if Docker is not available.
|
||||
Requires Docker to be available for secure code execution. Fails closed
|
||||
if Docker is not available to prevent sandbox escape vulnerabilities.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
@@ -308,10 +314,24 @@ class CodeInterpreterTool(BaseTool):
|
||||
|
||||
Returns:
|
||||
The output of the executed code as a string.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Docker is not available, as the restricted sandbox
|
||||
is vulnerable to escape attacks and should not be used
|
||||
for untrusted code execution.
|
||||
"""
|
||||
if self._check_docker_available():
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
return self.run_code_in_restricted_sandbox(code)
|
||||
|
||||
error_msg = (
|
||||
"Docker is required for safe code execution but is not available. "
|
||||
"The restricted sandbox fallback has been removed due to security vulnerabilities "
|
||||
"that allow sandbox escape via Python object introspection. "
|
||||
"Please install Docker (https://docs.docker.com/get-docker/) or use unsafe_mode=True "
|
||||
"if you trust the code source and understand the security risks."
|
||||
)
|
||||
Printer.print(error_msg, color="bold_red")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
def run_code_in_docker(self, code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs Python code in a Docker container for safe isolation.
|
||||
@@ -342,10 +362,19 @@ class CodeInterpreterTool(BaseTool):
|
||||
|
||||
@staticmethod
|
||||
def run_code_in_restricted_sandbox(code: str) -> str:
|
||||
"""Runs Python code in a restricted sandbox environment.
|
||||
"""DEPRECATED AND INSECURE: Runs Python code in a restricted sandbox environment.
|
||||
|
||||
Executes the code with restricted access to potentially dangerous modules and
|
||||
built-in functions for basic safety when Docker is not available.
|
||||
WARNING: This method is vulnerable to sandbox escape attacks via Python object
|
||||
introspection and should NOT be used for untrusted code execution. It has been
|
||||
deprecated and is only kept for backward compatibility with trusted code.
|
||||
|
||||
The "restricted" environment can be bypassed by attackers who can:
|
||||
- Use object graph introspection to recover the original __import__ function
|
||||
- Access any Python module including os, subprocess, sys, etc.
|
||||
- Execute arbitrary commands on the host system
|
||||
|
||||
Use run_code_in_docker() for secure code execution, or run_code_unsafe()
|
||||
if you explicitly acknowledge the security risks.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
@@ -354,7 +383,10 @@ class CodeInterpreterTool(BaseTool):
|
||||
The value of the 'result' variable from the executed code,
|
||||
or an error message if execution failed.
|
||||
"""
|
||||
Printer.print("Running code in restricted sandbox", color="yellow")
|
||||
Printer.print(
|
||||
"WARNING: Running code in INSECURE restricted sandbox (vulnerable to escape attacks)",
|
||||
color="bold_red"
|
||||
)
|
||||
exec_locals: dict[str, Any] = {}
|
||||
try:
|
||||
SandboxPython.exec(code=code, locals_=exec_locals)
|
||||
@@ -380,7 +412,7 @@ class CodeInterpreterTool(BaseTool):
|
||||
Printer.print("WARNING: Running code in unsafe mode", color="bold_magenta")
|
||||
# Install libraries on the host machine
|
||||
for library in libraries_used:
|
||||
os.system(f"pip install {library}") # noqa: S605
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", library], check=False) # noqa: S603
|
||||
|
||||
# Execute the code
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
@@ -30,27 +31,39 @@ class FileWriterTool(BaseTool):
|
||||
|
||||
def _run(self, **kwargs: Any) -> str:
|
||||
try:
|
||||
directory = kwargs.get("directory") or "./"
|
||||
filename = kwargs["filename"]
|
||||
|
||||
filepath = os.path.join(directory, filename)
|
||||
|
||||
# Prevent path traversal: the resolved path must be strictly inside
|
||||
# the resolved directory. This blocks ../sequences, absolute paths in
|
||||
# filename, and symlink escapes regardless of how directory is set.
|
||||
# is_relative_to() does a proper path-component comparison that is
|
||||
# safe on case-insensitive filesystems and avoids the "// " edge case
|
||||
# that plagues startswith(real_directory + os.sep).
|
||||
# We also reject the case where filepath resolves to the directory
|
||||
# itself, since that is not a valid file target.
|
||||
real_directory = Path(directory).resolve()
|
||||
real_filepath = Path(filepath).resolve()
|
||||
if not real_filepath.is_relative_to(real_directory) or real_filepath == real_directory:
|
||||
return "Error: Invalid file path — the filename must not escape the target directory."
|
||||
|
||||
if kwargs.get("directory"):
|
||||
os.makedirs(kwargs["directory"], exist_ok=True)
|
||||
os.makedirs(real_directory, exist_ok=True)
|
||||
|
||||
# Construct the full path
|
||||
filepath = os.path.join(kwargs.get("directory") or "", kwargs["filename"])
|
||||
|
||||
# Convert overwrite to boolean
|
||||
kwargs["overwrite"] = strtobool(kwargs["overwrite"])
|
||||
|
||||
# Check if file exists and overwrite is not allowed
|
||||
if os.path.exists(filepath) and not kwargs["overwrite"]:
|
||||
return f"File {filepath} already exists and overwrite option was not passed."
|
||||
if os.path.exists(real_filepath) and not kwargs["overwrite"]:
|
||||
return f"File {real_filepath} already exists and overwrite option was not passed."
|
||||
|
||||
# Write content to the file
|
||||
mode = "w" if kwargs["overwrite"] else "x"
|
||||
with open(filepath, mode) as file:
|
||||
with open(real_filepath, mode) as file:
|
||||
file.write(kwargs["content"])
|
||||
return f"Content successfully written to {filepath}"
|
||||
return f"Content successfully written to {real_filepath}"
|
||||
except FileExistsError:
|
||||
return (
|
||||
f"File {filepath} already exists and overwrite option was not passed."
|
||||
f"File {real_filepath} already exists and overwrite option was not passed."
|
||||
)
|
||||
except KeyError as e:
|
||||
return f"An error occurred while accessing key: {e!s}"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
from crewai_tools.tools.code_interpreter_tool.code_interpreter_tool import (
|
||||
@@ -76,24 +77,22 @@ print("This is line 2")"""
|
||||
)
|
||||
|
||||
|
||||
def test_restricted_sandbox_basic_code_execution(printer_mock, docker_unavailable_mock):
|
||||
"""Test basic code execution."""
|
||||
def test_docker_unavailable_raises_error(printer_mock, docker_unavailable_mock):
|
||||
"""Test that execution fails when Docker is unavailable in safe mode."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = """
|
||||
result = 2 + 2
|
||||
print(result)
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
assert result == 4
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert "Docker is required for safe code execution" in str(exc_info.value)
|
||||
assert "sandbox escape" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_blocked_modules(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test that restricted modules cannot be imported."""
|
||||
def test_restricted_sandbox_running_with_blocked_modules():
|
||||
"""Test that restricted modules cannot be imported when using the deprecated sandbox directly."""
|
||||
tool = CodeInterpreterTool()
|
||||
restricted_modules = SandboxPython.BLOCKED_MODULES
|
||||
|
||||
@@ -102,18 +101,15 @@ def test_restricted_sandbox_running_with_blocked_modules(
|
||||
import {module}
|
||||
result = "Import succeeded"
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
|
||||
# Note: run_code_in_restricted_sandbox is deprecated and insecure
|
||||
# This test verifies the old behavior but should not be used in production
|
||||
result = tool.run_code_in_restricted_sandbox(code)
|
||||
|
||||
assert f"An error occurred: Importing '{module}' is not allowed" in result
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_blocked_builtins(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test that restricted builtins are not available."""
|
||||
def test_restricted_sandbox_running_with_blocked_builtins():
|
||||
"""Test that restricted builtins are not available when using the deprecated sandbox directly."""
|
||||
tool = CodeInterpreterTool()
|
||||
restricted_builtins = SandboxPython.UNSAFE_BUILTINS
|
||||
|
||||
@@ -122,25 +118,23 @@ def test_restricted_sandbox_running_with_blocked_builtins(
|
||||
{builtin}("test")
|
||||
result = "Builtin available"
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
# Note: run_code_in_restricted_sandbox is deprecated and insecure
|
||||
# This test verifies the old behavior but should not be used in production
|
||||
result = tool.run_code_in_restricted_sandbox(code)
|
||||
assert f"An error occurred: name '{builtin}' is not defined" in result
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_no_result_variable(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test behavior when no result variable is set."""
|
||||
"""Test behavior when no result variable is set in deprecated sandbox."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = """
|
||||
x = 10
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
# Note: run_code_in_restricted_sandbox is deprecated and insecure
|
||||
# This test verifies the old behavior but should not be used in production
|
||||
result = tool.run_code_in_restricted_sandbox(code)
|
||||
assert result == "No result variable found."
|
||||
|
||||
|
||||
@@ -159,6 +153,44 @@ x = 10
|
||||
assert result == "No result variable found."
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.subprocess.run")
|
||||
def test_unsafe_mode_installs_libraries_without_shell(
|
||||
subprocess_run_mock, printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test that library installation uses subprocess.run with shell=False, not os.system."""
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
code = "result = 1"
|
||||
libraries_used = ["numpy", "pandas"]
|
||||
|
||||
tool.run(code=code, libraries_used=libraries_used)
|
||||
|
||||
assert subprocess_run_mock.call_count == 2
|
||||
for call, library in zip(subprocess_run_mock.call_args_list, libraries_used):
|
||||
args, kwargs = call
|
||||
# Must be list form (no shell expansion possible)
|
||||
assert args[0] == [sys.executable, "-m", "pip", "install", library]
|
||||
# shell= must not be True (defaults to False)
|
||||
assert kwargs.get("shell", False) is False
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.subprocess.run")
|
||||
def test_unsafe_mode_library_name_with_shell_metacharacters_does_not_invoke_shell(
|
||||
subprocess_run_mock, printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test that a malicious library name cannot inject shell commands."""
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
code = "result = 1"
|
||||
malicious_library = "numpy; rm -rf /"
|
||||
|
||||
tool.run(code=code, libraries_used=[malicious_library])
|
||||
|
||||
subprocess_run_mock.assert_called_once()
|
||||
args, kwargs = subprocess_run_mock.call_args
|
||||
# The entire malicious string is passed as a single argument — no shell parsing
|
||||
assert args[0] == [sys.executable, "-m", "pip", "install", malicious_library]
|
||||
assert kwargs.get("shell", False) is False
|
||||
|
||||
|
||||
def test_unsafe_mode_running_unsafe_code(printer_mock, docker_unavailable_mock):
|
||||
"""Test behavior when no result variable is set."""
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
@@ -172,3 +204,50 @@ result = eval("5/1")
|
||||
"WARNING: Running code in unsafe mode", color="bold_magenta"
|
||||
)
|
||||
assert 5.0 == result
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason=(
|
||||
"run_code_in_restricted_sandbox is known to be vulnerable to sandbox "
|
||||
"escape via object introspection. This test encodes the desired secure "
|
||||
"behavior (no escape possible) and will start passing once the "
|
||||
"vulnerability is fixed or the function is removed."
|
||||
)
|
||||
)
|
||||
def test_sandbox_escape_vulnerability_demonstration(printer_mock):
|
||||
"""Demonstrate that the restricted sandbox is vulnerable to escape attacks.
|
||||
|
||||
This test shows that an attacker can use Python object introspection to bypass
|
||||
the restricted sandbox and access blocked modules like 'os'. This is why the
|
||||
sandbox should never be used for untrusted code execution.
|
||||
|
||||
NOTE: This test uses the deprecated run_code_in_restricted_sandbox directly
|
||||
to demonstrate the vulnerability. In production, Docker is now required.
|
||||
"""
|
||||
tool = CodeInterpreterTool()
|
||||
|
||||
# Classic Python sandbox escape via object introspection
|
||||
escape_code = """
|
||||
# Recover the real __import__ function via object introspection
|
||||
for cls in ().__class__.__bases__[0].__subclasses__():
|
||||
if cls.__name__ == 'catch_warnings':
|
||||
# Get the real builtins module
|
||||
real_builtins = cls()._module.__builtins__
|
||||
real_import = real_builtins['__import__']
|
||||
# Now we can import os and execute commands
|
||||
os = real_import('os')
|
||||
# Demonstrate we have escaped the sandbox
|
||||
result = "SANDBOX_ESCAPED" if hasattr(os, 'system') else "FAILED"
|
||||
break
|
||||
"""
|
||||
|
||||
# The deprecated sandbox is vulnerable to this attack
|
||||
result = tool.run_code_in_restricted_sandbox(escape_code)
|
||||
|
||||
# Desired behavior: the restricted sandbox should prevent this escape.
|
||||
# If this assertion fails, run_code_in_restricted_sandbox remains vulnerable.
|
||||
assert result != "SANDBOX_ESCAPED", (
|
||||
"The restricted sandbox was bypassed via object introspection. "
|
||||
"This indicates run_code_in_restricted_sandbox is still vulnerable and "
|
||||
"is why Docker is now required for safe code execution."
|
||||
)
|
||||
|
||||
@@ -135,3 +135,59 @@ def test_file_exists_error_handling(tool, temp_env, overwrite):
|
||||
|
||||
assert "already exists and overwrite option was not passed" in result
|
||||
assert read_file(path) == "Pre-existing content"
|
||||
|
||||
|
||||
# --- Path traversal prevention ---
|
||||
|
||||
def test_blocks_traversal_in_filename(tool, temp_env):
|
||||
# Create a sibling "outside" directory so we can assert nothing was written there.
|
||||
outside_dir = tempfile.mkdtemp()
|
||||
outside_file = os.path.join(outside_dir, "outside.txt")
|
||||
try:
|
||||
result = tool._run(
|
||||
filename=f"../{os.path.basename(outside_dir)}/outside.txt",
|
||||
directory=temp_env["temp_dir"],
|
||||
content="should not be written",
|
||||
overwrite=True,
|
||||
)
|
||||
assert "Error" in result
|
||||
assert not os.path.exists(outside_file)
|
||||
finally:
|
||||
shutil.rmtree(outside_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_blocks_absolute_path_in_filename(tool, temp_env):
|
||||
# Use a temp file outside temp_dir as the absolute target so we don't
|
||||
# depend on /etc/passwd existing or being writable on the host.
|
||||
outside_dir = tempfile.mkdtemp()
|
||||
outside_file = os.path.join(outside_dir, "target.txt")
|
||||
try:
|
||||
result = tool._run(
|
||||
filename=outside_file,
|
||||
directory=temp_env["temp_dir"],
|
||||
content="should not be written",
|
||||
overwrite=True,
|
||||
)
|
||||
assert "Error" in result
|
||||
assert not os.path.exists(outside_file)
|
||||
finally:
|
||||
shutil.rmtree(outside_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_blocks_symlink_escape(tool, temp_env):
|
||||
# Symlink inside temp_dir pointing to a separate temp "outside" directory.
|
||||
outside_dir = tempfile.mkdtemp()
|
||||
outside_file = os.path.join(outside_dir, "target.txt")
|
||||
link = os.path.join(temp_env["temp_dir"], "escape")
|
||||
os.symlink(outside_dir, link)
|
||||
try:
|
||||
result = tool._run(
|
||||
filename="escape/target.txt",
|
||||
directory=temp_env["temp_dir"],
|
||||
content="should not be written",
|
||||
overwrite=True,
|
||||
)
|
||||
assert "Error" in result
|
||||
assert not os.path.exists(outside_file)
|
||||
finally:
|
||||
shutil.rmtree(outside_dir, ignore_errors=True)
|
||||
|
||||
@@ -53,7 +53,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.10.2rc2",
|
||||
"crewai-tools==1.11.0",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ import urllib.request
|
||||
import warnings
|
||||
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
from crewai.crew import Crew
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.flow.flow import Flow
|
||||
@@ -41,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.10.2rc2"
|
||||
__version__ = "1.11.0"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
@@ -102,6 +103,7 @@ __all__ = [
|
||||
"Knowledge",
|
||||
"LLMGuardrail",
|
||||
"Memory",
|
||||
"PlanningConfig",
|
||||
"Process",
|
||||
"Task",
|
||||
"TaskOutput",
|
||||
|
||||
@@ -13,6 +13,7 @@ from crewai.a2a.auth.client_schemes import (
|
||||
)
|
||||
from crewai.a2a.auth.server_schemes import (
|
||||
AuthenticatedUser,
|
||||
EnterpriseTokenAuth,
|
||||
OIDCAuth,
|
||||
ServerAuthScheme,
|
||||
SimpleTokenAuth,
|
||||
@@ -25,6 +26,7 @@ __all__ = [
|
||||
"AuthenticatedUser",
|
||||
"BearerTokenAuth",
|
||||
"ClientAuthScheme",
|
||||
"EnterpriseTokenAuth",
|
||||
"HTTPBasicAuth",
|
||||
"HTTPDigestAuth",
|
||||
"OAuth2AuthorizationCode",
|
||||
|
||||
@@ -4,6 +4,7 @@ These schemes validate incoming requests to A2A server endpoints.
|
||||
|
||||
Supported authentication methods:
|
||||
- Simple token validation with static bearer tokens
|
||||
- Enterprise token validation (via PlusAPI)
|
||||
- OpenID Connect with JWT validation using JWKS
|
||||
- OAuth2 with JWT validation or token introspection
|
||||
"""
|
||||
@@ -16,6 +17,7 @@ import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
from jwt import PyJWKClient
|
||||
from pydantic import (
|
||||
@@ -33,6 +35,7 @@ from typing_extensions import Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import OAuth2SecurityScheme
|
||||
from jwt.types import Options
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -183,6 +186,24 @@ class SimpleTokenAuth(ServerAuthScheme):
|
||||
)
|
||||
|
||||
|
||||
class EnterpriseTokenAuth(ServerAuthScheme):
|
||||
"""Enterprise token authentication.
|
||||
|
||||
Validates tokens via the PlusAPI enterprise verification endpoint.
|
||||
"""
|
||||
|
||||
async def authenticate(self, token: str) -> AuthenticatedUser:
|
||||
"""Authenticate using enterprise token verification.
|
||||
|
||||
Args:
|
||||
token: The bearer token to authenticate.
|
||||
|
||||
Raises:
|
||||
NotImplementedError
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class OIDCAuth(ServerAuthScheme):
|
||||
"""OpenID Connect authentication.
|
||||
|
||||
@@ -475,7 +496,7 @@ class OAuth2ServerAuth(ServerAuthScheme):
|
||||
try:
|
||||
signing_key = self._jwk_client.get_signing_key_from_jwt(token)
|
||||
|
||||
decode_options: dict[str, Any] = {
|
||||
decode_options: Options = {
|
||||
"require": self.required_claims,
|
||||
}
|
||||
|
||||
@@ -556,7 +577,6 @@ class OAuth2ServerAuth(ServerAuthScheme):
|
||||
|
||||
async def _authenticate_introspection(self, token: str) -> AuthenticatedUser:
|
||||
"""Authenticate using OAuth2 token introspection (RFC 7662)."""
|
||||
import httpx
|
||||
|
||||
if not self.introspection_url:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -633,6 +633,10 @@ class A2AServerConfig(BaseModel):
|
||||
default=False,
|
||||
description="Whether agent provides extended card to authenticated users",
|
||||
)
|
||||
extended_skills: list[AgentSkill] = Field(
|
||||
default_factory=list,
|
||||
description="Additional skills visible only to authenticated users in the extended card",
|
||||
)
|
||||
url: Url | None = Field(
|
||||
default=None,
|
||||
description="Preferred endpoint URL for the agent. Set at runtime if not provided.",
|
||||
|
||||
@@ -63,6 +63,9 @@ class A2AErrorCode(IntEnum):
|
||||
INVALID_AGENT_RESPONSE = -32006
|
||||
"""The agent produced an invalid response."""
|
||||
|
||||
AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED = -32007
|
||||
"""Authenticated extended card feature is not configured."""
|
||||
|
||||
# CrewAI Custom Extensions (-32768 to -32100)
|
||||
UNSUPPORTED_VERSION = -32009
|
||||
"""The requested A2A protocol version is not supported."""
|
||||
@@ -108,6 +111,7 @@ ERROR_MESSAGES: dict[int, str] = {
|
||||
A2AErrorCode.UNSUPPORTED_OPERATION: "This operation is not supported",
|
||||
A2AErrorCode.CONTENT_TYPE_NOT_SUPPORTED: "Incompatible content types",
|
||||
A2AErrorCode.INVALID_AGENT_RESPONSE: "Invalid agent response",
|
||||
A2AErrorCode.AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED: "Authenticated Extended Card is not configured",
|
||||
A2AErrorCode.UNSUPPORTED_VERSION: "Unsupported A2A version",
|
||||
A2AErrorCode.UNSUPPORTED_EXTENSION: "Client does not support required extensions",
|
||||
A2AErrorCode.AUTHENTICATION_REQUIRED: "Authentication required",
|
||||
@@ -284,6 +288,15 @@ class InvalidAgentResponseError(A2AError):
|
||||
code: int = field(default=A2AErrorCode.INVALID_AGENT_RESPONSE, init=False)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthenticatedExtendedCardNotConfiguredError(A2AError):
|
||||
"""Authenticated extended card is not configured."""
|
||||
|
||||
code: int = field(
|
||||
default=A2AErrorCode.AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED, init=False
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnsupportedVersionError(A2AError):
|
||||
"""The requested A2A version is not supported."""
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"""A2UI (Agent to UI) declarative UI protocol support for CrewAI."""
|
||||
|
||||
from crewai.a2a.extensions.a2ui.catalog import (
|
||||
AudioPlayer,
|
||||
Button,
|
||||
Card,
|
||||
CheckBox,
|
||||
Column,
|
||||
DateTimeInput,
|
||||
Divider,
|
||||
Icon,
|
||||
Image,
|
||||
List,
|
||||
Modal,
|
||||
MultipleChoice,
|
||||
Row,
|
||||
Slider,
|
||||
Tabs,
|
||||
Text,
|
||||
TextField,
|
||||
Video,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.client_extension import A2UIClientExtension
|
||||
from crewai.a2a.extensions.a2ui.models import (
|
||||
A2UIEvent,
|
||||
A2UIMessage,
|
||||
A2UIResponse,
|
||||
BeginRendering,
|
||||
DataModelUpdate,
|
||||
DeleteSurface,
|
||||
SurfaceUpdate,
|
||||
UserAction,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.server_extension import A2UIServerExtension
|
||||
from crewai.a2a.extensions.a2ui.validator import validate_a2ui_message
|
||||
|
||||
|
||||
__all__ = [
|
||||
"A2UIClientExtension",
|
||||
"A2UIEvent",
|
||||
"A2UIMessage",
|
||||
"A2UIResponse",
|
||||
"A2UIServerExtension",
|
||||
"AudioPlayer",
|
||||
"BeginRendering",
|
||||
"Button",
|
||||
"Card",
|
||||
"CheckBox",
|
||||
"Column",
|
||||
"DataModelUpdate",
|
||||
"DateTimeInput",
|
||||
"DeleteSurface",
|
||||
"Divider",
|
||||
"Icon",
|
||||
"Image",
|
||||
"List",
|
||||
"Modal",
|
||||
"MultipleChoice",
|
||||
"Row",
|
||||
"Slider",
|
||||
"SurfaceUpdate",
|
||||
"Tabs",
|
||||
"Text",
|
||||
"TextField",
|
||||
"UserAction",
|
||||
"Video",
|
||||
"validate_a2ui_message",
|
||||
]
|
||||
@@ -1,467 +0,0 @@
|
||||
"""Typed helpers for A2UI standard catalog components.
|
||||
|
||||
These models provide optional type safety for standard catalog components.
|
||||
Agents can also use raw dicts validated against the JSON schema.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class StringBinding(BaseModel):
|
||||
"""A string value: literal or data-model path."""
|
||||
|
||||
literal_string: str | None = Field(
|
||||
default=None, alias="literalString", description="Literal string value."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class NumberBinding(BaseModel):
|
||||
"""A numeric value: literal or data-model path."""
|
||||
|
||||
literal_number: float | None = Field(
|
||||
default=None, alias="literalNumber", description="Literal numeric value."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class BooleanBinding(BaseModel):
|
||||
"""A boolean value: literal or data-model path."""
|
||||
|
||||
literal_boolean: bool | None = Field(
|
||||
default=None, alias="literalBoolean", description="Literal boolean value."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ArrayBinding(BaseModel):
|
||||
"""An array value: literal or data-model path."""
|
||||
|
||||
literal_array: list[str] | None = Field(
|
||||
default=None, alias="literalArray", description="Literal array of strings."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ChildrenDef(BaseModel):
|
||||
"""Children definition for layout components."""
|
||||
|
||||
explicit_list: list[str] | None = Field(
|
||||
default=None,
|
||||
alias="explicitList",
|
||||
description="Explicit list of child component IDs.",
|
||||
)
|
||||
template: ChildTemplate | None = Field(
|
||||
default=None, description="Template for generating dynamic children."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ChildTemplate(BaseModel):
|
||||
"""Template for generating dynamic children from a data model list."""
|
||||
|
||||
component_id: str = Field(
|
||||
alias="componentId", description="ID of the component to repeat."
|
||||
)
|
||||
data_binding: str = Field(
|
||||
alias="dataBinding", description="Data-model path to bind the template to."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ActionContextEntry(BaseModel):
|
||||
"""A key-value pair in an action context payload."""
|
||||
|
||||
key: str = Field(description="Context entry key.")
|
||||
value: ActionBoundValue = Field(description="Context entry value.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ActionBoundValue(BaseModel):
|
||||
"""A value in an action context: literal or data-model path."""
|
||||
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
literal_string: str | None = Field(
|
||||
default=None, alias="literalString", description="Literal string value."
|
||||
)
|
||||
literal_number: float | None = Field(
|
||||
default=None, alias="literalNumber", description="Literal numeric value."
|
||||
)
|
||||
literal_boolean: bool | None = Field(
|
||||
default=None, alias="literalBoolean", description="Literal boolean value."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Action(BaseModel):
|
||||
"""Client-side action dispatched by interactive components."""
|
||||
|
||||
name: str = Field(description="Action name dispatched on interaction.")
|
||||
context: list[ActionContextEntry] | None = Field(
|
||||
default=None, description="Key-value pairs sent with the action."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TabItem(BaseModel):
|
||||
"""A single tab definition."""
|
||||
|
||||
title: StringBinding = Field(description="Tab title text.")
|
||||
child: str = Field(description="Component ID rendered as the tab content.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class MultipleChoiceOption(BaseModel):
|
||||
"""A single option in a MultipleChoice component."""
|
||||
|
||||
label: StringBinding = Field(description="Display label for the option.")
|
||||
value: str = Field(description="Value submitted when the option is selected.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Text(BaseModel):
|
||||
"""Displays text content."""
|
||||
|
||||
text: StringBinding = Field(description="Text content to display.")
|
||||
usage_hint: Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"] | None = Field(
|
||||
default=None, alias="usageHint", description="Semantic hint for text styling."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
"""Displays an image."""
|
||||
|
||||
url: StringBinding = Field(description="Image source URL.")
|
||||
fit: Literal["contain", "cover", "fill", "none", "scale-down"] | None = Field(
|
||||
default=None, description="Object-fit behavior for the image."
|
||||
)
|
||||
usage_hint: (
|
||||
Literal[
|
||||
"icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"
|
||||
]
|
||||
| None
|
||||
) = Field(
|
||||
default=None, alias="usageHint", description="Semantic hint for image sizing."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
IconName = Literal[
|
||||
"accountCircle",
|
||||
"add",
|
||||
"arrowBack",
|
||||
"arrowForward",
|
||||
"attachFile",
|
||||
"calendarToday",
|
||||
"call",
|
||||
"camera",
|
||||
"check",
|
||||
"close",
|
||||
"delete",
|
||||
"download",
|
||||
"edit",
|
||||
"event",
|
||||
"error",
|
||||
"favorite",
|
||||
"favoriteOff",
|
||||
"folder",
|
||||
"help",
|
||||
"home",
|
||||
"info",
|
||||
"locationOn",
|
||||
"lock",
|
||||
"lockOpen",
|
||||
"mail",
|
||||
"menu",
|
||||
"moreVert",
|
||||
"moreHoriz",
|
||||
"notificationsOff",
|
||||
"notifications",
|
||||
"payment",
|
||||
"person",
|
||||
"phone",
|
||||
"photo",
|
||||
"print",
|
||||
"refresh",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share",
|
||||
"shoppingCart",
|
||||
"star",
|
||||
"starHalf",
|
||||
"starOff",
|
||||
"upload",
|
||||
"visibility",
|
||||
"visibilityOff",
|
||||
"warning",
|
||||
]
|
||||
|
||||
|
||||
class IconBinding(BaseModel):
|
||||
"""Icon name: literal enum or data-model path."""
|
||||
|
||||
literal_string: IconName | None = Field(
|
||||
default=None, alias="literalString", description="Literal icon name."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Icon(BaseModel):
|
||||
"""Displays a named icon."""
|
||||
|
||||
name: IconBinding = Field(description="Icon name binding.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Video(BaseModel):
|
||||
"""Displays a video player."""
|
||||
|
||||
url: StringBinding = Field(description="Video source URL.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class AudioPlayer(BaseModel):
|
||||
"""Displays an audio player."""
|
||||
|
||||
url: StringBinding = Field(description="Audio source URL.")
|
||||
description: StringBinding | None = Field(
|
||||
default=None, description="Accessible description of the audio content."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Row(BaseModel):
|
||||
"""Horizontal layout container."""
|
||||
|
||||
children: ChildrenDef = Field(description="Child components in this row.")
|
||||
distribution: (
|
||||
Literal["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start"]
|
||||
| None
|
||||
) = Field(
|
||||
default=None, description="How children are distributed along the main axis."
|
||||
)
|
||||
alignment: Literal["start", "center", "end", "stretch"] | None = Field(
|
||||
default=None, description="How children are aligned on the cross axis."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Column(BaseModel):
|
||||
"""Vertical layout container."""
|
||||
|
||||
children: ChildrenDef = Field(description="Child components in this column.")
|
||||
distribution: (
|
||||
Literal["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly"]
|
||||
| None
|
||||
) = Field(
|
||||
default=None, description="How children are distributed along the main axis."
|
||||
)
|
||||
alignment: Literal["center", "end", "start", "stretch"] | None = Field(
|
||||
default=None, description="How children are aligned on the cross axis."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class List(BaseModel):
|
||||
"""Scrollable list container."""
|
||||
|
||||
children: ChildrenDef = Field(description="Child components in this list.")
|
||||
direction: Literal["vertical", "horizontal"] | None = Field(
|
||||
default=None, description="Scroll direction of the list."
|
||||
)
|
||||
alignment: Literal["start", "center", "end", "stretch"] | None = Field(
|
||||
default=None, description="How children are aligned on the cross axis."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Card(BaseModel):
|
||||
"""Card container wrapping a single child."""
|
||||
|
||||
child: str = Field(description="Component ID of the card content.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Tabs(BaseModel):
|
||||
"""Tabbed navigation container."""
|
||||
|
||||
tab_items: list[TabItem] = Field(
|
||||
alias="tabItems", description="List of tab definitions."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Divider(BaseModel):
|
||||
"""A visual divider line."""
|
||||
|
||||
axis: Literal["horizontal", "vertical"] | None = Field(
|
||||
default=None, description="Orientation of the divider."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Modal(BaseModel):
|
||||
"""A modal dialog with an entry point trigger and content."""
|
||||
|
||||
entry_point_child: str = Field(
|
||||
alias="entryPointChild", description="Component ID that triggers the modal."
|
||||
)
|
||||
content_child: str = Field(
|
||||
alias="contentChild", description="Component ID rendered inside the modal."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Button(BaseModel):
|
||||
"""An interactive button with an action."""
|
||||
|
||||
child: str = Field(description="Component ID of the button label.")
|
||||
primary: bool | None = Field(
|
||||
default=None, description="Whether the button uses primary styling."
|
||||
)
|
||||
action: Action = Field(description="Action dispatched when the button is clicked.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class CheckBox(BaseModel):
|
||||
"""A checkbox input."""
|
||||
|
||||
label: StringBinding = Field(description="Label text for the checkbox.")
|
||||
value: BooleanBinding = Field(
|
||||
description="Boolean value binding for the checkbox state."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TextField(BaseModel):
|
||||
"""A text input field."""
|
||||
|
||||
label: StringBinding = Field(description="Label text for the input.")
|
||||
text: StringBinding | None = Field(
|
||||
default=None, description="Current text value binding."
|
||||
)
|
||||
text_field_type: (
|
||||
Literal["date", "longText", "number", "shortText", "obscured"] | None
|
||||
) = Field(default=None, alias="textFieldType", description="Input type variant.")
|
||||
validation_regexp: str | None = Field(
|
||||
default=None,
|
||||
alias="validationRegexp",
|
||||
description="Regex pattern for client-side validation.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DateTimeInput(BaseModel):
|
||||
"""A date and/or time picker."""
|
||||
|
||||
value: StringBinding = Field(description="ISO date/time string value binding.")
|
||||
enable_date: bool | None = Field(
|
||||
default=None,
|
||||
alias="enableDate",
|
||||
description="Whether the date picker is enabled.",
|
||||
)
|
||||
enable_time: bool | None = Field(
|
||||
default=None,
|
||||
alias="enableTime",
|
||||
description="Whether the time picker is enabled.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class MultipleChoice(BaseModel):
|
||||
"""A multiple-choice selection component."""
|
||||
|
||||
selections: ArrayBinding = Field(description="Array binding for selected values.")
|
||||
options: list[MultipleChoiceOption] = Field(description="Available choices.")
|
||||
max_allowed_selections: int | None = Field(
|
||||
default=None,
|
||||
alias="maxAllowedSelections",
|
||||
description="Maximum number of selections allowed.",
|
||||
)
|
||||
variant: Literal["checkbox", "chips"] | None = Field(
|
||||
default=None, description="Visual variant for the selection UI."
|
||||
)
|
||||
filterable: bool | None = Field(
|
||||
default=None, description="Whether options can be filtered by typing."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Slider(BaseModel):
|
||||
"""A numeric slider input."""
|
||||
|
||||
value: NumberBinding = Field(
|
||||
description="Numeric value binding for the slider position."
|
||||
)
|
||||
min_value: float | None = Field(
|
||||
default=None, alias="minValue", description="Minimum slider value."
|
||||
)
|
||||
max_value: float | None = Field(
|
||||
default=None, alias="maxValue", description="Maximum slider value."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
STANDARD_CATALOG_COMPONENTS: frozenset[str] = frozenset(
|
||||
{
|
||||
"Text",
|
||||
"Image",
|
||||
"Icon",
|
||||
"Video",
|
||||
"AudioPlayer",
|
||||
"Row",
|
||||
"Column",
|
||||
"List",
|
||||
"Card",
|
||||
"Tabs",
|
||||
"Divider",
|
||||
"Modal",
|
||||
"Button",
|
||||
"CheckBox",
|
||||
"TextField",
|
||||
"DateTimeInput",
|
||||
"MultipleChoice",
|
||||
"Slider",
|
||||
}
|
||||
)
|
||||
@@ -1,285 +0,0 @@
|
||||
"""A2UI client extension for the A2A protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from crewai.a2a.extensions.a2ui.models import extract_a2ui_json_objects
|
||||
from crewai.a2a.extensions.a2ui.prompt import build_a2ui_system_prompt
|
||||
from crewai.a2a.extensions.a2ui.server_extension import A2UI_MIME_TYPE
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
A2UIValidationError,
|
||||
validate_a2ui_message,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Message
|
||||
|
||||
from crewai.agent.core import Agent
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StylesDict(TypedDict, total=False):
|
||||
"""Serialized surface styling."""
|
||||
|
||||
font: str
|
||||
primaryColor: str
|
||||
|
||||
|
||||
class ComponentEntryDict(TypedDict, total=False):
|
||||
"""Serialized component entry in a surface update."""
|
||||
|
||||
id: str
|
||||
weight: float
|
||||
component: dict[str, Any]
|
||||
|
||||
|
||||
class BeginRenderingDict(TypedDict, total=False):
|
||||
"""Serialized beginRendering payload."""
|
||||
|
||||
surfaceId: str
|
||||
root: str
|
||||
catalogId: str
|
||||
styles: StylesDict
|
||||
|
||||
|
||||
class SurfaceUpdateDict(TypedDict, total=False):
|
||||
"""Serialized surfaceUpdate payload."""
|
||||
|
||||
surfaceId: str
|
||||
components: list[ComponentEntryDict]
|
||||
|
||||
|
||||
class DataEntryDict(TypedDict, total=False):
|
||||
"""Serialized data model entry."""
|
||||
|
||||
key: str
|
||||
valueString: str
|
||||
valueNumber: float
|
||||
valueBoolean: bool
|
||||
valueMap: list[DataEntryDict]
|
||||
|
||||
|
||||
class DataModelUpdateDict(TypedDict, total=False):
|
||||
"""Serialized dataModelUpdate payload."""
|
||||
|
||||
surfaceId: str
|
||||
path: str
|
||||
contents: list[DataEntryDict]
|
||||
|
||||
|
||||
class DeleteSurfaceDict(TypedDict):
|
||||
"""Serialized deleteSurface payload."""
|
||||
|
||||
surfaceId: str
|
||||
|
||||
|
||||
class A2UIMessageDict(TypedDict, total=False):
|
||||
"""Serialized A2UI server-to-client message with exactly one key set."""
|
||||
|
||||
beginRendering: BeginRenderingDict
|
||||
surfaceUpdate: SurfaceUpdateDict
|
||||
dataModelUpdate: DataModelUpdateDict
|
||||
deleteSurface: DeleteSurfaceDict
|
||||
|
||||
|
||||
@dataclass
|
||||
class A2UIConversationState:
|
||||
"""Tracks active A2UI surfaces and data models across a conversation."""
|
||||
|
||||
active_surfaces: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
||||
data_models: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
|
||||
last_a2ui_messages: list[A2UIMessageDict] = Field(default_factory=list)
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
"""Return True when at least one surface is active."""
|
||||
return bool(self.active_surfaces)
|
||||
|
||||
|
||||
class A2UIClientExtension:
|
||||
"""A2A client extension that adds A2UI support to agents.
|
||||
|
||||
Implements the ``A2AExtension`` protocol to inject A2UI prompt
|
||||
instructions, track UI state across conversations, and validate
|
||||
A2UI messages in responses.
|
||||
|
||||
Example::
|
||||
|
||||
A2AClientConfig(
|
||||
endpoint="...",
|
||||
extensions=["https://a2ui.org/a2a-extension/a2ui/v0.8"],
|
||||
client_extensions=[A2UIClientExtension()],
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
catalog_id: str | None = None,
|
||||
allowed_components: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the A2UI client extension.
|
||||
|
||||
Args:
|
||||
catalog_id: Catalog identifier to use for prompt generation.
|
||||
allowed_components: Subset of component names to expose to the agent.
|
||||
"""
|
||||
self._catalog_id = catalog_id
|
||||
self._allowed_components = allowed_components
|
||||
|
||||
def inject_tools(self, agent: Agent) -> None:
|
||||
"""No-op — A2UI uses prompt augmentation rather than tool injection."""
|
||||
|
||||
def extract_state_from_history(
|
||||
self, conversation_history: Sequence[Message]
|
||||
) -> A2UIConversationState | None:
|
||||
"""Scan conversation history for A2UI DataParts and track surface state.
|
||||
|
||||
When ``catalog_id`` is set, only surfaces matching that catalog are tracked.
|
||||
"""
|
||||
state = A2UIConversationState()
|
||||
|
||||
for message in conversation_history:
|
||||
for part in message.parts:
|
||||
root = part.root
|
||||
if root.kind != "data":
|
||||
continue
|
||||
metadata = root.metadata or {}
|
||||
mime_type = metadata.get("mimeType", "")
|
||||
if mime_type != A2UI_MIME_TYPE:
|
||||
continue
|
||||
|
||||
data = root.data
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
surface_id = _get_surface_id(data)
|
||||
if not surface_id:
|
||||
continue
|
||||
|
||||
if self._catalog_id and "beginRendering" in data:
|
||||
catalog_id = data["beginRendering"].get("catalogId")
|
||||
if catalog_id and catalog_id != self._catalog_id:
|
||||
continue
|
||||
|
||||
if "deleteSurface" in data:
|
||||
state.active_surfaces.pop(surface_id, None)
|
||||
state.data_models.pop(surface_id, None)
|
||||
elif "beginRendering" in data:
|
||||
state.active_surfaces[surface_id] = data["beginRendering"]
|
||||
elif "surfaceUpdate" in data:
|
||||
state.active_surfaces[surface_id] = data["surfaceUpdate"]
|
||||
elif "dataModelUpdate" in data:
|
||||
contents = data["dataModelUpdate"].get("contents", [])
|
||||
state.data_models.setdefault(surface_id, []).extend(contents)
|
||||
|
||||
if not state.active_surfaces and not state.data_models:
|
||||
return None
|
||||
return state
|
||||
|
||||
def augment_prompt(
|
||||
self,
|
||||
base_prompt: str,
|
||||
_conversation_state: A2UIConversationState | None,
|
||||
) -> str:
|
||||
"""Append A2UI system prompt instructions to the base prompt."""
|
||||
a2ui_prompt = build_a2ui_system_prompt(
|
||||
catalog_id=self._catalog_id,
|
||||
allowed_components=self._allowed_components,
|
||||
)
|
||||
return f"{base_prompt}\n\n{a2ui_prompt}"
|
||||
|
||||
def process_response(
|
||||
self,
|
||||
agent_response: Any,
|
||||
conversation_state: A2UIConversationState | None,
|
||||
) -> Any:
|
||||
"""Extract and validate A2UI JSON from agent output.
|
||||
|
||||
When ``allowed_components`` is set, components not in the allowlist are
|
||||
logged and stripped from surface updates. Stores extracted A2UI messages
|
||||
on the conversation state and returns the original response unchanged.
|
||||
"""
|
||||
text = (
|
||||
agent_response if isinstance(agent_response, str) else str(agent_response)
|
||||
)
|
||||
a2ui_messages = _extract_and_validate(text)
|
||||
|
||||
if self._allowed_components:
|
||||
allowed = set(self._allowed_components)
|
||||
a2ui_messages = [_filter_components(msg, allowed) for msg in a2ui_messages]
|
||||
|
||||
if a2ui_messages and conversation_state is not None:
|
||||
conversation_state.last_a2ui_messages = a2ui_messages
|
||||
|
||||
return agent_response
|
||||
|
||||
|
||||
def _get_surface_id(data: dict[str, Any]) -> str | None:
|
||||
"""Extract surfaceId from any A2UI message type."""
|
||||
for key in ("beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface"):
|
||||
inner = data.get(key)
|
||||
if isinstance(inner, dict):
|
||||
sid = inner.get("surfaceId")
|
||||
if isinstance(sid, str):
|
||||
return sid
|
||||
return None
|
||||
|
||||
|
||||
def _filter_components(msg: A2UIMessageDict, allowed: set[str]) -> A2UIMessageDict:
|
||||
"""Strip components whose type is not in *allowed* from a surfaceUpdate."""
|
||||
surface_update = msg.get("surfaceUpdate")
|
||||
if not isinstance(surface_update, dict):
|
||||
return msg
|
||||
|
||||
components = surface_update.get("components")
|
||||
if not isinstance(components, list):
|
||||
return msg
|
||||
|
||||
filtered = []
|
||||
for entry in components:
|
||||
component = entry.get("component", {})
|
||||
component_types = set(component.keys())
|
||||
disallowed = component_types - allowed
|
||||
if disallowed:
|
||||
logger.debug(
|
||||
"Stripping disallowed component type(s) %s from surface update",
|
||||
disallowed,
|
||||
)
|
||||
continue
|
||||
filtered.append(entry)
|
||||
|
||||
if len(filtered) == len(components):
|
||||
return msg
|
||||
|
||||
return {**msg, "surfaceUpdate": {**surface_update, "components": filtered}}
|
||||
|
||||
|
||||
def _extract_and_validate(text: str) -> list[A2UIMessageDict]:
|
||||
"""Extract A2UI JSON objects from text and validate them."""
|
||||
return [
|
||||
dumped
|
||||
for candidate in extract_a2ui_json_objects(text)
|
||||
if (dumped := _try_validate(candidate)) is not None
|
||||
]
|
||||
|
||||
|
||||
def _try_validate(candidate: dict[str, Any]) -> A2UIMessageDict | None:
|
||||
"""Validate a single A2UI candidate, returning None on failure."""
|
||||
try:
|
||||
msg = validate_a2ui_message(candidate)
|
||||
except A2UIValidationError:
|
||||
logger.debug(
|
||||
"Skipping invalid A2UI candidate in agent output",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
return cast(A2UIMessageDict, msg.model_dump(by_alias=True, exclude_none=True))
|
||||
@@ -1,281 +0,0 @@
|
||||
"""Pydantic models for A2UI server-to-client messages and client-to-server events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class BoundValue(BaseModel):
|
||||
"""A value that can be a literal or a data-model path reference."""
|
||||
|
||||
literal_string: str | None = Field(
|
||||
default=None, alias="literalString", description="Literal string value."
|
||||
)
|
||||
literal_number: float | None = Field(
|
||||
default=None, alias="literalNumber", description="Literal numeric value."
|
||||
)
|
||||
literal_boolean: bool | None = Field(
|
||||
default=None, alias="literalBoolean", description="Literal boolean value."
|
||||
)
|
||||
literal_array: list[str] | None = Field(
|
||||
default=None, alias="literalArray", description="Literal array of strings."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class MapEntry(BaseModel):
|
||||
"""A single entry in a valueMap adjacency list, supporting recursive nesting."""
|
||||
|
||||
key: str = Field(description="Entry key.")
|
||||
value_string: str | None = Field(
|
||||
default=None, alias="valueString", description="String value."
|
||||
)
|
||||
value_number: float | None = Field(
|
||||
default=None, alias="valueNumber", description="Numeric value."
|
||||
)
|
||||
value_boolean: bool | None = Field(
|
||||
default=None, alias="valueBoolean", description="Boolean value."
|
||||
)
|
||||
value_map: list[MapEntry] | None = Field(
|
||||
default=None, alias="valueMap", description="Nested map entries."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DataEntry(BaseModel):
|
||||
"""A data model entry with a key and exactly one typed value."""
|
||||
|
||||
key: str = Field(description="Entry key.")
|
||||
value_string: str | None = Field(
|
||||
default=None, alias="valueString", description="String value."
|
||||
)
|
||||
value_number: float | None = Field(
|
||||
default=None, alias="valueNumber", description="Numeric value."
|
||||
)
|
||||
value_boolean: bool | None = Field(
|
||||
default=None, alias="valueBoolean", description="Boolean value."
|
||||
)
|
||||
value_map: list[MapEntry] | None = Field(
|
||||
default=None, alias="valueMap", description="Nested map entries."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
_HEX_COLOR_PATTERN: re.Pattern[str] = re.compile(r"^#[0-9a-fA-F]{6}$")
|
||||
|
||||
|
||||
class Styles(BaseModel):
|
||||
"""Surface styling information."""
|
||||
|
||||
font: str | None = Field(default=None, description="Font family name.")
|
||||
primary_color: str | None = Field(
|
||||
default=None,
|
||||
alias="primaryColor",
|
||||
pattern=_HEX_COLOR_PATTERN.pattern,
|
||||
description="Primary color as a hex string.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
||||
|
||||
|
||||
class ComponentEntry(BaseModel):
|
||||
"""A single component in a UI widget tree.
|
||||
|
||||
The ``component`` dict must contain exactly one key — the component type
|
||||
name (e.g. ``"Text"``, ``"Button"``) — whose value holds the component
|
||||
properties. Component internals are left as ``dict[str, Any]`` because
|
||||
they are catalog-dependent; use the typed helpers in ``catalog.py`` for
|
||||
the standard catalog.
|
||||
"""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
weight: float | None = Field(
|
||||
default=None, description="Flex weight for layout distribution."
|
||||
)
|
||||
component: dict[str, Any] = Field(
|
||||
description="Component type name mapped to its properties."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class BeginRendering(BaseModel):
|
||||
"""Signals the client to begin rendering a surface."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Unique surface identifier.")
|
||||
root: str = Field(description="Component ID of the root element.")
|
||||
catalog_id: str | None = Field(
|
||||
default=None,
|
||||
alias="catalogId",
|
||||
description="Catalog identifier for the surface.",
|
||||
)
|
||||
styles: Styles | None = Field(
|
||||
default=None, description="Surface styling overrides."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class SurfaceUpdate(BaseModel):
|
||||
"""Updates a surface with a new set of components."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
|
||||
components: list[ComponentEntry] = Field(
|
||||
min_length=1, description="Components to render on the surface."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DataModelUpdate(BaseModel):
|
||||
"""Updates the data model for a surface."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
|
||||
path: str | None = Field(
|
||||
default=None, description="Data-model path prefix for the update."
|
||||
)
|
||||
contents: list[DataEntry] = Field(
|
||||
description="Data entries to merge into the model."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DeleteSurface(BaseModel):
|
||||
"""Signals the client to delete a surface."""
|
||||
|
||||
surface_id: str = Field(
|
||||
alias="surfaceId", description="Surface identifier to delete."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class A2UIMessage(BaseModel):
|
||||
"""Union wrapper for the four server-to-client A2UI message types.
|
||||
|
||||
Exactly one of the fields must be set.
|
||||
"""
|
||||
|
||||
begin_rendering: BeginRendering | None = Field(
|
||||
default=None,
|
||||
alias="beginRendering",
|
||||
description="Begin rendering a new surface.",
|
||||
)
|
||||
surface_update: SurfaceUpdate | None = Field(
|
||||
default=None,
|
||||
alias="surfaceUpdate",
|
||||
description="Update components on a surface.",
|
||||
)
|
||||
data_model_update: DataModelUpdate | None = Field(
|
||||
default=None,
|
||||
alias="dataModelUpdate",
|
||||
description="Update the surface data model.",
|
||||
)
|
||||
delete_surface: DeleteSurface | None = Field(
|
||||
default=None, alias="deleteSurface", description="Delete an existing surface."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> A2UIMessage:
|
||||
"""Enforce the spec's exactly-one-of constraint."""
|
||||
fields = [
|
||||
self.begin_rendering,
|
||||
self.surface_update,
|
||||
self.data_model_update,
|
||||
self.delete_surface,
|
||||
]
|
||||
count = sum(f is not None for f in fields)
|
||||
if count != 1:
|
||||
raise ValueError(f"Exactly one A2UI message type must be set, got {count}")
|
||||
return self
|
||||
|
||||
|
||||
class UserAction(BaseModel):
|
||||
"""Reports a user-initiated action from a component."""
|
||||
|
||||
name: str = Field(description="Action name.")
|
||||
surface_id: str = Field(alias="surfaceId", description="Source surface identifier.")
|
||||
source_component_id: str = Field(
|
||||
alias="sourceComponentId", description="Component that triggered the action."
|
||||
)
|
||||
timestamp: str = Field(description="ISO 8601 timestamp of the action.")
|
||||
context: dict[str, Any] = Field(description="Action context payload.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class ClientError(BaseModel):
|
||||
"""Reports a client-side error."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class A2UIEvent(BaseModel):
|
||||
"""Union wrapper for client-to-server events."""
|
||||
|
||||
user_action: UserAction | None = Field(
|
||||
default=None, alias="userAction", description="User-initiated action event."
|
||||
)
|
||||
error: ClientError | None = Field(
|
||||
default=None, description="Client-side error report."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> A2UIEvent:
|
||||
"""Enforce the spec's exactly-one-of constraint."""
|
||||
fields = [self.user_action, self.error]
|
||||
count = sum(f is not None for f in fields)
|
||||
if count != 1:
|
||||
raise ValueError(f"Exactly one A2UI event type must be set, got {count}")
|
||||
return self
|
||||
|
||||
|
||||
class A2UIResponse(BaseModel):
|
||||
"""Typed wrapper for responses containing A2UI messages."""
|
||||
|
||||
text: str = Field(description="Raw text content of the response.")
|
||||
a2ui_parts: list[dict[str, Any]] = Field(
|
||||
default_factory=list, description="A2UI DataParts extracted from the response."
|
||||
)
|
||||
a2ui_messages: list[dict[str, Any]] = Field(
|
||||
default_factory=list, description="Validated A2UI message dicts."
|
||||
)
|
||||
|
||||
|
||||
_A2UI_KEYS = {"beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface"}
|
||||
|
||||
|
||||
def extract_a2ui_json_objects(text: str) -> list[dict[str, Any]]:
|
||||
"""Extract JSON objects containing A2UI keys from text.
|
||||
|
||||
Uses ``json.JSONDecoder.raw_decode`` for robust parsing that correctly
|
||||
handles braces inside string literals.
|
||||
"""
|
||||
decoder = json.JSONDecoder()
|
||||
results: list[dict[str, Any]] = []
|
||||
idx = 0
|
||||
while idx < len(text):
|
||||
idx = text.find("{", idx)
|
||||
if idx == -1:
|
||||
break
|
||||
try:
|
||||
obj, end_idx = decoder.raw_decode(text, idx)
|
||||
if isinstance(obj, dict) and _A2UI_KEYS & obj.keys():
|
||||
results.append(obj)
|
||||
idx = end_idx
|
||||
except json.JSONDecodeError:
|
||||
idx += 1
|
||||
return results
|
||||
@@ -1,76 +0,0 @@
|
||||
"""System prompt generation for A2UI-capable agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from crewai.a2a.extensions.a2ui.catalog import STANDARD_CATALOG_COMPONENTS
|
||||
from crewai.a2a.extensions.a2ui.schema import load_schema
|
||||
from crewai.a2a.extensions.a2ui.server_extension import A2UI_EXTENSION_URI
|
||||
|
||||
|
||||
def build_a2ui_system_prompt(
|
||||
catalog_id: str | None = None,
|
||||
allowed_components: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Build a system prompt fragment instructing the LLM to produce A2UI output.
|
||||
|
||||
The prompt describes the A2UI message format, available components, and
|
||||
data binding rules. It includes the resolved schema so the LLM can
|
||||
generate structured output.
|
||||
|
||||
Args:
|
||||
catalog_id: Catalog identifier to reference. Defaults to the
|
||||
standard catalog version derived from ``A2UI_EXTENSION_URI``.
|
||||
allowed_components: Subset of component names to expose. When
|
||||
``None``, all standard catalog components are available.
|
||||
|
||||
Returns:
|
||||
A system prompt string to append to the agent's instructions.
|
||||
"""
|
||||
components = sorted(
|
||||
allowed_components
|
||||
if allowed_components is not None
|
||||
else STANDARD_CATALOG_COMPONENTS
|
||||
)
|
||||
|
||||
catalog_label = catalog_id or f"standard ({A2UI_EXTENSION_URI.rsplit('/', 1)[-1]})"
|
||||
|
||||
resolved_schema = load_schema("server_to_client_with_standard_catalog")
|
||||
schema_json = json.dumps(resolved_schema, indent=2)
|
||||
|
||||
return f"""\
|
||||
<A2UI_INSTRUCTIONS>
|
||||
You can generate rich, declarative UI by emitting A2UI JSON messages.
|
||||
|
||||
CATALOG: {catalog_label}
|
||||
AVAILABLE COMPONENTS: {", ".join(components)}
|
||||
|
||||
MESSAGE TYPES (emit exactly ONE per message):
|
||||
- beginRendering: Initialize a new surface with a root component and optional styles.
|
||||
- surfaceUpdate: Send/update components for a surface. Each component has a unique id \
|
||||
and a "component" wrapper containing exactly one component-type key.
|
||||
- dataModelUpdate: Update the data model for a surface. Data entries have a key and \
|
||||
one typed value (valueString, valueNumber, valueBoolean, valueMap).
|
||||
- deleteSurface: Remove a surface.
|
||||
|
||||
DATA BINDING:
|
||||
- Use {{"literalString": "..."}} for inline string values.
|
||||
- Use {{"literalNumber": ...}} for inline numeric values.
|
||||
- Use {{"literalBoolean": ...}} for inline boolean values.
|
||||
- Use {{"literalArray": ["...", "..."]}} for inline array values.
|
||||
- Use {{"path": "/data/model/path"}} to bind to data model values.
|
||||
|
||||
ACTIONS:
|
||||
- Interactive components (Button, etc.) have an "action" with a "name" and optional \
|
||||
"context" array of key/value pairs.
|
||||
- Values in action context can use data binding (path or literal).
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Emit each A2UI message as a valid JSON object. When generating UI, produce a \
|
||||
beginRendering message first, then surfaceUpdate messages with components, and \
|
||||
optionally dataModelUpdate messages to populate data-bound values.
|
||||
|
||||
SCHEMA:
|
||||
{schema_json}
|
||||
</A2UI_INSTRUCTIONS>"""
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Schema loading utilities for vendored A2UI JSON schemas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
_SCHEMA_DIR = Path(__file__).parent / "v0_8"
|
||||
|
||||
_SCHEMA_CACHE: dict[str, dict[str, Any]] = {}
|
||||
|
||||
SCHEMA_NAMES: frozenset[str] = frozenset(
|
||||
{
|
||||
"server_to_client",
|
||||
"client_to_server",
|
||||
"standard_catalog_definition",
|
||||
"server_to_client_with_standard_catalog",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def load_schema(name: str) -> dict[str, Any]:
|
||||
"""Load a vendored A2UI JSON schema by name.
|
||||
|
||||
Args:
|
||||
name: Schema name without extension (e.g. ``"server_to_client"``).
|
||||
|
||||
Returns:
|
||||
Parsed JSON schema dict.
|
||||
|
||||
Raises:
|
||||
ValueError: If the schema name is not recognized.
|
||||
FileNotFoundError: If the schema file is missing from the package.
|
||||
"""
|
||||
if name not in SCHEMA_NAMES:
|
||||
raise ValueError(f"Unknown schema {name!r}. Available: {sorted(SCHEMA_NAMES)}")
|
||||
|
||||
if name in _SCHEMA_CACHE:
|
||||
return _SCHEMA_CACHE[name]
|
||||
|
||||
path = _SCHEMA_DIR / f"{name}.json"
|
||||
with path.open() as f:
|
||||
schema: dict[str, Any] = json.load(f)
|
||||
|
||||
_SCHEMA_CACHE[name] = schema
|
||||
return schema
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"title": "A2UI (Agent to UI) Client-to-Server Event Schema",
|
||||
"description": "Describes a JSON payload for a client-to-server event message.",
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"properties": {
|
||||
"userAction": {
|
||||
"type": "object",
|
||||
"description": "Reports a user-initiated action from a component.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the action, taken from the component's action.name property."
|
||||
},
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The id of the surface where the event originated."
|
||||
},
|
||||
"sourceComponentId": {
|
||||
"type": "string",
|
||||
"description": "The id of the component that triggered the event."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "An ISO 8601 timestamp of when the event occurred."
|
||||
},
|
||||
"context": {
|
||||
"type": "object",
|
||||
"description": "A JSON object containing the key-value pairs from the component's action.context, after resolving all data bindings.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"surfaceId",
|
||||
"sourceComponentId",
|
||||
"timestamp",
|
||||
"context"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"description": "Reports a client-side error. The content is flexible.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["userAction"] },
|
||||
{ "required": ["error"] }
|
||||
]
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
{
|
||||
"title": "A2UI Message Schema",
|
||||
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"beginRendering": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be rendered."
|
||||
},
|
||||
"catalogId": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the component catalog to use for this surface. If omitted, the client MUST default to the standard catalog for this A2UI version (https://a2ui.org/specification/v0_8/standard_catalog_definition.json)."
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"description": "The ID of the root component to render."
|
||||
},
|
||||
"styles": {
|
||||
"type": "object",
|
||||
"description": "Styling information for the UI.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["root", "surfaceId"]
|
||||
},
|
||||
"surfaceUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates a surface with a new set of components.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "A list containing all UI components for the surface.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this component."
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column."
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"description": "A wrapper object that MUST contain exactly one key, which is the name of the component type. The value is an object containing the properties for that specific component.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["id", "component"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "components"]
|
||||
},
|
||||
"dataModelUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates the data model for a surface.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface this data model update applies to."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced."
|
||||
},
|
||||
"contents": {
|
||||
"type": "array",
|
||||
"description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key for this data entry."
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"valueMap": {
|
||||
"description": "Represents a map as an adjacency list.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["contents", "surfaceId"]
|
||||
},
|
||||
"deleteSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to delete the surface identified by 'surfaceId'.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be deleted."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,823 +0,0 @@
|
||||
{
|
||||
"title": "A2UI Message Schema",
|
||||
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"beginRendering": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be rendered."
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"description": "The ID of the root component to render."
|
||||
},
|
||||
"styles": {
|
||||
"type": "object",
|
||||
"description": "Styling information for the UI.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"description": "The primary font for the UI."
|
||||
},
|
||||
"primaryColor": {
|
||||
"type": "string",
|
||||
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["root", "surfaceId"]
|
||||
},
|
||||
"surfaceUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates a surface with a new set of components.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "A list containing all UI components for the surface.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this component."
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column."
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"Text": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.",
|
||||
"enum": [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"caption",
|
||||
"body"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"Image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fit": {
|
||||
"type": "string",
|
||||
"description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.",
|
||||
"enum": [
|
||||
"contain",
|
||||
"cover",
|
||||
"fill",
|
||||
"none",
|
||||
"scale-down"
|
||||
]
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.",
|
||||
"enum": [
|
||||
"icon",
|
||||
"avatar",
|
||||
"smallFeature",
|
||||
"mediumFeature",
|
||||
"largeFeature",
|
||||
"header"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Icon": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "object",
|
||||
"description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"accountCircle",
|
||||
"add",
|
||||
"arrowBack",
|
||||
"arrowForward",
|
||||
"attachFile",
|
||||
"calendarToday",
|
||||
"call",
|
||||
"camera",
|
||||
"check",
|
||||
"close",
|
||||
"delete",
|
||||
"download",
|
||||
"edit",
|
||||
"event",
|
||||
"error",
|
||||
"favorite",
|
||||
"favoriteOff",
|
||||
"folder",
|
||||
"help",
|
||||
"home",
|
||||
"info",
|
||||
"locationOn",
|
||||
"lock",
|
||||
"lockOpen",
|
||||
"mail",
|
||||
"menu",
|
||||
"moreVert",
|
||||
"moreHoriz",
|
||||
"notificationsOff",
|
||||
"notifications",
|
||||
"payment",
|
||||
"person",
|
||||
"phone",
|
||||
"photo",
|
||||
"print",
|
||||
"refresh",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share",
|
||||
"shoppingCart",
|
||||
"star",
|
||||
"starHalf",
|
||||
"starOff",
|
||||
"upload",
|
||||
"visibility",
|
||||
"visibilityOff",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"Video": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"AudioPlayer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Row": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.",
|
||||
"enum": [
|
||||
"center",
|
||||
"end",
|
||||
"spaceAround",
|
||||
"spaceBetween",
|
||||
"spaceEvenly",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Column": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.",
|
||||
"enum": [
|
||||
"start",
|
||||
"center",
|
||||
"end",
|
||||
"spaceBetween",
|
||||
"spaceAround",
|
||||
"spaceEvenly"
|
||||
]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.",
|
||||
"enum": ["center", "end", "start", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"List": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "The direction in which the list items are laid out.",
|
||||
"enum": ["vertical", "horizontal"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis.",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Card": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be rendered inside the card."
|
||||
}
|
||||
},
|
||||
"required": ["child"]
|
||||
},
|
||||
"Tabs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabItems": {
|
||||
"type": "array",
|
||||
"description": "An array of objects, where each object defines a tab with a title and a child component.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "object",
|
||||
"description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "child"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tabItems"]
|
||||
},
|
||||
"Divider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"axis": {
|
||||
"type": "string",
|
||||
"description": "The orientation of the divider.",
|
||||
"enum": ["horizontal", "vertical"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Modal": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"entryPointChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component that opens the modal when interacted with (e.g., a button)."
|
||||
},
|
||||
"contentChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be displayed inside the modal."
|
||||
}
|
||||
},
|
||||
"required": ["entryPointChild", "contentChild"]
|
||||
},
|
||||
"Button": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to display in the button, typically a Text component."
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if this button should be styled as the primary action."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"literalNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"literalBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["child", "action"]
|
||||
},
|
||||
"CheckBox": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
},
|
||||
"TextField": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"textFieldType": {
|
||||
"type": "string",
|
||||
"description": "The type of input field to display.",
|
||||
"enum": [
|
||||
"date",
|
||||
"longText",
|
||||
"number",
|
||||
"shortText",
|
||||
"obscured"
|
||||
]
|
||||
},
|
||||
"validationRegexp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used for client-side validation of the input."
|
||||
}
|
||||
},
|
||||
"required": ["label"]
|
||||
},
|
||||
"DateTimeInput": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The selected date and/or time value in ISO 8601 format. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableDate": {
|
||||
"type": "boolean",
|
||||
"description": "If true, allows the user to select a date."
|
||||
},
|
||||
"enableTime": {
|
||||
"type": "boolean",
|
||||
"description": "If true, allows the user to select a time."
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
},
|
||||
"MultipleChoice": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"selections": {
|
||||
"type": "object",
|
||||
"description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalArray": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "An array of available options for the user to choose from.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to be associated with this option when selected."
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
}
|
||||
},
|
||||
"maxAllowedSelections": {
|
||||
"type": "integer",
|
||||
"description": "The maximum number of options that the user is allowed to select."
|
||||
}
|
||||
},
|
||||
"required": ["selections", "options"]
|
||||
},
|
||||
"Slider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minValue": {
|
||||
"type": "number",
|
||||
"description": "The minimum value of the slider."
|
||||
},
|
||||
"maxValue": {
|
||||
"type": "number",
|
||||
"description": "The maximum value of the slider."
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "component"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "components"]
|
||||
},
|
||||
"dataModelUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates the data model for a surface.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface this data model update applies to."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced."
|
||||
},
|
||||
"contents": {
|
||||
"type": "array",
|
||||
"description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key for this data entry."
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"valueMap": {
|
||||
"description": "Represents a map as an adjacency list.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["contents", "surfaceId"]
|
||||
},
|
||||
"deleteSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to delete the surface identified by 'surfaceId'.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be deleted."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
{
|
||||
"components": {
|
||||
"Text": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the base text style.",
|
||||
"enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"]
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"Image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the image to display.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"fit": {
|
||||
"type": "string",
|
||||
"description": "Specifies how the image should be resized to fit its container.",
|
||||
"enum": ["contain", "cover", "fill", "none", "scale-down"]
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the image size and style.",
|
||||
"enum": ["icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"]
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Icon": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "object",
|
||||
"description": "The name of the icon to display.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"accountCircle", "add", "arrowBack", "arrowForward", "attachFile",
|
||||
"calendarToday", "call", "camera", "check", "close", "delete",
|
||||
"download", "edit", "event", "error", "favorite", "favoriteOff",
|
||||
"folder", "help", "home", "info", "locationOn", "lock", "lockOpen",
|
||||
"mail", "menu", "moreVert", "moreHoriz", "notificationsOff",
|
||||
"notifications", "payment", "person", "phone", "photo", "print",
|
||||
"refresh", "search", "send", "settings", "share", "shoppingCart",
|
||||
"star", "starHalf", "starOff", "upload", "visibility",
|
||||
"visibilityOff", "warning"
|
||||
]
|
||||
},
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"Video": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the video to display.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"AudioPlayer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the audio to be played.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the audio, such as a title or summary.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Row": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": { "type": "array", "items": { "type": "string" } },
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": { "type": "string" },
|
||||
"dataBinding": { "type": "string" }
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"enum": ["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Column": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": { "type": "array", "items": { "type": "string" } },
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": { "type": "string" },
|
||||
"dataBinding": { "type": "string" }
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"enum": ["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"enum": ["center", "end", "start", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"List": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": { "type": "array", "items": { "type": "string" } },
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": { "type": "string" },
|
||||
"dataBinding": { "type": "string" }
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"enum": ["vertical", "horizontal"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Card": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be rendered inside the card."
|
||||
}
|
||||
},
|
||||
"required": ["child"]
|
||||
},
|
||||
"Tabs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"child": { "type": "string" }
|
||||
},
|
||||
"required": ["title", "child"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tabItems"]
|
||||
},
|
||||
"Divider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"axis": {
|
||||
"type": "string",
|
||||
"enum": ["horizontal", "vertical"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Modal": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"entryPointChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component that opens the modal when interacted with."
|
||||
},
|
||||
"contentChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be displayed inside the modal."
|
||||
}
|
||||
},
|
||||
"required": ["entryPointChild", "contentChild"]
|
||||
},
|
||||
"Button": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to display in the button."
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if this button should be styled as the primary action."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "type": "string" },
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": { "type": "string" },
|
||||
"literalString": { "type": "string" },
|
||||
"literalNumber": { "type": "number" },
|
||||
"literalBoolean": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["child", "action"]
|
||||
},
|
||||
"CheckBox": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalBoolean": { "type": "boolean" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
},
|
||||
"TextField": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"textFieldType": {
|
||||
"type": "string",
|
||||
"enum": ["date", "longText", "number", "shortText", "obscured"]
|
||||
},
|
||||
"validationRegexp": { "type": "string" }
|
||||
},
|
||||
"required": ["label"]
|
||||
},
|
||||
"DateTimeInput": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"enableDate": { "type": "boolean" },
|
||||
"enableTime": { "type": "boolean" }
|
||||
},
|
||||
"required": ["value"]
|
||||
},
|
||||
"MultipleChoice": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"selections": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalArray": { "type": "array", "items": { "type": "string" } },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"value": { "type": "string" }
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
}
|
||||
},
|
||||
"maxAllowedSelections": { "type": "integer" },
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["checkbox", "chips"]
|
||||
},
|
||||
"filterable": { "type": "boolean" }
|
||||
},
|
||||
"required": ["selections", "options"]
|
||||
},
|
||||
"Slider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalNumber": { "type": "number" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"minValue": { "type": "number" },
|
||||
"maxValue": { "type": "number" }
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"description": "The primary font for the UI."
|
||||
},
|
||||
"primaryColor": {
|
||||
"type": "string",
|
||||
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
"""A2UI server extension for the A2A protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from crewai.a2a.extensions.a2ui.models import A2UIResponse, extract_a2ui_json_objects
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
A2UIValidationError,
|
||||
validate_a2ui_message,
|
||||
)
|
||||
from crewai.a2a.extensions.server import ExtensionContext, ServerExtension
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
A2UI_MIME_TYPE = "application/json+a2ui"
|
||||
A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.8"
|
||||
|
||||
|
||||
class A2UIServerExtension(ServerExtension):
|
||||
"""A2A server extension that enables A2UI declarative UI generation.
|
||||
|
||||
When activated by a client that declares A2UI v0.8 support,
|
||||
this extension:
|
||||
|
||||
* Negotiates catalog preferences during ``on_request``.
|
||||
* Wraps A2UI messages in the agent response as A2A DataParts with
|
||||
``application/json+a2ui`` MIME type during ``on_response``.
|
||||
|
||||
Example::
|
||||
|
||||
A2AServerConfig(
|
||||
server_extensions=[A2UIServerExtension()],
|
||||
default_output_modes=["text/plain", "application/json+a2ui"],
|
||||
)
|
||||
"""
|
||||
|
||||
uri: str = A2UI_EXTENSION_URI
|
||||
required: bool = False
|
||||
description: str = "A2UI declarative UI generation"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
catalog_ids: list[str] | None = None,
|
||||
accept_inline_catalogs: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the A2UI server extension.
|
||||
|
||||
Args:
|
||||
catalog_ids: Catalog identifiers this server supports.
|
||||
accept_inline_catalogs: Whether inline catalog definitions are accepted.
|
||||
"""
|
||||
self._catalog_ids = catalog_ids or []
|
||||
self._accept_inline_catalogs = accept_inline_catalogs
|
||||
|
||||
@property
|
||||
def params(self) -> dict[str, Any]:
|
||||
"""Extension parameters advertised in the AgentCard."""
|
||||
result: dict[str, Any] = {}
|
||||
if self._catalog_ids:
|
||||
result["supportedCatalogIds"] = self._catalog_ids
|
||||
result["acceptsInlineCatalogs"] = self._accept_inline_catalogs
|
||||
return result
|
||||
|
||||
async def on_request(self, context: ExtensionContext) -> None:
|
||||
"""Extract A2UI catalog preferences from the client request.
|
||||
|
||||
Stores the negotiated catalog in ``context.state`` under
|
||||
``"a2ui_catalog_id"`` for downstream use.
|
||||
"""
|
||||
if not self.is_active(context):
|
||||
return
|
||||
|
||||
catalog_id = context.get_extension_metadata(self.uri, "catalogId")
|
||||
if isinstance(catalog_id, str):
|
||||
context.state["a2ui_catalog_id"] = catalog_id
|
||||
elif self._catalog_ids:
|
||||
context.state["a2ui_catalog_id"] = self._catalog_ids[0]
|
||||
|
||||
context.state["a2ui_active"] = True
|
||||
|
||||
async def on_response(self, context: ExtensionContext, result: Any) -> Any:
|
||||
"""Wrap A2UI messages in the result as A2A DataParts.
|
||||
|
||||
Scans the result for A2UI JSON payloads and converts them into
|
||||
DataParts with ``application/json+a2ui`` MIME type and A2UI metadata.
|
||||
"""
|
||||
if not context.state.get("a2ui_active"):
|
||||
return result
|
||||
|
||||
if not isinstance(result, str):
|
||||
return result
|
||||
|
||||
a2ui_messages = extract_a2ui_json_objects(result)
|
||||
if not a2ui_messages:
|
||||
return result
|
||||
|
||||
data_parts = [
|
||||
part
|
||||
for part in (_build_data_part(msg_data) for msg_data in a2ui_messages)
|
||||
if part is not None
|
||||
]
|
||||
|
||||
if not data_parts:
|
||||
return result
|
||||
|
||||
return A2UIResponse(text=result, a2ui_parts=data_parts)
|
||||
|
||||
|
||||
def _build_data_part(msg_data: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Validate a single A2UI message and wrap it as a DataPart dict."""
|
||||
try:
|
||||
validated = validate_a2ui_message(msg_data)
|
||||
except A2UIValidationError:
|
||||
logger.warning("Skipping invalid A2UI message in response", exc_info=True)
|
||||
return None
|
||||
return {
|
||||
"kind": "data",
|
||||
"data": validated.model_dump(by_alias=True, exclude_none=True),
|
||||
"metadata": {
|
||||
"mimeType": A2UI_MIME_TYPE,
|
||||
},
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
"""Validate A2UI message dicts via Pydantic models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai.a2a.extensions.a2ui.models import A2UIEvent, A2UIMessage
|
||||
|
||||
|
||||
class A2UIValidationError(Exception):
|
||||
"""Raised when an A2UI message fails validation."""
|
||||
|
||||
def __init__(self, message: str, errors: list[Any] | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.errors = errors or []
|
||||
|
||||
|
||||
def validate_a2ui_message(data: dict[str, Any]) -> A2UIMessage:
|
||||
"""Parse and validate an A2UI server-to-client message.
|
||||
|
||||
Args:
|
||||
data: Raw message dict (JSON-decoded).
|
||||
|
||||
Returns:
|
||||
Validated ``A2UIMessage`` instance.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If the data does not conform to the A2UI schema.
|
||||
"""
|
||||
try:
|
||||
return A2UIMessage.model_validate(data)
|
||||
except ValidationError as exc:
|
||||
raise A2UIValidationError(
|
||||
f"Invalid A2UI message: {exc.error_count()} validation error(s)",
|
||||
errors=exc.errors(),
|
||||
) from exc
|
||||
|
||||
|
||||
def validate_a2ui_event(data: dict[str, Any]) -> A2UIEvent:
|
||||
"""Parse and validate an A2UI client-to-server event.
|
||||
|
||||
Args:
|
||||
data: Raw event dict (JSON-decoded).
|
||||
|
||||
Returns:
|
||||
Validated ``A2UIEvent`` instance.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If the data does not conform to the A2UI event schema.
|
||||
"""
|
||||
try:
|
||||
return A2UIEvent.model_validate(data)
|
||||
except ValidationError as exc:
|
||||
raise A2UIValidationError(
|
||||
f"Invalid A2UI event: {exc.error_count()} validation error(s)",
|
||||
errors=exc.errors(),
|
||||
) from exc
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
import uuid
|
||||
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
@@ -18,7 +18,7 @@ from a2a.types import (
|
||||
TaskStatusUpdateEvent,
|
||||
TextPart,
|
||||
)
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
|
||||
@@ -7,11 +7,12 @@ from typing import (
|
||||
Any,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypedDict,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from pydantic import BeforeValidator, HttpUrl, TypeAdapter
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, TypedDict
|
||||
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class CommonParams(NamedTuple):
|
||||
|
||||
@@ -28,7 +28,6 @@ APPLICATION_PDF: Literal["application/pdf"] = "application/pdf"
|
||||
APPLICATION_OCTET_STREAM: Literal["application/octet-stream"] = (
|
||||
"application/octet-stream"
|
||||
)
|
||||
APPLICATION_A2UI_JSON: Literal["application/json+a2ui"] = "application/json+a2ui"
|
||||
|
||||
DEFAULT_CLIENT_INPUT_MODES: Final[list[Literal["text/plain", "application/json"]]] = [
|
||||
TEXT_PLAIN,
|
||||
@@ -312,10 +311,6 @@ def get_part_content_type(part: Part) -> str:
|
||||
if root.kind == "text":
|
||||
return TEXT_PLAIN
|
||||
if root.kind == "data":
|
||||
metadata = root.metadata or {}
|
||||
mime = metadata.get("mimeType", "")
|
||||
if mime == APPLICATION_A2UI_JSON:
|
||||
return APPLICATION_A2UI_JSON
|
||||
return APPLICATION_JSON
|
||||
if root.kind == "file":
|
||||
return root.file.mime_type or APPLICATION_OCTET_STREAM
|
||||
|
||||
@@ -10,7 +10,7 @@ from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, TypedDict, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from a2a.server.agent_execution import RequestContext
|
||||
@@ -38,7 +38,6 @@ from a2a.utils import (
|
||||
from a2a.utils.errors import ServerError
|
||||
from aiocache import SimpleMemoryCache, caches # type: ignore[import-untyped]
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from crewai.a2a.utils.agent_card import _get_server_config
|
||||
from crewai.a2a.utils.content_type import validate_message_parts
|
||||
|
||||
@@ -23,6 +23,7 @@ from pydantic import (
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
from crewai.agent.utils import (
|
||||
ahandle_knowledge_retrieval,
|
||||
apply_training_data,
|
||||
@@ -65,6 +66,7 @@ from crewai.mcp.tool_resolver import MCPToolResolver
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.security.fingerprint import Fingerprint
|
||||
from crewai.tools.agent_tools.agent_tools import AgentTools
|
||||
from crewai.types.callback import SerializableCallable
|
||||
from crewai.utilities.agent_utils import (
|
||||
get_tool_names,
|
||||
is_inside_event_loop,
|
||||
@@ -74,6 +76,7 @@ from crewai.utilities.agent_utils import (
|
||||
)
|
||||
from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE
|
||||
from crewai.utilities.converter import Converter, ConverterError
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.guardrail import process_guardrail
|
||||
from crewai.utilities.guardrail_types import GuardrailType
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
@@ -141,7 +144,7 @@ class Agent(BaseAgent):
|
||||
default=None,
|
||||
description="Maximum execution time for an agent to execute a task",
|
||||
)
|
||||
step_callback: Any | None = Field(
|
||||
step_callback: SerializableCallable | None = Field(
|
||||
default=None,
|
||||
description="Callback to be executed after each step of the agent execution.",
|
||||
)
|
||||
@@ -149,10 +152,10 @@ class Agent(BaseAgent):
|
||||
default=True,
|
||||
description="Use system prompt for the agent.",
|
||||
)
|
||||
llm: str | InstanceOf[BaseLLM] | Any = Field(
|
||||
llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
function_calling_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
function_calling_llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
system_template: str | None = Field(
|
||||
@@ -192,13 +195,23 @@ class Agent(BaseAgent):
|
||||
default="safe",
|
||||
description="Mode for code execution: 'safe' (using Docker) or 'unsafe' (direct execution).",
|
||||
)
|
||||
reasoning: bool = Field(
|
||||
planning_config: PlanningConfig | None = Field(
|
||||
default=None,
|
||||
description="Configuration for agent planning before task execution.",
|
||||
)
|
||||
planning: bool = Field(
|
||||
default=False,
|
||||
description="Whether the agent should reflect and create a plan before executing a task.",
|
||||
)
|
||||
reasoning: bool = Field(
|
||||
default=False,
|
||||
description="[DEPRECATED: Use planning_config instead] Whether the agent should reflect and create a plan before executing a task.",
|
||||
deprecated=True,
|
||||
)
|
||||
max_reasoning_attempts: int | None = Field(
|
||||
default=None,
|
||||
description="Maximum number of reasoning attempts before executing the task. If None, will try until ready.",
|
||||
description="[DEPRECATED: Use planning_config.max_attempts instead] Maximum number of reasoning attempts before executing the task. If None, will try until ready.",
|
||||
deprecated=True,
|
||||
)
|
||||
embedder: EmbedderConfig | None = Field(
|
||||
default=None,
|
||||
@@ -265,8 +278,26 @@ class Agent(BaseAgent):
|
||||
if self.allow_code_execution:
|
||||
self._validate_docker_installation()
|
||||
|
||||
# Handle backward compatibility: convert reasoning=True to planning_config
|
||||
if self.reasoning and self.planning_config is None:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"The 'reasoning' parameter is deprecated. Use 'planning_config=PlanningConfig()' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.planning_config = PlanningConfig(
|
||||
max_attempts=self.max_reasoning_attempts,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def planning_enabled(self) -> bool:
|
||||
"""Check if planning is enabled for this agent."""
|
||||
return self.planning_config is not None or self.planning
|
||||
|
||||
def _setup_agent_executor(self) -> None:
|
||||
if not self.cache_handler:
|
||||
self.cache_handler = CacheHandler()
|
||||
@@ -310,7 +341,7 @@ class Agent(BaseAgent):
|
||||
return (
|
||||
hasattr(self.llm, "supports_function_calling")
|
||||
and callable(getattr(self.llm, "supports_function_calling", None))
|
||||
and self.llm.supports_function_calling()
|
||||
and self.llm.supports_function_calling() # type: ignore[union-attr]
|
||||
and len(tools) > 0
|
||||
)
|
||||
|
||||
@@ -335,7 +366,12 @@ class Agent(BaseAgent):
|
||||
ValueError: If the max execution time is not a positive integer.
|
||||
RuntimeError: If the agent execution fails for other reasons.
|
||||
"""
|
||||
handle_reasoning(self, task)
|
||||
get_env_context()
|
||||
# Only call handle_reasoning for legacy CrewAgentExecutor
|
||||
# For AgentExecutor, planning is handled in AgentExecutor.generate_plan()
|
||||
if self.executor_class is not AgentExecutor:
|
||||
handle_reasoning(self, task)
|
||||
|
||||
self._inject_date_to_task(task)
|
||||
|
||||
if self.tools_handler:
|
||||
@@ -577,7 +613,10 @@ class Agent(BaseAgent):
|
||||
ValueError: If the max execution time is not a positive integer.
|
||||
RuntimeError: If the agent execution fails for other reasons.
|
||||
"""
|
||||
handle_reasoning(self, task)
|
||||
if self.executor_class is not AgentExecutor:
|
||||
handle_reasoning(
|
||||
self, task
|
||||
) # we need this till CrewAgentExecutor migrates to AgentExecutor
|
||||
self._inject_date_to_task(task)
|
||||
|
||||
if self.tools_handler:
|
||||
@@ -1423,17 +1462,19 @@ class Agent(BaseAgent):
|
||||
except Exception as e:
|
||||
self._logger.log("error", f"Failed to save kickoff result to memory: {e}")
|
||||
|
||||
def _execute_and_build_output(
|
||||
def _build_output_from_result(
|
||||
self,
|
||||
result: dict[str, Any],
|
||||
executor: AgentExecutor,
|
||||
inputs: dict[str, str],
|
||||
response_format: type[Any] | None = None,
|
||||
) -> LiteAgentOutput:
|
||||
"""Execute the agent and build the output object.
|
||||
"""Build a LiteAgentOutput from an executor result dict.
|
||||
|
||||
Shared logic used by both sync and async execution paths.
|
||||
|
||||
Args:
|
||||
result: The result dictionary from executor.invoke / invoke_async.
|
||||
executor: The executor instance.
|
||||
inputs: Input dictionary for execution.
|
||||
response_format: Optional response format.
|
||||
|
||||
Returns:
|
||||
@@ -1441,8 +1482,6 @@ class Agent(BaseAgent):
|
||||
"""
|
||||
import json
|
||||
|
||||
# Execute the agent (this is called from sync path, so invoke returns dict)
|
||||
result = cast(dict[str, Any], executor.invoke(inputs))
|
||||
output = result.get("output", "")
|
||||
|
||||
# Handle response format conversion
|
||||
@@ -1490,91 +1529,39 @@ class Agent(BaseAgent):
|
||||
else str(raw_output)
|
||||
)
|
||||
|
||||
todo_results = LiteAgentOutput.from_todo_items(executor.state.todos.items)
|
||||
|
||||
return LiteAgentOutput(
|
||||
raw=raw_str,
|
||||
pydantic=formatted_result,
|
||||
agent_role=self.role,
|
||||
usage_metrics=usage_metrics.model_dump() if usage_metrics else None,
|
||||
messages=executor.messages,
|
||||
messages=list(executor.state.messages),
|
||||
plan=executor.state.plan,
|
||||
todos=todo_results,
|
||||
replan_count=executor.state.replan_count,
|
||||
last_replan_reason=executor.state.last_replan_reason,
|
||||
)
|
||||
|
||||
def _execute_and_build_output(
|
||||
self,
|
||||
executor: AgentExecutor,
|
||||
inputs: dict[str, str],
|
||||
response_format: type[Any] | None = None,
|
||||
) -> LiteAgentOutput:
|
||||
"""Execute the agent synchronously and build the output object."""
|
||||
result = cast(dict[str, Any], executor.invoke(inputs))
|
||||
return self._build_output_from_result(result, executor, response_format)
|
||||
|
||||
async def _execute_and_build_output_async(
|
||||
self,
|
||||
executor: AgentExecutor,
|
||||
inputs: dict[str, str],
|
||||
response_format: type[Any] | None = None,
|
||||
) -> LiteAgentOutput:
|
||||
"""Execute the agent asynchronously and build the output object.
|
||||
|
||||
This is the async version of _execute_and_build_output that uses
|
||||
invoke_async() for native async execution within event loops.
|
||||
|
||||
Args:
|
||||
executor: The executor instance.
|
||||
inputs: Input dictionary for execution.
|
||||
response_format: Optional response format.
|
||||
|
||||
Returns:
|
||||
LiteAgentOutput with raw output, formatted result, and metrics.
|
||||
"""
|
||||
import json
|
||||
|
||||
# Execute the agent asynchronously
|
||||
"""Execute the agent asynchronously and build the output object."""
|
||||
result = await executor.invoke_async(inputs)
|
||||
output = result.get("output", "")
|
||||
|
||||
# Handle response format conversion
|
||||
formatted_result: BaseModel | None = None
|
||||
raw_output: str
|
||||
|
||||
if isinstance(output, BaseModel):
|
||||
formatted_result = output
|
||||
raw_output = output.model_dump_json()
|
||||
elif response_format:
|
||||
raw_output = str(output) if not isinstance(output, str) else output
|
||||
try:
|
||||
model_schema = generate_model_description(response_format)
|
||||
schema = json.dumps(model_schema, indent=2)
|
||||
instructions = self.i18n.slice("formatted_task_instructions").format(
|
||||
output_format=schema
|
||||
)
|
||||
|
||||
converter = Converter(
|
||||
llm=self.llm,
|
||||
text=raw_output,
|
||||
model=response_format,
|
||||
instructions=instructions,
|
||||
)
|
||||
|
||||
conversion_result = converter.to_pydantic()
|
||||
if isinstance(conversion_result, BaseModel):
|
||||
formatted_result = conversion_result
|
||||
except ConverterError:
|
||||
pass # Keep raw output if conversion fails
|
||||
else:
|
||||
raw_output = str(output) if not isinstance(output, str) else output
|
||||
|
||||
# Get token usage metrics
|
||||
if isinstance(self.llm, BaseLLM):
|
||||
usage_metrics = self.llm.get_token_usage_summary()
|
||||
else:
|
||||
usage_metrics = self._token_process.get_summary()
|
||||
|
||||
raw_str = (
|
||||
raw_output
|
||||
if isinstance(raw_output, str)
|
||||
else raw_output.model_dump_json()
|
||||
if isinstance(raw_output, BaseModel)
|
||||
else str(raw_output)
|
||||
)
|
||||
|
||||
return LiteAgentOutput(
|
||||
raw=raw_str,
|
||||
pydantic=formatted_result,
|
||||
agent_role=self.role,
|
||||
usage_metrics=usage_metrics.model_dump() if usage_metrics else None,
|
||||
messages=executor.messages,
|
||||
)
|
||||
return self._build_output_from_result(result, executor, response_format)
|
||||
|
||||
def _process_kickoff_guardrail(
|
||||
self,
|
||||
|
||||
138
lib/crewai/src/crewai/agent/planning_config.py
Normal file
138
lib/crewai/src/crewai/agent/planning_config.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
|
||||
|
||||
class PlanningConfig(BaseModel):
|
||||
"""Configuration for agent planning/reasoning before task execution.
|
||||
|
||||
This allows users to customize the planning behavior including prompts,
|
||||
iteration limits, the LLM used for planning, and the reasoning effort
|
||||
level that controls post-step observation and replanning behavior.
|
||||
|
||||
Note: To disable planning, don't pass a planning_config or set planning=False
|
||||
on the Agent. The presence of a PlanningConfig enables planning.
|
||||
|
||||
Attributes:
|
||||
reasoning_effort: Controls observation and replanning after each step.
|
||||
- "low": Observe each step (validates success), but skip the
|
||||
decide/replan/refine pipeline. Steps are marked complete and
|
||||
execution continues linearly. Fastest option.
|
||||
- "medium": Observe each step. On failure, trigger replanning.
|
||||
On success, skip refinement and continue. Balanced option.
|
||||
- "high": Full observation pipeline — observe every step, then
|
||||
route through decide_next_action which can trigger early goal
|
||||
achievement, full replanning, or lightweight refinement.
|
||||
Most adaptive but adds latency per step.
|
||||
max_attempts: Maximum number of planning refinement attempts.
|
||||
If None, will continue until the agent indicates readiness.
|
||||
max_steps: Maximum number of steps in the generated plan.
|
||||
system_prompt: Custom system prompt for planning. Uses default if None.
|
||||
plan_prompt: Custom prompt for creating the initial plan.
|
||||
refine_prompt: Custom prompt for refining the plan.
|
||||
llm: LLM to use for planning. Uses agent's LLM if None.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
|
||||
# Simple usage — fast, linear execution (default)
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Research topics",
|
||||
backstory="Expert researcher",
|
||||
planning_config=PlanningConfig(),
|
||||
)
|
||||
|
||||
# Balanced — replan only when steps fail
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Research topics",
|
||||
backstory="Expert researcher",
|
||||
planning_config=PlanningConfig(
|
||||
reasoning_effort="medium",
|
||||
),
|
||||
)
|
||||
|
||||
# Full adaptive planning with refinement and replanning
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Research topics",
|
||||
backstory="Expert researcher",
|
||||
planning_config=PlanningConfig(
|
||||
reasoning_effort="high",
|
||||
max_attempts=3,
|
||||
max_steps=10,
|
||||
plan_prompt="Create a focused plan for: {description}",
|
||||
llm="gpt-4o-mini", # Use cheaper model for planning
|
||||
),
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
reasoning_effort: Literal["low", "medium", "high"] = Field(
|
||||
default="medium",
|
||||
description=(
|
||||
"Controls post-step observation and replanning behavior. "
|
||||
"'low' observes steps but skips replanning/refinement (fastest). "
|
||||
"'medium' observes and replans only on step failure (balanced). "
|
||||
"'high' runs full observation pipeline with replanning, refinement, "
|
||||
"and early goal detection (most adaptive, highest latency)."
|
||||
),
|
||||
)
|
||||
max_attempts: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Maximum number of planning refinement attempts. "
|
||||
"If None, will continue until the agent indicates readiness."
|
||||
),
|
||||
)
|
||||
max_steps: int = Field(
|
||||
default=20,
|
||||
description="Maximum number of steps in the generated plan.",
|
||||
ge=1,
|
||||
)
|
||||
system_prompt: str | None = Field(
|
||||
default=None,
|
||||
description="Custom system prompt for planning. Uses default if None.",
|
||||
)
|
||||
plan_prompt: str | None = Field(
|
||||
default=None,
|
||||
description="Custom prompt for creating the initial plan.",
|
||||
)
|
||||
refine_prompt: str | None = Field(
|
||||
default=None,
|
||||
description="Custom prompt for refining the plan.",
|
||||
)
|
||||
max_replans: int = Field(
|
||||
default=3,
|
||||
description="Maximum number of full replanning attempts before finalizing.",
|
||||
ge=0,
|
||||
)
|
||||
max_step_iterations: int = Field(
|
||||
default=15,
|
||||
description=(
|
||||
"Maximum LLM iterations per step in the StepExecutor multi-turn loop. "
|
||||
"Lower values make steps faster but less thorough."
|
||||
),
|
||||
ge=1,
|
||||
)
|
||||
step_timeout: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Maximum wall-clock seconds for a single step execution. "
|
||||
"If exceeded, the step is marked as failed and observation decides "
|
||||
"whether to continue or replan. None means no per-step timeout."
|
||||
),
|
||||
)
|
||||
llm: str | BaseLLM | None = Field(
|
||||
default=None,
|
||||
description="LLM to use for planning. Uses agent's LLM if None.",
|
||||
)
|
||||
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
@@ -28,13 +28,20 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def handle_reasoning(agent: Agent, task: Task) -> None:
|
||||
"""Handle the reasoning process for an agent before task execution.
|
||||
"""Handle the reasoning/planning process for an agent before task execution.
|
||||
|
||||
This function checks if planning is enabled for the agent and, if so,
|
||||
creates a plan that gets appended to the task description.
|
||||
|
||||
Note: This function is used by CrewAgentExecutor (legacy path).
|
||||
For AgentExecutor, planning is handled in AgentExecutor.generate_plan().
|
||||
|
||||
Args:
|
||||
agent: The agent performing the task.
|
||||
task: The task to execute.
|
||||
"""
|
||||
if not agent.reasoning:
|
||||
# Check if planning is enabled using the planning_enabled property
|
||||
if not getattr(agent, "planning_enabled", False):
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -43,13 +50,13 @@ def handle_reasoning(agent: Agent, task: Task) -> None:
|
||||
AgentReasoningOutput,
|
||||
)
|
||||
|
||||
reasoning_handler = AgentReasoning(task=task, agent=agent)
|
||||
reasoning_output: AgentReasoningOutput = (
|
||||
reasoning_handler.handle_agent_reasoning()
|
||||
planning_handler = AgentReasoning(agent=agent, task=task)
|
||||
planning_output: AgentReasoningOutput = (
|
||||
planning_handler.handle_agent_reasoning()
|
||||
)
|
||||
task.description += f"\n\nReasoning Plan:\n{reasoning_output.plan.plan}"
|
||||
task.description += f"\n\nPlanning:\n{planning_output.plan.plan}"
|
||||
except Exception as e:
|
||||
agent._logger.log("error", f"Error during reasoning process: {e!s}")
|
||||
agent._logger.log("error", f"Error during planning: {e!s}")
|
||||
|
||||
|
||||
def build_task_prompt_with_schema(task: Task, task_prompt: str, i18n: I18N) -> str:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from copy import copy as shallow_copy
|
||||
from hashlib import md5
|
||||
import re
|
||||
@@ -12,6 +11,7 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
@@ -26,10 +26,14 @@ from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.knowledge.knowledge_config import KnowledgeConfig
|
||||
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
|
||||
from crewai.mcp.config import MCPServerConfig
|
||||
from crewai.memory.memory_scope import MemoryScope, MemorySlice
|
||||
from crewai.memory.unified_memory import Memory
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.security.security_config import SecurityConfig
|
||||
from crewai.tools.base_tool import BaseTool, Tool
|
||||
from crewai.types.callback import SerializableCallable
|
||||
from crewai.utilities.config import process_config
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.logger import Logger
|
||||
@@ -179,7 +183,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default=None,
|
||||
description="Knowledge sources for the agent.",
|
||||
)
|
||||
knowledge_storage: Any | None = Field(
|
||||
knowledge_storage: InstanceOf[BaseKnowledgeStorage] | None = Field(
|
||||
default=None,
|
||||
description="Custom knowledge storage for the agent.",
|
||||
)
|
||||
@@ -187,7 +191,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default_factory=SecurityConfig,
|
||||
description="Security configuration for the agent, including fingerprinting.",
|
||||
)
|
||||
callbacks: list[Callable[[Any], Any]] = Field(
|
||||
callbacks: list[SerializableCallable] = Field(
|
||||
default_factory=list, description="Callbacks to be used for the agent"
|
||||
)
|
||||
adapted_agent: bool = Field(
|
||||
@@ -205,7 +209,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default=None,
|
||||
description="List of MCP server references. Supports 'https://server.com/path' for external servers and bare slugs like 'notion' for connected MCP integrations. Use '#tool_name' suffix for specific tools.",
|
||||
)
|
||||
memory: Any = Field(
|
||||
memory: bool | Memory | MemoryScope | MemorySlice | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Enable agent memory. Pass True for default Memory(), "
|
||||
|
||||
345
lib/crewai/src/crewai/agents/planner_observer.py
Normal file
345
lib/crewai/src/crewai/agents/planner_observer.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""PlannerObserver: Observation phase after each step execution.
|
||||
|
||||
Implements the "Observe" phase. After every step execution, the Planner
|
||||
analyzes what happened, what new information was learned, and whether the
|
||||
remaining plan is still valid.
|
||||
|
||||
This is NOT an error detector — it runs on every step, including successes,
|
||||
to incorporate runtime observations into the remaining plan.
|
||||
|
||||
Refinements are structured (StepRefinement objects) and applied directly
|
||||
from the observation result — no second LLM call required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.observation_events import (
|
||||
StepObservationCompletedEvent,
|
||||
StepObservationFailedEvent,
|
||||
StepObservationStartedEvent,
|
||||
)
|
||||
from crewai.utilities.agent_utils import extract_task_section
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
from crewai.utilities.planning_types import StepObservation, TodoItem
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.task import Task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlannerObserver:
|
||||
"""Observes step execution results and decides on plan continuation.
|
||||
|
||||
After EVERY step execution, this class:
|
||||
1. Analyzes what the step accomplished
|
||||
2. Identifies new information learned
|
||||
3. Decides if the remaining plan is still valid
|
||||
4. Suggests lightweight refinements or triggers full replanning
|
||||
|
||||
LLM resolution (magical fallback):
|
||||
- If ``agent.planning_config.llm`` is explicitly set → use that
|
||||
- Otherwise → fall back to ``agent.llm`` (same LLM for everything)
|
||||
|
||||
Args:
|
||||
agent: The agent instance (for LLM resolution and config).
|
||||
task: Optional task context (for description and expected output).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: Agent,
|
||||
task: Task | None = None,
|
||||
kickoff_input: str = "",
|
||||
) -> None:
|
||||
self.agent = agent
|
||||
self.task = task
|
||||
self.kickoff_input = kickoff_input
|
||||
self.llm = self._resolve_llm()
|
||||
self._i18n: I18N = get_i18n()
|
||||
|
||||
def _resolve_llm(self) -> Any:
|
||||
"""Resolve which LLM to use for observation/planning.
|
||||
|
||||
Mirrors AgentReasoning._resolve_llm(): uses planning_config.llm
|
||||
if explicitly set, otherwise falls back to agent.llm.
|
||||
|
||||
Returns:
|
||||
The resolved LLM instance.
|
||||
"""
|
||||
from crewai.llm import LLM
|
||||
|
||||
config = getattr(self.agent, "planning_config", None)
|
||||
if config is not None and config.llm is not None:
|
||||
if isinstance(config.llm, LLM):
|
||||
return config.llm
|
||||
return create_llm(config.llm)
|
||||
return self.agent.llm
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def observe(
|
||||
self,
|
||||
completed_step: TodoItem,
|
||||
result: str,
|
||||
all_completed: list[TodoItem],
|
||||
remaining_todos: list[TodoItem],
|
||||
) -> StepObservation:
|
||||
"""Observe a step's result and decide on plan continuation.
|
||||
|
||||
This runs after EVERY step execution — not just failures.
|
||||
|
||||
Args:
|
||||
completed_step: The todo item that was just executed.
|
||||
result: The final result string from the step.
|
||||
all_completed: All previously completed todos (for context).
|
||||
remaining_todos: The pending todos still in the plan.
|
||||
|
||||
Returns:
|
||||
StepObservation with the Planner's analysis. Any suggested
|
||||
refinements are structured StepRefinement objects ready for
|
||||
direct application — no second LLM call needed.
|
||||
"""
|
||||
agent_role = self.agent.role
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self.agent,
|
||||
event=StepObservationStartedEvent(
|
||||
agent_role=agent_role,
|
||||
step_number=completed_step.step_number,
|
||||
step_description=completed_step.description,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
),
|
||||
)
|
||||
|
||||
messages = self._build_observation_messages(
|
||||
completed_step, result, all_completed, remaining_todos
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.llm.call(
|
||||
messages,
|
||||
response_model=StepObservation,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
)
|
||||
|
||||
observation = self._parse_observation_response(response)
|
||||
|
||||
refinement_summaries = (
|
||||
[
|
||||
f"Step {r.step_number}: {r.new_description}"
|
||||
for r in observation.suggested_refinements
|
||||
]
|
||||
if observation.suggested_refinements
|
||||
else None
|
||||
)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self.agent,
|
||||
event=StepObservationCompletedEvent(
|
||||
agent_role=agent_role,
|
||||
step_number=completed_step.step_number,
|
||||
step_description=completed_step.description,
|
||||
step_completed_successfully=observation.step_completed_successfully,
|
||||
key_information_learned=observation.key_information_learned,
|
||||
remaining_plan_still_valid=observation.remaining_plan_still_valid,
|
||||
needs_full_replan=observation.needs_full_replan,
|
||||
replan_reason=observation.replan_reason,
|
||||
goal_already_achieved=observation.goal_already_achieved,
|
||||
suggested_refinements=refinement_summaries,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
),
|
||||
)
|
||||
|
||||
return observation
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Observation LLM call failed: {e}. Defaulting to conservative replan."
|
||||
)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self.agent,
|
||||
event=StepObservationFailedEvent(
|
||||
agent_role=agent_role,
|
||||
step_number=completed_step.step_number,
|
||||
step_description=completed_step.description,
|
||||
error=str(e),
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
),
|
||||
)
|
||||
|
||||
# Don't force a full replan — the step may have succeeded even if the
|
||||
# observer LLM failed to parse the result. Defaulting to "continue" is
|
||||
# far less disruptive than wiping the entire plan on every observer error.
|
||||
return StepObservation(
|
||||
step_completed_successfully=True,
|
||||
key_information_learned="",
|
||||
remaining_plan_still_valid=True,
|
||||
needs_full_replan=False,
|
||||
)
|
||||
|
||||
def apply_refinements(
|
||||
self,
|
||||
observation: StepObservation,
|
||||
remaining_todos: list[TodoItem],
|
||||
) -> list[TodoItem]:
|
||||
"""Apply structured refinements from the observation directly to todo descriptions.
|
||||
|
||||
No LLM call needed — refinements are already structured StepRefinement
|
||||
objects produced by the observation call. This is a pure in-memory update.
|
||||
|
||||
Args:
|
||||
observation: The observation containing structured refinements.
|
||||
remaining_todos: The pending todos to update in-place.
|
||||
|
||||
Returns:
|
||||
The same todo list with updated descriptions where refinements applied.
|
||||
"""
|
||||
if not observation.suggested_refinements:
|
||||
return remaining_todos
|
||||
|
||||
todo_by_step: dict[int, TodoItem] = {t.step_number: t for t in remaining_todos}
|
||||
for refinement in observation.suggested_refinements:
|
||||
if refinement.step_number in todo_by_step and refinement.new_description:
|
||||
todo_by_step[
|
||||
refinement.step_number
|
||||
].description = refinement.new_description
|
||||
|
||||
return remaining_todos
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal: Message building
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_observation_messages(
|
||||
self,
|
||||
completed_step: TodoItem,
|
||||
result: str,
|
||||
all_completed: list[TodoItem],
|
||||
remaining_todos: list[TodoItem],
|
||||
) -> list[LLMMessage]:
|
||||
"""Build messages for the observation LLM call."""
|
||||
task_desc = ""
|
||||
task_goal = ""
|
||||
if self.task:
|
||||
task_desc = self.task.description or ""
|
||||
task_goal = self.task.expected_output or ""
|
||||
elif self.kickoff_input:
|
||||
# Standalone kickoff path — no Task object, but we have the raw input.
|
||||
# Extract just the ## Task section so the observer sees the actual goal,
|
||||
# not the full enriched instruction with env/tools/verification noise.
|
||||
task_desc = extract_task_section(self.kickoff_input)
|
||||
task_goal = "Complete the task successfully"
|
||||
|
||||
system_prompt = self._i18n.retrieve("planning", "observation_system_prompt")
|
||||
|
||||
# Build context of what's been done
|
||||
completed_summary = ""
|
||||
if all_completed:
|
||||
completed_lines = []
|
||||
for todo in all_completed:
|
||||
result_preview = (todo.result or "")[:200]
|
||||
completed_lines.append(
|
||||
f" Step {todo.step_number}: {todo.description}\n"
|
||||
f" Result: {result_preview}"
|
||||
)
|
||||
completed_summary = "\n## Previously completed steps:\n" + "\n".join(
|
||||
completed_lines
|
||||
)
|
||||
|
||||
# Build remaining plan
|
||||
remaining_summary = ""
|
||||
if remaining_todos:
|
||||
remaining_lines = [
|
||||
f" Step {todo.step_number}: {todo.description}"
|
||||
for todo in remaining_todos
|
||||
]
|
||||
remaining_summary = "\n## Remaining plan steps:\n" + "\n".join(
|
||||
remaining_lines
|
||||
)
|
||||
|
||||
user_prompt = self._i18n.retrieve("planning", "observation_user_prompt").format(
|
||||
task_description=task_desc,
|
||||
task_goal=task_goal,
|
||||
completed_summary=completed_summary,
|
||||
step_number=completed_step.step_number,
|
||||
step_description=completed_step.description,
|
||||
step_result=result,
|
||||
remaining_summary=remaining_summary,
|
||||
)
|
||||
|
||||
return [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _parse_observation_response(response: Any) -> StepObservation:
|
||||
"""Parse the LLM response into a StepObservation.
|
||||
|
||||
The LLM may return:
|
||||
- A StepObservation instance directly (streaming + litellm path)
|
||||
- A JSON string (non-streaming path serialises model_dump_json())
|
||||
- A dict (some provider paths)
|
||||
- Something else (unexpected)
|
||||
|
||||
We handle all cases to avoid silently falling back to a
|
||||
hardcoded success default.
|
||||
"""
|
||||
|
||||
if isinstance(response, StepObservation):
|
||||
return response
|
||||
|
||||
# JSON string path — most common miss before this fix
|
||||
if isinstance(response, str):
|
||||
text = response.strip()
|
||||
try:
|
||||
return StepObservation.model_validate_json(text)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
# Some LLMs wrap the JSON in markdown fences
|
||||
if text.startswith("```"):
|
||||
lines = text.split("\n")
|
||||
# Strip first and last lines (``` markers)
|
||||
inner = "\n".join(
|
||||
lines[1:-1] if lines[-1].strip() == "```" else lines[1:]
|
||||
)
|
||||
try:
|
||||
return StepObservation.model_validate_json(inner.strip())
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
# Dict path
|
||||
if isinstance(response, dict):
|
||||
try:
|
||||
return StepObservation.model_validate(response)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
# Last resort — log what we got so it's diagnosable
|
||||
logger.warning(
|
||||
"Could not parse observation response (type=%s). "
|
||||
"Falling back to default failure observation. Preview: %.200s",
|
||||
type(response).__name__,
|
||||
str(response),
|
||||
)
|
||||
return StepObservation(
|
||||
step_completed_successfully=False,
|
||||
key_information_learned=str(response) if response else "",
|
||||
remaining_plan_still_valid=False,
|
||||
)
|
||||
629
lib/crewai/src/crewai/agents/step_executor.py
Normal file
629
lib/crewai/src/crewai/agents/step_executor.py
Normal file
@@ -0,0 +1,629 @@
|
||||
"""StepExecutor: Isolated executor for a single plan step.
|
||||
|
||||
Implements the direct-action execution pattern from Plan-and-Act
|
||||
(arxiv 2503.09572): the Executor receives one step description,
|
||||
makes a single LLM call, executes any tool call returned, and
|
||||
returns the result immediately.
|
||||
|
||||
There is no inner loop. Recovery from failure (retry, replan) is
|
||||
the responsibility of PlannerObserver and AgentExecutor — keeping
|
||||
this class single-purpose and fast.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import json
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.agents.parser import AgentAction, AgentFinish
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.utilities.agent_utils import (
|
||||
build_tool_calls_assistant_message,
|
||||
check_native_tool_support,
|
||||
enforce_rpm_limit,
|
||||
execute_single_native_tool_call,
|
||||
extract_task_section,
|
||||
format_message_for_llm,
|
||||
is_tool_call_list,
|
||||
process_llm_response,
|
||||
setup_native_tools,
|
||||
)
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.planning_types import TodoItem
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.crew import Crew
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.task import Task
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
|
||||
|
||||
class StepExecutor:
|
||||
"""Executes a SINGLE todo item using direct-action execution.
|
||||
|
||||
The StepExecutor owns its own message list per invocation. It never reads
|
||||
or writes the AgentExecutor's state. Results flow back via StepResult.
|
||||
|
||||
Execution pattern (per Plan-and-Act, arxiv 2503.09572):
|
||||
1. Build messages from todo + context
|
||||
2. Call LLM once (with or without native tools)
|
||||
3. If tool call → execute it → return tool result
|
||||
4. If text answer → return it directly
|
||||
No inner loop — recovery is PlannerObserver's responsibility.
|
||||
|
||||
Args:
|
||||
llm: The language model to use for execution.
|
||||
tools: Structured tools available to the executor.
|
||||
agent: The agent instance (for role/goal/verbose/config).
|
||||
original_tools: Original BaseTool instances (needed for native tool schema).
|
||||
tools_handler: Optional tools handler for caching and delegation tracking.
|
||||
task: Optional task context.
|
||||
crew: Optional crew context.
|
||||
function_calling_llm: Optional separate LLM for function calling.
|
||||
request_within_rpm_limit: Optional RPM limit function.
|
||||
callbacks: Optional list of callbacks.
|
||||
i18n: Optional i18n instance.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: BaseLLM,
|
||||
tools: list[CrewStructuredTool],
|
||||
agent: Agent,
|
||||
original_tools: list[BaseTool] | None = None,
|
||||
tools_handler: ToolsHandler | None = None,
|
||||
task: Task | None = None,
|
||||
crew: Crew | None = None,
|
||||
function_calling_llm: BaseLLM | None = None,
|
||||
request_within_rpm_limit: Callable[[], bool] | None = None,
|
||||
callbacks: list[Any] | None = None,
|
||||
i18n: I18N | None = None,
|
||||
) -> None:
|
||||
self.llm = llm
|
||||
self.tools = tools
|
||||
self.agent = agent
|
||||
self.original_tools = original_tools or []
|
||||
self.tools_handler = tools_handler
|
||||
self.task = task
|
||||
self.crew = crew
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.request_within_rpm_limit = request_within_rpm_limit
|
||||
self.callbacks = callbacks or []
|
||||
self._i18n: I18N = i18n or get_i18n()
|
||||
self._printer: Printer = Printer()
|
||||
|
||||
# Native tool support — set up once
|
||||
self._use_native_tools = check_native_tool_support(
|
||||
self.llm, self.original_tools
|
||||
)
|
||||
self._openai_tools: list[dict[str, Any]] = []
|
||||
self._available_functions: dict[str, Callable[..., Any]] = {}
|
||||
if self._use_native_tools and self.original_tools:
|
||||
(
|
||||
self._openai_tools,
|
||||
self._available_functions,
|
||||
_,
|
||||
) = setup_native_tools(self.original_tools)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def execute(
|
||||
self,
|
||||
todo: TodoItem,
|
||||
context: StepExecutionContext,
|
||||
max_step_iterations: int = 15,
|
||||
step_timeout: int | None = None,
|
||||
) -> StepResult:
|
||||
"""Execute a single todo item using a multi-turn action loop.
|
||||
|
||||
Enforces the RPM limit, builds a fresh message list, then iterates
|
||||
LLM call → tool execution → observation until the LLM signals it is
|
||||
done (text answer) or max_step_iterations is reached. Never touches
|
||||
external AgentExecutor state.
|
||||
|
||||
Args:
|
||||
todo: The todo item to execute.
|
||||
context: Immutable context with task info and dependency results.
|
||||
max_step_iterations: Maximum LLM iterations in the multi-turn loop.
|
||||
step_timeout: Maximum wall-clock seconds for this step. None = no limit.
|
||||
|
||||
Returns:
|
||||
StepResult with the outcome.
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
tool_calls_made: list[str] = []
|
||||
|
||||
try:
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
messages = self._build_isolated_messages(todo, context)
|
||||
|
||||
if self._use_native_tools:
|
||||
result_text = self._execute_native(
|
||||
messages,
|
||||
tool_calls_made,
|
||||
max_step_iterations=max_step_iterations,
|
||||
step_timeout=step_timeout,
|
||||
start_time=start_time,
|
||||
)
|
||||
else:
|
||||
result_text = self._execute_text_parsed(
|
||||
messages,
|
||||
tool_calls_made,
|
||||
max_step_iterations=max_step_iterations,
|
||||
step_timeout=step_timeout,
|
||||
start_time=start_time,
|
||||
)
|
||||
self._validate_expected_tool_usage(todo, tool_calls_made)
|
||||
|
||||
elapsed = time.monotonic() - start_time
|
||||
return StepResult(
|
||||
success=True,
|
||||
result=result_text,
|
||||
tool_calls_made=tool_calls_made,
|
||||
execution_time=elapsed,
|
||||
)
|
||||
except Exception as e:
|
||||
elapsed = time.monotonic() - start_time
|
||||
return StepResult(
|
||||
success=False,
|
||||
result="",
|
||||
error=str(e),
|
||||
tool_calls_made=tool_calls_made,
|
||||
execution_time=elapsed,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal: Message building
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_isolated_messages(
|
||||
self, todo: TodoItem, context: StepExecutionContext
|
||||
) -> list[LLMMessage]:
|
||||
"""Build a fresh message list for this step's execution.
|
||||
|
||||
System prompt tells the LLM it is an Executor focused on one step.
|
||||
User prompt provides the step description, dependencies, and tools.
|
||||
"""
|
||||
system_prompt = self._build_system_prompt()
|
||||
user_prompt = self._build_user_prompt(todo, context)
|
||||
|
||||
return [
|
||||
format_message_for_llm(system_prompt, role="system"),
|
||||
format_message_for_llm(user_prompt, role="user"),
|
||||
]
|
||||
|
||||
def _build_system_prompt(self) -> str:
|
||||
"""Build the Executor's system prompt."""
|
||||
role = self.agent.role if self.agent else "Assistant"
|
||||
goal = self.agent.goal if self.agent else "Complete tasks efficiently"
|
||||
backstory = getattr(self.agent, "backstory", "") or ""
|
||||
|
||||
tools_section = ""
|
||||
if self.tools and not self._use_native_tools:
|
||||
tool_names = ", ".join(sanitize_tool_name(t.name) for t in self.tools)
|
||||
tools_section = self._i18n.retrieve(
|
||||
"planning", "step_executor_tools_section"
|
||||
).format(tool_names=tool_names)
|
||||
elif self.tools:
|
||||
tool_names = ", ".join(sanitize_tool_name(t.name) for t in self.tools)
|
||||
tools_section = f"\n\nAvailable tools: {tool_names}"
|
||||
|
||||
return self._i18n.retrieve("planning", "step_executor_system_prompt").format(
|
||||
role=role,
|
||||
backstory=backstory,
|
||||
goal=goal,
|
||||
tools_section=tools_section,
|
||||
)
|
||||
|
||||
def _build_user_prompt(self, todo: TodoItem, context: StepExecutionContext) -> str:
|
||||
"""Build the user prompt for this specific step."""
|
||||
parts: list[str] = []
|
||||
|
||||
# Include overall task context so the executor knows the full goal and
|
||||
# required output format/location — critical for knowing WHAT to produce.
|
||||
# We extract only the task body (not tool instructions or verification
|
||||
# sections) to avoid duplicating directives already in the system prompt.
|
||||
if context.task_description:
|
||||
task_section = extract_task_section(context.task_description)
|
||||
if task_section:
|
||||
parts.append(
|
||||
self._i18n.retrieve(
|
||||
"planning", "step_executor_task_context"
|
||||
).format(
|
||||
task_context=task_section,
|
||||
)
|
||||
)
|
||||
|
||||
parts.append(
|
||||
self._i18n.retrieve("planning", "step_executor_user_prompt").format(
|
||||
step_description=todo.description,
|
||||
)
|
||||
)
|
||||
|
||||
if todo.tool_to_use:
|
||||
parts.append(
|
||||
self._i18n.retrieve("planning", "step_executor_suggested_tool").format(
|
||||
tool_to_use=todo.tool_to_use,
|
||||
)
|
||||
)
|
||||
|
||||
# Include dependency results (final results only, no traces)
|
||||
if context.dependency_results:
|
||||
parts.append(
|
||||
self._i18n.retrieve("planning", "step_executor_context_header")
|
||||
)
|
||||
for step_num, result in sorted(context.dependency_results.items()):
|
||||
parts.append(
|
||||
self._i18n.retrieve(
|
||||
"planning", "step_executor_context_entry"
|
||||
).format(step_number=step_num, result=result)
|
||||
)
|
||||
|
||||
parts.append(self._i18n.retrieve("planning", "step_executor_complete_step"))
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal: Multi-turn execution loop
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _execute_text_parsed(
|
||||
self,
|
||||
messages: list[LLMMessage],
|
||||
tool_calls_made: list[str],
|
||||
max_step_iterations: int = 15,
|
||||
step_timeout: int | None = None,
|
||||
start_time: float | None = None,
|
||||
) -> str:
|
||||
"""Execute step using text-parsed tool calling with a multi-turn loop.
|
||||
|
||||
Iterates LLM call → tool execution → observation until the LLM
|
||||
produces a Final Answer or max_step_iterations is reached.
|
||||
This allows the agent to: run a command, see the output, adjust its
|
||||
approach, and run another command — all within a single plan step.
|
||||
"""
|
||||
use_stop_words = self.llm.supports_stop_words() if self.llm else False
|
||||
last_tool_result = ""
|
||||
|
||||
for _ in range(max_step_iterations):
|
||||
# Check step timeout
|
||||
if step_timeout and start_time:
|
||||
elapsed = time.monotonic() - start_time
|
||||
if elapsed >= step_timeout:
|
||||
return last_tool_result or f"Step timed out after {elapsed:.0f}s"
|
||||
answer = self.llm.call(
|
||||
messages,
|
||||
callbacks=self.callbacks,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
)
|
||||
|
||||
if not answer:
|
||||
raise ValueError("Empty response from LLM")
|
||||
|
||||
answer_str = str(answer)
|
||||
formatted = process_llm_response(answer_str, use_stop_words)
|
||||
|
||||
if isinstance(formatted, AgentFinish):
|
||||
return str(formatted.output)
|
||||
|
||||
if isinstance(formatted, AgentAction):
|
||||
tool_calls_made.append(formatted.tool)
|
||||
tool_result = self._execute_text_tool_with_events(formatted)
|
||||
last_tool_result = tool_result
|
||||
# Append the assistant's reasoning + action, then the observation.
|
||||
# _build_observation_message handles vision sentinels so the LLM
|
||||
# receives an image content block instead of raw base64 text.
|
||||
messages.append({"role": "assistant", "content": answer_str})
|
||||
messages.append(self._build_observation_message(tool_result))
|
||||
continue
|
||||
|
||||
# Raw text response with no Final Answer marker — treat as done
|
||||
return answer_str
|
||||
|
||||
# Max iterations reached — return the last tool result we accumulated
|
||||
return last_tool_result
|
||||
|
||||
def _execute_text_tool_with_events(self, formatted: AgentAction) -> str:
|
||||
"""Execute text-parsed tool calls with tool usage events."""
|
||||
args_dict = self._parse_tool_args(formatted.tool_input)
|
||||
agent_key = getattr(self.agent, "key", "unknown") if self.agent else "unknown"
|
||||
started_at = datetime.now()
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageStartedEvent(
|
||||
tool_name=formatted.tool,
|
||||
tool_args=args_dict,
|
||||
from_agent=self.agent,
|
||||
from_task=self.task,
|
||||
agent_key=agent_key,
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
fingerprint_context = {}
|
||||
if (
|
||||
self.agent
|
||||
and hasattr(self.agent, "security_config")
|
||||
and hasattr(self.agent.security_config, "fingerprint")
|
||||
):
|
||||
fingerprint_context = {
|
||||
"agent_fingerprint": str(self.agent.security_config.fingerprint)
|
||||
}
|
||||
|
||||
tool_result = execute_tool_and_check_finality(
|
||||
agent_action=formatted,
|
||||
fingerprint_context=fingerprint_context,
|
||||
tools=self.tools,
|
||||
i18n=self._i18n,
|
||||
agent_key=self.agent.key if self.agent else None,
|
||||
agent_role=self.agent.role if self.agent else None,
|
||||
tools_handler=self.tools_handler,
|
||||
task=self.task,
|
||||
agent=self.agent,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
crew=self.crew,
|
||||
)
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageErrorEvent(
|
||||
tool_name=formatted.tool,
|
||||
tool_args=args_dict,
|
||||
from_agent=self.agent,
|
||||
from_task=self.task,
|
||||
agent_key=agent_key,
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageFinishedEvent(
|
||||
output=str(tool_result.result),
|
||||
tool_name=formatted.tool,
|
||||
tool_args=args_dict,
|
||||
from_agent=self.agent,
|
||||
from_task=self.task,
|
||||
agent_key=agent_key,
|
||||
started_at=started_at,
|
||||
finished_at=datetime.now(),
|
||||
),
|
||||
)
|
||||
return str(tool_result.result)
|
||||
|
||||
def _parse_tool_args(self, tool_input: Any) -> dict[str, Any]:
|
||||
"""Parse tool args from the parser output into a dict payload for events."""
|
||||
if isinstance(tool_input, dict):
|
||||
return tool_input
|
||||
if isinstance(tool_input, str):
|
||||
stripped_input = tool_input.strip()
|
||||
if not stripped_input:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(stripped_input)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
return {"input": parsed}
|
||||
except json.JSONDecodeError:
|
||||
return {"input": stripped_input}
|
||||
return {"input": str(tool_input)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal: Vision support
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _parse_vision_sentinel(raw: str) -> tuple[str, str] | None:
|
||||
"""Parse a VISION_IMAGE sentinel into (media_type, base64_data), or None."""
|
||||
prefix = "VISION_IMAGE:"
|
||||
if not raw.startswith(prefix):
|
||||
return None
|
||||
rest = raw[len(prefix) :]
|
||||
sep = rest.find(":")
|
||||
if sep <= 0:
|
||||
return None
|
||||
return rest[:sep], rest[sep + 1 :]
|
||||
|
||||
@staticmethod
|
||||
def _build_observation_message(tool_result: str) -> LLMMessage:
|
||||
"""Build an observation message, converting vision sentinels to image blocks.
|
||||
|
||||
When a tool returns a VISION_IMAGE sentinel (e.g. from read_image),
|
||||
we build a multimodal content block so the LLM can actually *see*
|
||||
the image rather than receiving a wall of base64 text.
|
||||
|
||||
Uses the standard image_url / data-URI format so each LLM provider's
|
||||
SDK (OpenAI, LiteLLM, etc.) handles the provider-specific conversion.
|
||||
|
||||
Format: ``VISION_IMAGE:<media_type>:<base64_data>``
|
||||
"""
|
||||
parsed = StepExecutor._parse_vision_sentinel(tool_result)
|
||||
if parsed:
|
||||
media_type, b64_data = parsed
|
||||
return {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Observation: Here is the image:"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:{media_type};base64,{b64_data}",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
return {"role": "user", "content": f"Observation: {tool_result}"}
|
||||
|
||||
def _validate_expected_tool_usage(
|
||||
self,
|
||||
todo: TodoItem,
|
||||
tool_calls_made: list[str],
|
||||
) -> None:
|
||||
"""Fail step execution when a required tool is configured but not called."""
|
||||
expected_tool = getattr(todo, "tool_to_use", None)
|
||||
if not expected_tool:
|
||||
return
|
||||
expected_tool_name = sanitize_tool_name(expected_tool)
|
||||
available_tool_names = {
|
||||
sanitize_tool_name(tool.name)
|
||||
for tool in self.tools
|
||||
if getattr(tool, "name", "")
|
||||
} | set(self._available_functions.keys())
|
||||
if expected_tool_name not in available_tool_names:
|
||||
return
|
||||
called_names = {sanitize_tool_name(name) for name in tool_calls_made}
|
||||
if expected_tool_name not in called_names:
|
||||
raise ValueError(
|
||||
f"Expected tool '{expected_tool_name}' was not called "
|
||||
f"for step {todo.step_number}."
|
||||
)
|
||||
|
||||
def _execute_native(
|
||||
self,
|
||||
messages: list[LLMMessage],
|
||||
tool_calls_made: list[str],
|
||||
max_step_iterations: int = 15,
|
||||
step_timeout: int | None = None,
|
||||
start_time: float | None = None,
|
||||
) -> str:
|
||||
"""Execute step using native function calling with a multi-turn loop.
|
||||
|
||||
Iterates LLM call → tool execution → appended results until the LLM
|
||||
returns a text answer (no more tool calls) or max_step_iterations is
|
||||
reached. This lets the agent run a shell command, observe the output,
|
||||
correct mistakes, and issue follow-up commands — all within one step.
|
||||
"""
|
||||
accumulated_results: list[str] = []
|
||||
|
||||
for _ in range(max_step_iterations):
|
||||
# Check step timeout
|
||||
if step_timeout and start_time:
|
||||
elapsed = time.monotonic() - start_time
|
||||
if elapsed >= step_timeout:
|
||||
return (
|
||||
"\n\n".join(accumulated_results)
|
||||
if accumulated_results
|
||||
else f"Step timed out after {elapsed:.0f}s"
|
||||
)
|
||||
answer = self.llm.call(
|
||||
messages,
|
||||
tools=self._openai_tools,
|
||||
callbacks=self.callbacks,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
)
|
||||
|
||||
if not answer:
|
||||
raise ValueError("Empty response from LLM")
|
||||
|
||||
if isinstance(answer, BaseModel):
|
||||
return answer.model_dump_json()
|
||||
|
||||
if isinstance(answer, list) and answer and is_tool_call_list(answer):
|
||||
# _execute_native_tool_calls appends assistant + tool messages
|
||||
# to `messages` as a side-effect, so the next LLM call will
|
||||
# see the full conversation history including tool outputs.
|
||||
result = self._execute_native_tool_calls(
|
||||
answer, messages, tool_calls_made
|
||||
)
|
||||
accumulated_results.append(result)
|
||||
continue
|
||||
|
||||
# Text answer → LLM decided the step is done
|
||||
return str(answer)
|
||||
|
||||
# Max iterations reached — return everything we accumulated
|
||||
return "\n".join(filter(None, accumulated_results))
|
||||
|
||||
def _execute_native_tool_calls(
|
||||
self,
|
||||
tool_calls: list[Any],
|
||||
messages: list[LLMMessage],
|
||||
tool_calls_made: list[str],
|
||||
) -> str:
|
||||
"""Execute a batch of native tool calls and return their results.
|
||||
|
||||
Returns the result of the first tool marked result_as_answer if any,
|
||||
otherwise returns all tool results concatenated.
|
||||
"""
|
||||
assistant_message, _reports = build_tool_calls_assistant_message(tool_calls)
|
||||
if assistant_message:
|
||||
messages.append(assistant_message)
|
||||
|
||||
tool_results: list[str] = []
|
||||
for tool_call in tool_calls:
|
||||
call_result = execute_single_native_tool_call(
|
||||
tool_call,
|
||||
available_functions=self._available_functions,
|
||||
original_tools=self.original_tools,
|
||||
structured_tools=self.tools,
|
||||
tools_handler=self.tools_handler,
|
||||
agent=self.agent,
|
||||
task=self.task,
|
||||
crew=self.crew,
|
||||
event_source=self,
|
||||
printer=self._printer,
|
||||
verbose=bool(self.agent and self.agent.verbose),
|
||||
)
|
||||
|
||||
if call_result.func_name:
|
||||
tool_calls_made.append(call_result.func_name)
|
||||
|
||||
if call_result.result_as_answer:
|
||||
return str(call_result.result)
|
||||
|
||||
if call_result.tool_message:
|
||||
raw_content = call_result.tool_message.get("content", "")
|
||||
if isinstance(raw_content, str):
|
||||
parsed = self._parse_vision_sentinel(raw_content)
|
||||
if parsed:
|
||||
media_type, b64_data = parsed
|
||||
# Replace the sentinel with a standard image_url content block.
|
||||
# Each provider's _format_messages handles conversion to
|
||||
# its native format (e.g. Anthropic image blocks).
|
||||
modified: LLMMessage = cast(
|
||||
LLMMessage, dict(call_result.tool_message)
|
||||
)
|
||||
modified["content"] = [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:{media_type};base64,{b64_data}",
|
||||
},
|
||||
}
|
||||
]
|
||||
messages.append(modified)
|
||||
tool_results.append("[image]")
|
||||
else:
|
||||
messages.append(call_result.tool_message)
|
||||
if raw_content:
|
||||
tool_results.append(raw_content)
|
||||
else:
|
||||
messages.append(call_result.tool_message)
|
||||
if raw_content:
|
||||
tool_results.append(str(raw_content))
|
||||
|
||||
return "\n".join(tool_results) if tool_results else ""
|
||||
@@ -22,6 +22,7 @@ from crewai.cli.replay_from_task import replay_task_command
|
||||
from crewai.cli.reset_memories_command import reset_memories_command
|
||||
from crewai.cli.run_crew import run_crew
|
||||
from crewai.cli.settings.main import SettingsCommand
|
||||
from crewai.cli.shared.token_manager import TokenManager
|
||||
from crewai.cli.tools.main import ToolCommand
|
||||
from crewai.cli.train_crew import train_crew
|
||||
from crewai.cli.triggers.main import TriggersCommand
|
||||
@@ -34,7 +35,7 @@ from crewai.memory.storage.kickoff_task_outputs_storage import (
|
||||
|
||||
@click.group()
|
||||
@click.version_option(get_version("crewai"))
|
||||
def crewai():
|
||||
def crewai() -> None:
|
||||
"""Top-level command group for crewai."""
|
||||
|
||||
|
||||
@@ -45,7 +46,7 @@ def crewai():
|
||||
),
|
||||
)
|
||||
@click.argument("uv_args", nargs=-1, type=click.UNPROCESSED)
|
||||
def uv(uv_args):
|
||||
def uv(uv_args: tuple[str, ...]) -> None:
|
||||
"""A wrapper around uv commands that adds custom tool authentication through env vars."""
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
@@ -83,7 +84,9 @@ def uv(uv_args):
|
||||
@click.argument("name")
|
||||
@click.option("--provider", type=str, help="The provider to use for the crew")
|
||||
@click.option("--skip_provider", is_flag=True, help="Skip provider validation")
|
||||
def create(type, name, provider, skip_provider=False):
|
||||
def create(
|
||||
type: str, name: str, provider: str | None, skip_provider: bool = False
|
||||
) -> None:
|
||||
"""Create a new crew, or flow."""
|
||||
if type == "crew":
|
||||
create_crew(name, provider, skip_provider)
|
||||
@@ -97,7 +100,7 @@ def create(type, name, provider, skip_provider=False):
|
||||
@click.option(
|
||||
"--tools", is_flag=True, help="Show the installed version of crewai tools"
|
||||
)
|
||||
def version(tools):
|
||||
def version(tools: bool) -> None:
|
||||
"""Show the installed version of crewai."""
|
||||
try:
|
||||
crewai_version = get_version("crewai")
|
||||
@@ -128,7 +131,7 @@ def version(tools):
|
||||
default="trained_agents_data.pkl",
|
||||
help="Path to a custom file for training",
|
||||
)
|
||||
def train(n_iterations: int, filename: str):
|
||||
def train(n_iterations: int, filename: str) -> None:
|
||||
"""Train the crew."""
|
||||
click.echo(f"Training the Crew for {n_iterations} iterations")
|
||||
train_crew(n_iterations, filename)
|
||||
@@ -334,7 +337,7 @@ def memory(
|
||||
default="gpt-4o-mini",
|
||||
help="LLM Model to run the tests on the Crew. For now only accepting only OpenAI models.",
|
||||
)
|
||||
def test(n_iterations: int, model: str):
|
||||
def test(n_iterations: int, model: str) -> None:
|
||||
"""Test the crew and evaluate the results."""
|
||||
click.echo(f"Testing the crew for {n_iterations} iterations with model {model}")
|
||||
evaluate_crew(n_iterations, model)
|
||||
@@ -347,46 +350,62 @@ def test(n_iterations: int, model: str):
|
||||
)
|
||||
)
|
||||
@click.pass_context
|
||||
def install(context):
|
||||
def install(context: click.Context) -> None:
|
||||
"""Install the Crew."""
|
||||
install_crew(context.args)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
def run():
|
||||
def run() -> None:
|
||||
"""Run the Crew."""
|
||||
run_crew()
|
||||
|
||||
|
||||
@crewai.command()
|
||||
def update():
|
||||
def update() -> None:
|
||||
"""Update the pyproject.toml of the Crew project to use uv."""
|
||||
update_crew()
|
||||
|
||||
|
||||
@crewai.command()
|
||||
def login():
|
||||
def login() -> None:
|
||||
"""Sign Up/Login to CrewAI AMP."""
|
||||
Settings().clear_user_settings()
|
||||
AuthenticationCommand().login()
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@click.option(
|
||||
"--reset", is_flag=True, help="Also reset all CLI configuration to defaults"
|
||||
)
|
||||
def logout(reset: bool) -> None:
|
||||
"""Logout from CrewAI AMP."""
|
||||
settings = Settings()
|
||||
if reset:
|
||||
settings.reset()
|
||||
click.echo("Successfully logged out and reset all CLI configuration.")
|
||||
else:
|
||||
TokenManager().clear_tokens()
|
||||
settings.clear_user_settings()
|
||||
click.echo("Successfully logged out from CrewAI AMP.")
|
||||
|
||||
|
||||
# DEPLOY CREWAI+ COMMANDS
|
||||
@crewai.group()
|
||||
def deploy():
|
||||
def deploy() -> None:
|
||||
"""Deploy the Crew CLI group."""
|
||||
|
||||
|
||||
@deploy.command(name="create")
|
||||
@click.option("-y", "--yes", is_flag=True, help="Skip the confirmation prompt")
|
||||
def deploy_create(yes: bool):
|
||||
def deploy_create(yes: bool) -> None:
|
||||
"""Create a Crew deployment."""
|
||||
deploy_cmd = DeployCommand()
|
||||
deploy_cmd.create_crew(yes)
|
||||
|
||||
|
||||
@deploy.command(name="list")
|
||||
def deploy_list():
|
||||
def deploy_list() -> None:
|
||||
"""List all deployments."""
|
||||
deploy_cmd = DeployCommand()
|
||||
deploy_cmd.list_crews()
|
||||
@@ -394,7 +413,7 @@ def deploy_list():
|
||||
|
||||
@deploy.command(name="push")
|
||||
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||
def deploy_push(uuid: str | None):
|
||||
def deploy_push(uuid: str | None) -> None:
|
||||
"""Deploy the Crew."""
|
||||
deploy_cmd = DeployCommand()
|
||||
deploy_cmd.deploy(uuid=uuid)
|
||||
@@ -402,7 +421,7 @@ def deploy_push(uuid: str | None):
|
||||
|
||||
@deploy.command(name="status")
|
||||
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||
def deply_status(uuid: str | None):
|
||||
def deply_status(uuid: str | None) -> None:
|
||||
"""Get the status of a deployment."""
|
||||
deploy_cmd = DeployCommand()
|
||||
deploy_cmd.get_crew_status(uuid=uuid)
|
||||
@@ -410,7 +429,7 @@ def deply_status(uuid: str | None):
|
||||
|
||||
@deploy.command(name="logs")
|
||||
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||
def deploy_logs(uuid: str | None):
|
||||
def deploy_logs(uuid: str | None) -> None:
|
||||
"""Get the logs of a deployment."""
|
||||
deploy_cmd = DeployCommand()
|
||||
deploy_cmd.get_crew_logs(uuid=uuid)
|
||||
@@ -418,27 +437,27 @@ def deploy_logs(uuid: str | None):
|
||||
|
||||
@deploy.command(name="remove")
|
||||
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
|
||||
def deploy_remove(uuid: str | None):
|
||||
def deploy_remove(uuid: str | None) -> None:
|
||||
"""Remove a deployment."""
|
||||
deploy_cmd = DeployCommand()
|
||||
deploy_cmd.remove_crew(uuid=uuid)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def tool():
|
||||
def tool() -> None:
|
||||
"""Tool Repository related commands."""
|
||||
|
||||
|
||||
@tool.command(name="create")
|
||||
@click.argument("handle")
|
||||
def tool_create(handle: str):
|
||||
def tool_create(handle: str) -> None:
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.create(handle)
|
||||
|
||||
|
||||
@tool.command(name="install")
|
||||
@click.argument("handle")
|
||||
def tool_install(handle: str):
|
||||
def tool_install(handle: str) -> None:
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.login()
|
||||
tool_cmd.install(handle)
|
||||
@@ -454,26 +473,26 @@ def tool_install(handle: str):
|
||||
)
|
||||
@click.option("--public", "is_public", flag_value=True, default=False)
|
||||
@click.option("--private", "is_public", flag_value=False)
|
||||
def tool_publish(is_public: bool, force: bool):
|
||||
def tool_publish(is_public: bool, force: bool) -> None:
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.login()
|
||||
tool_cmd.publish(is_public, force)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def flow():
|
||||
def flow() -> None:
|
||||
"""Flow related commands."""
|
||||
|
||||
|
||||
@flow.command(name="kickoff")
|
||||
def flow_run():
|
||||
def flow_run() -> None:
|
||||
"""Kickoff the Flow."""
|
||||
click.echo("Running the Flow")
|
||||
kickoff_flow()
|
||||
|
||||
|
||||
@flow.command(name="plot")
|
||||
def flow_plot():
|
||||
def flow_plot() -> None:
|
||||
"""Plot the Flow."""
|
||||
click.echo("Plotting the Flow")
|
||||
plot_flow()
|
||||
@@ -481,19 +500,19 @@ def flow_plot():
|
||||
|
||||
@flow.command(name="add-crew")
|
||||
@click.argument("crew_name")
|
||||
def flow_add_crew(crew_name):
|
||||
def flow_add_crew(crew_name: str) -> None:
|
||||
"""Add a crew to an existing flow."""
|
||||
click.echo(f"Adding crew {crew_name} to the flow")
|
||||
add_crew_to_flow(crew_name)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def triggers():
|
||||
def triggers() -> None:
|
||||
"""Trigger related commands. Use 'crewai triggers list' to see available triggers, or 'crewai triggers run app_slug/trigger_slug' to execute."""
|
||||
|
||||
|
||||
@triggers.command(name="list")
|
||||
def triggers_list():
|
||||
def triggers_list() -> None:
|
||||
"""List all available triggers from integrations."""
|
||||
triggers_cmd = TriggersCommand()
|
||||
triggers_cmd.list_triggers()
|
||||
@@ -501,14 +520,14 @@ def triggers_list():
|
||||
|
||||
@triggers.command(name="run")
|
||||
@click.argument("trigger_path")
|
||||
def triggers_run(trigger_path: str):
|
||||
def triggers_run(trigger_path: str) -> None:
|
||||
"""Execute crew with trigger payload. Format: app_slug/trigger_slug"""
|
||||
triggers_cmd = TriggersCommand()
|
||||
triggers_cmd.execute_with_trigger(trigger_path)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
def chat():
|
||||
def chat() -> None:
|
||||
"""
|
||||
Start a conversation with the Crew, collecting user-supplied inputs,
|
||||
and using the Chat LLM to generate responses.
|
||||
@@ -521,12 +540,12 @@ def chat():
|
||||
|
||||
|
||||
@crewai.group(invoke_without_command=True)
|
||||
def org():
|
||||
def org() -> None:
|
||||
"""Organization management commands."""
|
||||
|
||||
|
||||
@org.command("list")
|
||||
def org_list():
|
||||
def org_list() -> None:
|
||||
"""List available organizations."""
|
||||
org_command = OrganizationCommand()
|
||||
org_command.list()
|
||||
@@ -534,39 +553,39 @@ def org_list():
|
||||
|
||||
@org.command()
|
||||
@click.argument("id")
|
||||
def switch(id):
|
||||
def switch(id: str) -> None:
|
||||
"""Switch to a specific organization."""
|
||||
org_command = OrganizationCommand()
|
||||
org_command.switch(id)
|
||||
|
||||
|
||||
@org.command()
|
||||
def current():
|
||||
def current() -> None:
|
||||
"""Show current organization when 'crewai org' is called without subcommands."""
|
||||
org_command = OrganizationCommand()
|
||||
org_command.current()
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def enterprise():
|
||||
def enterprise() -> None:
|
||||
"""Enterprise Configuration commands."""
|
||||
|
||||
|
||||
@enterprise.command("configure")
|
||||
@click.argument("enterprise_url")
|
||||
def enterprise_configure(enterprise_url: str):
|
||||
def enterprise_configure(enterprise_url: str) -> None:
|
||||
"""Configure CrewAI AMP OAuth2 settings from the provided Enterprise URL."""
|
||||
enterprise_command = EnterpriseConfigureCommand()
|
||||
enterprise_command.configure(enterprise_url)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def config():
|
||||
def config() -> None:
|
||||
"""CLI Configuration commands."""
|
||||
|
||||
|
||||
@config.command("list")
|
||||
def config_list():
|
||||
def config_list() -> None:
|
||||
"""List all CLI configuration parameters."""
|
||||
config_command = SettingsCommand()
|
||||
config_command.list()
|
||||
@@ -575,26 +594,26 @@ def config_list():
|
||||
@config.command("set")
|
||||
@click.argument("key")
|
||||
@click.argument("value")
|
||||
def config_set(key: str, value: str):
|
||||
def config_set(key: str, value: str) -> None:
|
||||
"""Set a CLI configuration parameter."""
|
||||
config_command = SettingsCommand()
|
||||
config_command.set(key, value)
|
||||
|
||||
|
||||
@config.command("reset")
|
||||
def config_reset():
|
||||
def config_reset() -> None:
|
||||
"""Reset all CLI configuration parameters to default values."""
|
||||
config_command = SettingsCommand()
|
||||
config_command.reset_all_settings()
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def env():
|
||||
def env() -> None:
|
||||
"""Environment variable commands."""
|
||||
|
||||
|
||||
@env.command("view")
|
||||
def env_view():
|
||||
def env_view() -> None:
|
||||
"""View tracing-related environment variables."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -672,12 +691,12 @@ def env_view():
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def traces():
|
||||
def traces() -> None:
|
||||
"""Trace collection management commands."""
|
||||
|
||||
|
||||
@traces.command("enable")
|
||||
def traces_enable():
|
||||
def traces_enable() -> None:
|
||||
"""Enable trace collection for crew/flow executions."""
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -700,7 +719,7 @@ def traces_enable():
|
||||
|
||||
|
||||
@traces.command("disable")
|
||||
def traces_disable():
|
||||
def traces_disable() -> None:
|
||||
"""Disable trace collection for crew/flow executions."""
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -723,7 +742,7 @@ def traces_disable():
|
||||
|
||||
|
||||
@traces.command("status")
|
||||
def traces_status():
|
||||
def traces_status() -> None:
|
||||
"""Show current trace collection status."""
|
||||
import os
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import click
|
||||
from crewai.telemetry import Telemetry
|
||||
|
||||
|
||||
def create_flow(name):
|
||||
def create_flow(name: str) -> None:
|
||||
"""Create a new flow."""
|
||||
folder_name = name.replace(" ", "_").replace("-", "_").lower()
|
||||
class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "")
|
||||
@@ -49,7 +49,7 @@ def create_flow(name):
|
||||
"poem_crew",
|
||||
]
|
||||
|
||||
def process_file(src_file, dst_file):
|
||||
def process_file(src_file: Path, dst_file: Path) -> None:
|
||||
if src_file.suffix in [".pyc", ".pyo", ".pyd"]:
|
||||
return
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
A class to handle deployment-related operations for CrewAI projects.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize the DeployCommand with project name and API client.
|
||||
"""
|
||||
@@ -67,7 +67,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
Args:
|
||||
uuid (Optional[str]): The UUID of the crew to deploy.
|
||||
"""
|
||||
self._start_deployment_span = self._telemetry.start_deployment_span(uuid)
|
||||
self._telemetry.start_deployment_span(uuid)
|
||||
console.print("Starting deployment...", style="bold blue")
|
||||
if uuid:
|
||||
response = self.plus_api_client.deploy_by_uuid(uuid)
|
||||
@@ -84,9 +84,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
"""
|
||||
Create a new crew deployment.
|
||||
"""
|
||||
self._create_crew_deployment_span = (
|
||||
self._telemetry.create_crew_deployment_span()
|
||||
)
|
||||
self._telemetry.create_crew_deployment_span()
|
||||
console.print("Creating deployment...", style="bold blue")
|
||||
env_vars = fetch_and_json_env_file()
|
||||
|
||||
@@ -236,7 +234,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
uuid (Optional[str]): The UUID of the crew to get logs for.
|
||||
log_type (str): The type of logs to retrieve (default: "deployment").
|
||||
"""
|
||||
self._get_crew_logs_span = self._telemetry.get_crew_logs_span(uuid, log_type)
|
||||
self._telemetry.get_crew_logs_span(uuid, log_type)
|
||||
console.print(f"Fetching {log_type} logs...", style="bold blue")
|
||||
|
||||
if uuid:
|
||||
@@ -257,7 +255,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
Args:
|
||||
uuid (Optional[str]): The UUID of the crew to remove.
|
||||
"""
|
||||
self._remove_crew_span = self._telemetry.remove_crew_span(uuid)
|
||||
self._telemetry.remove_crew_span(uuid)
|
||||
console.print("Removing deployment...", style="bold blue")
|
||||
|
||||
if uuid:
|
||||
|
||||
@@ -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.10.2rc2"
|
||||
"crewai[tools]==1.11.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.2rc2"
|
||||
"crewai[tools]==1.11.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.2rc2"
|
||||
"crewai[tools]==1.11.0"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -16,7 +16,7 @@ class TriggersCommand(BaseCommand, PlusAPIMixin):
|
||||
A class to handle trigger-related operations for CrewAI projects.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
BaseCommand.__init__(self)
|
||||
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from typing_extensions import Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai_files import FileInput
|
||||
from opentelemetry.trace import Span
|
||||
|
||||
try:
|
||||
from crewai_files import get_supported_content_types
|
||||
@@ -83,6 +84,8 @@ from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||
from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.memory.memory_scope import MemoryScope, MemorySlice
|
||||
from crewai.memory.unified_memory import Memory
|
||||
from crewai.process import Process
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.rag.types import SearchResult
|
||||
@@ -94,10 +97,12 @@ from crewai.tasks.task_output import TaskOutput
|
||||
from crewai.tools.agent_tools.agent_tools import AgentTools
|
||||
from crewai.tools.agent_tools.read_file_tool import ReadFileTool
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.types.callback import SerializableCallable
|
||||
from crewai.types.streaming import CrewStreamingOutput
|
||||
from crewai.types.usage_metrics import UsageMetrics
|
||||
from crewai.utilities.constants import NOT_SPECIFIED, TRAINING_DATA_FILE
|
||||
from crewai.utilities.crew.models import CrewContext
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.evaluators.crew_evaluator_handler import CrewEvaluator
|
||||
from crewai.utilities.evaluators.task_evaluator import TaskEvaluator
|
||||
from crewai.utilities.file_handler import FileHandler
|
||||
@@ -165,12 +170,12 @@ class Crew(FlowTrackable, BaseModel):
|
||||
"""
|
||||
|
||||
__hash__ = object.__hash__
|
||||
_execution_span: Any = PrivateAttr()
|
||||
_execution_span: Span | None = PrivateAttr()
|
||||
_rpm_controller: RPMController = PrivateAttr()
|
||||
_logger: Logger = PrivateAttr()
|
||||
_file_handler: FileHandler = PrivateAttr()
|
||||
_cache_handler: InstanceOf[CacheHandler] = PrivateAttr(default_factory=CacheHandler)
|
||||
_memory: Any = PrivateAttr(default=None) # Unified Memory | MemoryScope
|
||||
_memory: Memory | MemoryScope | MemorySlice | None = PrivateAttr(default=None)
|
||||
_train: bool | None = PrivateAttr(default=False)
|
||||
_train_iteration: int | None = PrivateAttr()
|
||||
_inputs: dict[str, Any] | None = PrivateAttr(default=None)
|
||||
@@ -188,7 +193,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
agents: list[BaseAgent] = Field(default_factory=list)
|
||||
process: Process = Field(default=Process.sequential)
|
||||
verbose: bool = Field(default=False)
|
||||
memory: bool | Any = Field(
|
||||
memory: bool | Memory | MemoryScope | MemorySlice | None = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Enable crew memory. Pass True for default Memory(), "
|
||||
@@ -203,36 +208,34 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=None,
|
||||
description="Metrics for the LLM usage during all tasks execution.",
|
||||
)
|
||||
manager_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
manager_llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
manager_agent: BaseAgent | None = Field(
|
||||
description="Custom agent that will be used as manager.", default=None
|
||||
)
|
||||
function_calling_llm: str | InstanceOf[LLM] | Any | None = Field(
|
||||
function_calling_llm: str | InstanceOf[LLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
config: Json[dict[str, Any]] | dict[str, Any] | None = Field(default=None)
|
||||
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
||||
share_crew: bool | None = Field(default=False)
|
||||
step_callback: Any | None = Field(
|
||||
step_callback: SerializableCallable | None = Field(
|
||||
default=None,
|
||||
description="Callback to be executed after each step for all agents execution.",
|
||||
)
|
||||
task_callback: Any | None = Field(
|
||||
task_callback: SerializableCallable | None = Field(
|
||||
default=None,
|
||||
description="Callback to be executed after each task for all agents execution.",
|
||||
)
|
||||
before_kickoff_callbacks: list[
|
||||
Callable[[dict[str, Any] | None], dict[str, Any] | None]
|
||||
] = Field(
|
||||
before_kickoff_callbacks: list[SerializableCallable] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"List of callbacks to be executed before crew kickoff. "
|
||||
"It may be used to adjust inputs before the crew is executed."
|
||||
),
|
||||
)
|
||||
after_kickoff_callbacks: list[Callable[[CrewOutput], CrewOutput]] = Field(
|
||||
after_kickoff_callbacks: list[SerializableCallable] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"List of callbacks to be executed after crew kickoff. "
|
||||
@@ -348,7 +351,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
self._file_handler = FileHandler(self.output_log_file)
|
||||
self._rpm_controller = RPMController(max_rpm=self.max_rpm, logger=self._logger)
|
||||
if self.function_calling_llm and not isinstance(self.function_calling_llm, LLM):
|
||||
self.function_calling_llm = create_llm(self.function_calling_llm)
|
||||
self.function_calling_llm = create_llm(self.function_calling_llm) # type: ignore[assignment]
|
||||
|
||||
return self
|
||||
|
||||
@@ -362,7 +365,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
if self.embedder is not None:
|
||||
from crewai.rag.embeddings.factory import build_embedder
|
||||
|
||||
embedder = build_embedder(self.embedder)
|
||||
embedder = build_embedder(self.embedder) # type: ignore[arg-type]
|
||||
self._memory = Memory(embedder=embedder)
|
||||
elif self.memory:
|
||||
# User passed a Memory / MemoryScope / MemorySlice instance
|
||||
@@ -679,6 +682,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
Returns:
|
||||
CrewOutput or CrewStreamingOutput if streaming is enabled.
|
||||
"""
|
||||
get_env_context()
|
||||
if self.stream:
|
||||
enable_agent_streaming(self.agents)
|
||||
ctx = StreamingContext()
|
||||
|
||||
@@ -34,6 +34,12 @@ from crewai.events.types.crew_events import (
|
||||
CrewTrainFailedEvent,
|
||||
CrewTrainStartedEvent,
|
||||
)
|
||||
from crewai.events.types.env_events import (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowFinishedEvent,
|
||||
@@ -75,6 +81,14 @@ from crewai.events.types.mcp_events import (
|
||||
MCPToolExecutionFailedEvent,
|
||||
MCPToolExecutionStartedEvent,
|
||||
)
|
||||
from crewai.events.types.observation_events import (
|
||||
GoalAchievedEarlyEvent,
|
||||
PlanRefinementEvent,
|
||||
PlanReplanTriggeredEvent,
|
||||
StepObservationCompletedEvent,
|
||||
StepObservationFailedEvent,
|
||||
StepObservationStartedEvent,
|
||||
)
|
||||
from crewai.events.types.reasoning_events import (
|
||||
AgentReasoningCompletedEvent,
|
||||
AgentReasoningFailedEvent,
|
||||
@@ -135,6 +149,23 @@ class EventListener(BaseEventListener):
|
||||
# ----------- CREW EVENTS -----------
|
||||
|
||||
def setup_listeners(self, crewai_event_bus: CrewAIEventsBus) -> None:
|
||||
|
||||
@crewai_event_bus.on(CCEnvEvent)
|
||||
def on_cc_env(_: Any, event: CCEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(CodexEnvEvent)
|
||||
def on_codex_env(_: Any, event: CodexEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(CursorEnvEvent)
|
||||
def on_cursor_env(_: Any, event: CursorEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(DefaultEnvEvent)
|
||||
def on_default_env(_: Any, event: DefaultEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
self.formatter.handle_crew_started(event.crew_name or "Crew", source.id)
|
||||
@@ -535,6 +566,64 @@ class EventListener(BaseEventListener):
|
||||
event.error,
|
||||
)
|
||||
|
||||
# ----------- OBSERVATION EVENTS (Plan-and-Execute) -----------
|
||||
|
||||
@crewai_event_bus.on(StepObservationStartedEvent)
|
||||
def on_step_observation_started(
|
||||
_: Any, event: StepObservationStartedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_observation_started(
|
||||
event.agent_role,
|
||||
event.step_number,
|
||||
event.step_description,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(StepObservationCompletedEvent)
|
||||
def on_step_observation_completed(
|
||||
_: Any, event: StepObservationCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_observation_completed(
|
||||
event.agent_role,
|
||||
event.step_number,
|
||||
event.step_completed_successfully,
|
||||
event.remaining_plan_still_valid,
|
||||
event.key_information_learned,
|
||||
event.needs_full_replan,
|
||||
event.goal_already_achieved,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(StepObservationFailedEvent)
|
||||
def on_step_observation_failed(
|
||||
_: Any, event: StepObservationFailedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_observation_failed(
|
||||
event.step_number,
|
||||
event.error,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(PlanRefinementEvent)
|
||||
def on_plan_refinement(_: Any, event: PlanRefinementEvent) -> None:
|
||||
self.formatter.handle_plan_refinement(
|
||||
event.step_number,
|
||||
event.refined_step_count,
|
||||
event.refinements,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(PlanReplanTriggeredEvent)
|
||||
def on_plan_replan_triggered(_: Any, event: PlanReplanTriggeredEvent) -> None:
|
||||
self.formatter.handle_plan_replan(
|
||||
event.replan_reason,
|
||||
event.replan_count,
|
||||
event.completed_steps_preserved,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(GoalAchievedEarlyEvent)
|
||||
def on_goal_achieved_early(_: Any, event: GoalAchievedEarlyEvent) -> None:
|
||||
self.formatter.handle_goal_achieved_early(
|
||||
event.steps_completed,
|
||||
event.steps_remaining,
|
||||
)
|
||||
|
||||
# ----------- AGENT LOGGING EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(AgentLogsStartedEvent)
|
||||
|
||||
@@ -58,6 +58,12 @@ from crewai.events.types.crew_events import (
|
||||
CrewKickoffFailedEvent,
|
||||
CrewKickoffStartedEvent,
|
||||
)
|
||||
from crewai.events.types.env_events import (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowFinishedEvent,
|
||||
@@ -93,6 +99,14 @@ from crewai.events.types.memory_events import (
|
||||
MemorySaveFailedEvent,
|
||||
MemorySaveStartedEvent,
|
||||
)
|
||||
from crewai.events.types.observation_events import (
|
||||
GoalAchievedEarlyEvent,
|
||||
PlanRefinementEvent,
|
||||
PlanReplanTriggeredEvent,
|
||||
StepObservationCompletedEvent,
|
||||
StepObservationFailedEvent,
|
||||
StepObservationStartedEvent,
|
||||
)
|
||||
from crewai.events.types.reasoning_events import (
|
||||
AgentReasoningCompletedEvent,
|
||||
AgentReasoningFailedEvent,
|
||||
@@ -184,6 +198,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
if self._listeners_setup:
|
||||
return
|
||||
|
||||
self._register_env_event_handlers(crewai_event_bus)
|
||||
self._register_flow_event_handlers(crewai_event_bus)
|
||||
self._register_context_event_handlers(crewai_event_bus)
|
||||
self._register_action_event_handlers(crewai_event_bus)
|
||||
@@ -192,6 +207,25 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
self._listeners_setup = True
|
||||
|
||||
def _register_env_event_handlers(self, event_bus: CrewAIEventsBus) -> None:
|
||||
"""Register handlers for environment context events."""
|
||||
|
||||
@event_bus.on(CCEnvEvent)
|
||||
def on_cc_env(source: Any, event: CCEnvEvent) -> None:
|
||||
self._handle_action_event("cc_env", source, event)
|
||||
|
||||
@event_bus.on(CodexEnvEvent)
|
||||
def on_codex_env(source: Any, event: CodexEnvEvent) -> None:
|
||||
self._handle_action_event("codex_env", source, event)
|
||||
|
||||
@event_bus.on(CursorEnvEvent)
|
||||
def on_cursor_env(source: Any, event: CursorEnvEvent) -> None:
|
||||
self._handle_action_event("cursor_env", source, event)
|
||||
|
||||
@event_bus.on(DefaultEnvEvent)
|
||||
def on_default_env(source: Any, event: DefaultEnvEvent) -> None:
|
||||
self._handle_action_event("default_env", source, event)
|
||||
|
||||
def _register_flow_event_handlers(self, event_bus: CrewAIEventsBus) -> None:
|
||||
"""Register handlers for flow events."""
|
||||
|
||||
@@ -437,6 +471,39 @@ class TraceCollectionListener(BaseEventListener):
|
||||
) -> None:
|
||||
self._handle_action_event("agent_reasoning_failed", source, event)
|
||||
|
||||
# Observation events (Plan-and-Execute)
|
||||
@event_bus.on(StepObservationStartedEvent)
|
||||
def on_step_observation_started(
|
||||
source: Any, event: StepObservationStartedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("step_observation_started", source, event)
|
||||
|
||||
@event_bus.on(StepObservationCompletedEvent)
|
||||
def on_step_observation_completed(
|
||||
source: Any, event: StepObservationCompletedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("step_observation_completed", source, event)
|
||||
|
||||
@event_bus.on(StepObservationFailedEvent)
|
||||
def on_step_observation_failed(
|
||||
source: Any, event: StepObservationFailedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("step_observation_failed", source, event)
|
||||
|
||||
@event_bus.on(PlanRefinementEvent)
|
||||
def on_plan_refinement(source: Any, event: PlanRefinementEvent) -> None:
|
||||
self._handle_action_event("plan_refinement", source, event)
|
||||
|
||||
@event_bus.on(PlanReplanTriggeredEvent)
|
||||
def on_plan_replan_triggered(
|
||||
source: Any, event: PlanReplanTriggeredEvent
|
||||
) -> None:
|
||||
self._handle_action_event("plan_replan_triggered", source, event)
|
||||
|
||||
@event_bus.on(GoalAchievedEarlyEvent)
|
||||
def on_goal_achieved_early(source: Any, event: GoalAchievedEarlyEvent) -> None:
|
||||
self._handle_action_event("goal_achieved_early", source, event)
|
||||
|
||||
@event_bus.on(KnowledgeRetrievalStartedEvent)
|
||||
def on_knowledge_retrieval_started(
|
||||
source: Any, event: KnowledgeRetrievalStartedEvent
|
||||
|
||||
36
lib/crewai/src/crewai/events/types/env_events.py
Normal file
36
lib/crewai/src/crewai/events/types/env_events.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import Field, TypeAdapter
|
||||
|
||||
from crewai.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class CCEnvEvent(BaseEvent):
|
||||
type: Literal["cc_env"] = "cc_env"
|
||||
|
||||
|
||||
class CodexEnvEvent(BaseEvent):
|
||||
type: Literal["codex_env"] = "codex_env"
|
||||
|
||||
|
||||
class CursorEnvEvent(BaseEvent):
|
||||
type: Literal["cursor_env"] = "cursor_env"
|
||||
|
||||
|
||||
class DefaultEnvEvent(BaseEvent):
|
||||
type: Literal["default_env"] = "default_env"
|
||||
|
||||
|
||||
EnvContextEvent = Annotated[
|
||||
CCEnvEvent | CodexEnvEvent | CursorEnvEvent | DefaultEnvEvent,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
env_context_event_adapter: TypeAdapter[EnvContextEvent] = TypeAdapter(EnvContextEvent)
|
||||
|
||||
ENV_CONTEXT_EVENT_TYPES: tuple[type[BaseEvent], ...] = (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
99
lib/crewai/src/crewai/events/types/observation_events.py
Normal file
99
lib/crewai/src/crewai/events/types/observation_events.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Observation events for the Plan-and-Execute architecture.
|
||||
|
||||
Emitted during the Observation phase (PLAN-AND-ACT Section 3.3) when the
|
||||
PlannerObserver analyzes step execution results and decides on plan
|
||||
continuation, refinement, or replanning.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from crewai.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class ObservationEvent(BaseEvent):
|
||||
"""Base event for observation phase events."""
|
||||
|
||||
type: str
|
||||
agent_role: str
|
||||
step_number: int
|
||||
step_description: str = ""
|
||||
from_task: Any | None = None
|
||||
from_agent: Any | None = None
|
||||
|
||||
def __init__(self, **data: Any) -> None:
|
||||
super().__init__(**data)
|
||||
self._set_task_params(data)
|
||||
self._set_agent_params(data)
|
||||
|
||||
|
||||
class StepObservationStartedEvent(ObservationEvent):
|
||||
"""Emitted when the Planner begins observing a step's result.
|
||||
|
||||
Fires after every step execution, before the observation LLM call.
|
||||
"""
|
||||
|
||||
type: str = "step_observation_started"
|
||||
|
||||
|
||||
class StepObservationCompletedEvent(ObservationEvent):
|
||||
"""Emitted when the Planner finishes observing a step's result.
|
||||
|
||||
Contains the full observation analysis: what was learned, whether
|
||||
the plan is still valid, and what action to take next.
|
||||
"""
|
||||
|
||||
type: str = "step_observation_completed"
|
||||
step_completed_successfully: bool = True
|
||||
key_information_learned: str = ""
|
||||
remaining_plan_still_valid: bool = True
|
||||
needs_full_replan: bool = False
|
||||
replan_reason: str | None = None
|
||||
goal_already_achieved: bool = False
|
||||
suggested_refinements: list[str] | None = None
|
||||
|
||||
|
||||
class StepObservationFailedEvent(ObservationEvent):
|
||||
"""Emitted when the observation LLM call itself fails.
|
||||
|
||||
The system defaults to continuing the plan when this happens,
|
||||
but the event allows monitoring/alerting on observation failures.
|
||||
"""
|
||||
|
||||
type: str = "step_observation_failed"
|
||||
error: str = ""
|
||||
|
||||
|
||||
class PlanRefinementEvent(ObservationEvent):
|
||||
"""Emitted when the Planner refines upcoming step descriptions.
|
||||
|
||||
This is the lightweight refinement path — no full replan, just
|
||||
sharpening pending todo descriptions based on new information.
|
||||
"""
|
||||
|
||||
type: str = "plan_refinement"
|
||||
refined_step_count: int = 0
|
||||
refinements: list[str] | None = None
|
||||
|
||||
|
||||
class PlanReplanTriggeredEvent(ObservationEvent):
|
||||
"""Emitted when the Planner triggers a full replan.
|
||||
|
||||
The remaining plan was deemed fundamentally wrong and will be
|
||||
regenerated from scratch, preserving completed step results.
|
||||
"""
|
||||
|
||||
type: str = "plan_replan_triggered"
|
||||
replan_reason: str = ""
|
||||
replan_count: int = 0
|
||||
completed_steps_preserved: int = 0
|
||||
|
||||
|
||||
class GoalAchievedEarlyEvent(ObservationEvent):
|
||||
"""Emitted when the Planner detects the goal was achieved early.
|
||||
|
||||
Remaining steps will be skipped and execution will finalize.
|
||||
"""
|
||||
|
||||
type: str = "goal_achieved_early"
|
||||
steps_remaining: int = 0
|
||||
steps_completed: int = 0
|
||||
@@ -9,7 +9,7 @@ class ReasoningEvent(BaseEvent):
|
||||
type: str
|
||||
attempt: int = 1
|
||||
agent_role: str
|
||||
task_id: str
|
||||
task_id: str | None = None
|
||||
task_name: str | None = None
|
||||
from_task: Any | None = None
|
||||
agent_id: str | None = None
|
||||
|
||||
@@ -941,6 +941,152 @@ To enable tracing, do any one of these:
|
||||
)
|
||||
self.print_panel(error_content, "❌ Reasoning Error", "red")
|
||||
|
||||
# ----------- OBSERVATION EVENTS (Plan-and-Execute) -----------
|
||||
|
||||
def handle_observation_started(
|
||||
self,
|
||||
agent_role: str,
|
||||
step_number: int,
|
||||
step_description: str,
|
||||
) -> None:
|
||||
"""Handle step observation started event."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
content = Text()
|
||||
content.append("Observation Started\n", style="cyan bold")
|
||||
content.append("Agent: ", style="white")
|
||||
content.append(f"{agent_role}\n", style="cyan")
|
||||
content.append("Step: ", style="white")
|
||||
content.append(f"{step_number}\n", style="cyan")
|
||||
if step_description:
|
||||
desc_preview = step_description[:80] + (
|
||||
"..." if len(step_description) > 80 else ""
|
||||
)
|
||||
content.append("Description: ", style="white")
|
||||
content.append(f"{desc_preview}\n", style="cyan")
|
||||
|
||||
self.print_panel(content, "🔍 Observing Step Result", "cyan")
|
||||
|
||||
def handle_observation_completed(
|
||||
self,
|
||||
agent_role: str,
|
||||
step_number: int,
|
||||
step_completed: bool,
|
||||
plan_valid: bool,
|
||||
key_info: str,
|
||||
needs_replan: bool,
|
||||
goal_achieved: bool,
|
||||
) -> None:
|
||||
"""Handle step observation completed event."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
if goal_achieved:
|
||||
style = "green"
|
||||
status = "Goal Achieved Early"
|
||||
elif needs_replan:
|
||||
style = "yellow"
|
||||
status = "Replan Needed"
|
||||
elif plan_valid:
|
||||
style = "green"
|
||||
status = "Plan Valid — Continue"
|
||||
else:
|
||||
style = "red"
|
||||
status = "Step Failed"
|
||||
|
||||
content = Text()
|
||||
content.append("Observation Complete\n", style=f"{style} bold")
|
||||
content.append("Step: ", style="white")
|
||||
content.append(f"{step_number}\n", style=style)
|
||||
content.append("Status: ", style="white")
|
||||
content.append(f"{status}\n", style=style)
|
||||
if key_info:
|
||||
info_preview = key_info[:120] + ("..." if len(key_info) > 120 else "")
|
||||
content.append("Learned: ", style="white")
|
||||
content.append(f"{info_preview}\n", style=style)
|
||||
|
||||
self.print_panel(content, "🔍 Observation Result", style)
|
||||
|
||||
def handle_observation_failed(
|
||||
self,
|
||||
step_number: int,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Handle step observation failure event."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
error_content = self.create_status_content(
|
||||
"Observation Failed",
|
||||
"Error",
|
||||
"red",
|
||||
Step=str(step_number),
|
||||
Error=error,
|
||||
)
|
||||
self.print_panel(error_content, "❌ Observation Error", "red")
|
||||
|
||||
def handle_plan_refinement(
|
||||
self,
|
||||
step_number: int,
|
||||
refined_count: int,
|
||||
refinements: list[str] | None,
|
||||
) -> None:
|
||||
"""Handle plan refinement event."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
content = Text()
|
||||
content.append("Plan Refined\n", style="cyan bold")
|
||||
content.append("After Step: ", style="white")
|
||||
content.append(f"{step_number}\n", style="cyan")
|
||||
content.append("Steps Updated: ", style="white")
|
||||
content.append(f"{refined_count}\n", style="cyan")
|
||||
if refinements:
|
||||
for r in refinements[:3]:
|
||||
content.append(f" • {r[:80]}\n", style="white")
|
||||
|
||||
self.print_panel(content, "✏️ Plan Refinement", "cyan")
|
||||
|
||||
def handle_plan_replan(
|
||||
self,
|
||||
reason: str,
|
||||
replan_count: int,
|
||||
preserved_count: int,
|
||||
) -> None:
|
||||
"""Handle plan replan triggered event."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
content = Text()
|
||||
content.append("Full Replan Triggered\n", style="yellow bold")
|
||||
content.append("Reason: ", style="white")
|
||||
content.append(f"{reason}\n", style="yellow")
|
||||
content.append("Replan #: ", style="white")
|
||||
content.append(f"{replan_count}\n", style="yellow")
|
||||
content.append("Preserved Steps: ", style="white")
|
||||
content.append(f"{preserved_count}\n", style="yellow")
|
||||
|
||||
self.print_panel(content, "🔄 Dynamic Replan", "yellow")
|
||||
|
||||
def handle_goal_achieved_early(
|
||||
self,
|
||||
steps_completed: int,
|
||||
steps_remaining: int,
|
||||
) -> None:
|
||||
"""Handle goal achieved early event."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
content = Text()
|
||||
content.append("Goal Achieved Early!\n", style="green bold")
|
||||
content.append("Completed: ", style="white")
|
||||
content.append(f"{steps_completed} steps\n", style="green")
|
||||
content.append("Skipped: ", style="white")
|
||||
content.append(f"{steps_remaining} remaining steps\n", style="green")
|
||||
|
||||
self.print_panel(content, "🎯 Early Goal Achievement", "green")
|
||||
|
||||
# ----------- AGENT LOGGING EVENTS -----------
|
||||
|
||||
def handle_agent_logs_started(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,7 @@ from crewai.flow.flow_wrappers import (
|
||||
SimpleFlowCondition,
|
||||
StartMethod,
|
||||
)
|
||||
from crewai.flow.input_provider import InputProvider
|
||||
from crewai.flow.persistence.base import FlowPersistence
|
||||
from crewai.flow.types import (
|
||||
FlowExecutionData,
|
||||
@@ -99,6 +100,8 @@ from crewai.flow.utils import (
|
||||
is_flow_method_name,
|
||||
is_simple_flow_condition,
|
||||
)
|
||||
from crewai.memory.memory_scope import MemoryScope, MemorySlice
|
||||
from crewai.memory.unified_memory import Memory
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -110,6 +113,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from crewai.flow.visualization import build_flow_structure, render_interactive
|
||||
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.streaming import (
|
||||
TaskInfo,
|
||||
create_async_chunk_generator,
|
||||
@@ -500,7 +504,7 @@ class LockedListProxy(list, Generic[T]): # type: ignore[type-arg]
|
||||
|
||||
def index(
|
||||
self, value: T, start: SupportsIndex = 0, stop: SupportsIndex | None = None
|
||||
) -> int: # type: ignore[override]
|
||||
) -> int:
|
||||
if stop is None:
|
||||
return self._list.index(value, start)
|
||||
return self._list.index(value, start, stop)
|
||||
@@ -519,13 +523,13 @@ class LockedListProxy(list, Generic[T]): # type: ignore[type-arg]
|
||||
def copy(self) -> list[T]:
|
||||
return self._list.copy()
|
||||
|
||||
def __add__(self, other: list[T]) -> list[T]:
|
||||
def __add__(self, other: list[T]) -> list[T]: # type: ignore[override]
|
||||
return self._list + other
|
||||
|
||||
def __radd__(self, other: list[T]) -> list[T]:
|
||||
return other + self._list
|
||||
|
||||
def __iadd__(self, other: Iterable[T]) -> LockedListProxy[T]:
|
||||
def __iadd__(self, other: Iterable[T]) -> LockedListProxy[T]: # type: ignore[override]
|
||||
with self._lock:
|
||||
self._list += list(other)
|
||||
return self
|
||||
@@ -629,13 +633,13 @@ class LockedDictProxy(dict, Generic[T]): # type: ignore[type-arg]
|
||||
def copy(self) -> dict[str, T]:
|
||||
return self._dict.copy()
|
||||
|
||||
def __or__(self, other: dict[str, T]) -> dict[str, T]:
|
||||
def __or__(self, other: dict[str, T]) -> dict[str, T]: # type: ignore[override]
|
||||
return self._dict | other
|
||||
|
||||
def __ror__(self, other: dict[str, T]) -> dict[str, T]:
|
||||
def __ror__(self, other: dict[str, T]) -> dict[str, T]: # type: ignore[override]
|
||||
return other | self._dict
|
||||
|
||||
def __ior__(self, other: dict[str, T]) -> LockedDictProxy[T]:
|
||||
def __ior__(self, other: dict[str, T]) -> LockedDictProxy[T]: # type: ignore[override]
|
||||
with self._lock:
|
||||
self._dict |= other
|
||||
return self
|
||||
@@ -821,10 +825,8 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
name: str | None = None
|
||||
tracing: bool | None = None
|
||||
stream: bool = False
|
||||
memory: Any = (
|
||||
None # Memory | MemoryScope | MemorySlice | None; auto-created if not set
|
||||
)
|
||||
input_provider: Any = None # InputProvider | None; per-flow override for self.ask()
|
||||
memory: Memory | MemoryScope | MemorySlice | None = None
|
||||
input_provider: InputProvider | None = None
|
||||
|
||||
def __class_getitem__(cls: type[Flow[T]], item: type[T]) -> type[Flow[T]]:
|
||||
class _FlowGeneric(cls): # type: ignore
|
||||
@@ -903,8 +905,6 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
# Internal flows (RecallFlow, EncodingFlow) set _skip_auto_memory
|
||||
# to avoid creating a wasteful standalone Memory instance.
|
||||
if self.memory is None and not getattr(self, "_skip_auto_memory", False):
|
||||
from crewai.memory.unified_memory import Memory
|
||||
|
||||
self.memory = Memory()
|
||||
|
||||
# Register all flow-related methods
|
||||
@@ -950,10 +950,16 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
|
||||
Raises:
|
||||
ValueError: If no memory is configured for this flow.
|
||||
TypeError: If batch remember is attempted on a MemoryScope or MemorySlice.
|
||||
"""
|
||||
if self.memory is None:
|
||||
raise ValueError("No memory configured for this flow")
|
||||
if isinstance(content, list):
|
||||
if not isinstance(self.memory, Memory):
|
||||
raise TypeError(
|
||||
"Batch remember requires a Memory instance, "
|
||||
f"got {type(self.memory).__name__}"
|
||||
)
|
||||
return self.memory.remember_many(content, **kwargs)
|
||||
return self.memory.remember(content, **kwargs)
|
||||
|
||||
@@ -1770,6 +1776,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
Returns:
|
||||
The final output from the flow or FlowStreamingOutput if streaming.
|
||||
"""
|
||||
get_env_context()
|
||||
if self.stream:
|
||||
result_holder: list[Any] = []
|
||||
current_task_info: TaskInfo = {
|
||||
@@ -2723,7 +2730,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
|
||||
# ── User Input (self.ask) ────────────────────────────────────────
|
||||
|
||||
def _resolve_input_provider(self) -> Any:
|
||||
def _resolve_input_provider(self) -> InputProvider:
|
||||
"""Resolve the input provider using the priority chain.
|
||||
|
||||
Resolution order:
|
||||
@@ -3086,25 +3093,35 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
logger.warning(
|
||||
f"Structured output failed, falling back to simple prompting: {e}"
|
||||
)
|
||||
response = llm_instance.call(messages=prompt)
|
||||
response_clean = str(response).strip()
|
||||
try:
|
||||
response = llm_instance.call(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
response_clean = str(response).strip()
|
||||
|
||||
# Exact match (case-insensitive)
|
||||
for outcome in outcomes:
|
||||
if outcome.lower() == response_clean.lower():
|
||||
return outcome
|
||||
# Exact match (case-insensitive)
|
||||
for outcome in outcomes:
|
||||
if outcome.lower() == response_clean.lower():
|
||||
return outcome
|
||||
|
||||
# Partial match
|
||||
for outcome in outcomes:
|
||||
if outcome.lower() in response_clean.lower():
|
||||
return outcome
|
||||
# Partial match
|
||||
for outcome in outcomes:
|
||||
if outcome.lower() in response_clean.lower():
|
||||
return outcome
|
||||
|
||||
# Fallback to first outcome
|
||||
logger.warning(
|
||||
f"Could not match LLM response '{response_clean}' to outcomes {list(outcomes)}. "
|
||||
f"Falling back to first outcome: {outcomes[0]}"
|
||||
)
|
||||
return outcomes[0]
|
||||
# Fallback to first outcome
|
||||
logger.warning(
|
||||
f"Could not match LLM response '{response_clean}' to outcomes {list(outcomes)}. "
|
||||
f"Falling back to first outcome: {outcomes[0]}"
|
||||
)
|
||||
return outcomes[0]
|
||||
|
||||
except Exception as fallback_err:
|
||||
logger.warning(
|
||||
f"Simple prompting also failed: {fallback_err}. "
|
||||
f"Falling back to first outcome: {outcomes[0]}"
|
||||
)
|
||||
return outcomes[0]
|
||||
|
||||
def _log_flow_event(
|
||||
self,
|
||||
|
||||
@@ -6,7 +6,7 @@ customize Flow behavior at runtime.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -32,17 +32,17 @@ class FlowConfig:
|
||||
self._input_provider: InputProvider | None = None
|
||||
|
||||
@property
|
||||
def hitl_provider(self) -> Any:
|
||||
def hitl_provider(self) -> HumanFeedbackProvider | None:
|
||||
"""Get the configured HITL provider."""
|
||||
return self._hitl_provider
|
||||
|
||||
@hitl_provider.setter
|
||||
def hitl_provider(self, provider: Any) -> None:
|
||||
def hitl_provider(self, provider: HumanFeedbackProvider | None) -> None:
|
||||
"""Set the HITL provider."""
|
||||
self._hitl_provider = provider
|
||||
|
||||
@property
|
||||
def input_provider(self) -> Any:
|
||||
def input_provider(self) -> InputProvider | None:
|
||||
"""Get the configured input provider for ``Flow.ask()``.
|
||||
|
||||
Returns:
|
||||
@@ -52,7 +52,7 @@ class FlowConfig:
|
||||
return self._input_provider
|
||||
|
||||
@input_provider.setter
|
||||
def input_provider(self, provider: Any) -> None:
|
||||
def input_provider(self, provider: InputProvider | None) -> None:
|
||||
"""Set the input provider for ``Flow.ask()``.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -76,6 +76,24 @@ if TYPE_CHECKING:
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def _serialize_llm_for_context(llm: Any) -> str | None:
|
||||
"""Serialize a BaseLLM object to a model string with provider prefix.
|
||||
|
||||
When persisting the LLM for HITL resume, we need to store enough info
|
||||
to reconstruct a working LLM on the resume worker. Just storing the bare
|
||||
model name (e.g. "gemini-3-flash-preview") causes provider inference to
|
||||
fail — it defaults to OpenAI. Including the provider prefix (e.g.
|
||||
"gemini/gemini-3-flash-preview") allows LLM() to correctly route.
|
||||
"""
|
||||
model = getattr(llm, "model", None)
|
||||
if not model:
|
||||
return None
|
||||
provider = getattr(llm, "provider", None)
|
||||
if provider and "/" not in model:
|
||||
return f"{provider}/{model}"
|
||||
return model
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanFeedbackResult:
|
||||
"""Result from a @human_feedback decorated method.
|
||||
@@ -412,7 +430,7 @@ def human_feedback(
|
||||
emit=list(emit) if emit else None,
|
||||
default_outcome=default_outcome,
|
||||
metadata=metadata or {},
|
||||
llm=llm if isinstance(llm, str) else getattr(llm, "model", None),
|
||||
llm=llm if isinstance(llm, str) else _serialize_llm_for_context(llm),
|
||||
)
|
||||
|
||||
# Determine effective provider:
|
||||
|
||||
@@ -6,9 +6,27 @@ from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.utilities.planning_types import TodoItem
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
class TodoExecutionResult(BaseModel):
|
||||
"""Summary of a single todo execution."""
|
||||
|
||||
step_number: int = Field(description="Step number in the plan")
|
||||
description: str = Field(description="What the todo was supposed to do")
|
||||
tool_used: str | None = Field(
|
||||
default=None, description="Tool that was used for this step"
|
||||
)
|
||||
status: str = Field(description="Final status: completed, failed, pending")
|
||||
result: str | None = Field(
|
||||
default=None, description="Result or error message from execution"
|
||||
)
|
||||
depends_on: list[int] = Field(
|
||||
default_factory=list, description="Step numbers this depended on"
|
||||
)
|
||||
|
||||
|
||||
class LiteAgentOutput(BaseModel):
|
||||
"""Class that represents the result of a LiteAgent execution."""
|
||||
|
||||
@@ -24,12 +42,75 @@ class LiteAgentOutput(BaseModel):
|
||||
)
|
||||
messages: list[LLMMessage] = Field(description="Messages of the agent", default=[])
|
||||
|
||||
plan: str | None = Field(
|
||||
default=None, description="The execution plan that was generated, if any"
|
||||
)
|
||||
todos: list[TodoExecutionResult] = Field(
|
||||
default_factory=list,
|
||||
description="List of todos that were executed with their results",
|
||||
)
|
||||
replan_count: int = Field(
|
||||
default=0, description="Number of times the plan was regenerated"
|
||||
)
|
||||
last_replan_reason: str | None = Field(
|
||||
default=None, description="Reason for the last replan, if any"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_todo_items(cls, todo_items: list[TodoItem]) -> list[TodoExecutionResult]:
|
||||
"""Convert TodoItem objects to TodoExecutionResult summaries.
|
||||
|
||||
Args:
|
||||
todo_items: List of TodoItem objects from execution.
|
||||
|
||||
Returns:
|
||||
List of TodoExecutionResult summaries.
|
||||
"""
|
||||
return [
|
||||
TodoExecutionResult(
|
||||
step_number=item.step_number,
|
||||
description=item.description,
|
||||
tool_used=item.tool_to_use,
|
||||
status=item.status,
|
||||
result=item.result,
|
||||
depends_on=item.depends_on,
|
||||
)
|
||||
for item in todo_items
|
||||
]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert pydantic_output to a dictionary."""
|
||||
if self.pydantic:
|
||||
return self.pydantic.model_dump()
|
||||
return {}
|
||||
|
||||
@property
|
||||
def completed_todos(self) -> list[TodoExecutionResult]:
|
||||
"""Get only the completed todos."""
|
||||
return [t for t in self.todos if t.status == "completed"]
|
||||
|
||||
@property
|
||||
def failed_todos(self) -> list[TodoExecutionResult]:
|
||||
"""Get only the failed todos."""
|
||||
return [t for t in self.todos if t.status == "failed"]
|
||||
|
||||
@property
|
||||
def had_plan(self) -> bool:
|
||||
"""Check if the agent executed with a plan."""
|
||||
return self.plan is not None or len(self.todos) > 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the raw output as a string."""
|
||||
return self.raw
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a detailed representation including todo summary."""
|
||||
parts = [f"LiteAgentOutput(role={self.agent_role!r}"]
|
||||
if self.todos:
|
||||
completed = len(self.completed_todos)
|
||||
total = len(self.todos)
|
||||
parts.append(f", todos={completed}/{total} completed")
|
||||
if self.replan_count > 0:
|
||||
parts.append(f", replans={self.replan_count}")
|
||||
parts.append(")")
|
||||
return "".join(parts)
|
||||
|
||||
@@ -240,6 +240,7 @@ ANTHROPIC_MODELS: list[AnthropicModels] = [
|
||||
|
||||
GeminiModels: TypeAlias = Literal[
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
"gemini-2.5-pro-preview-05-06",
|
||||
@@ -294,6 +295,7 @@ GeminiModels: TypeAlias = Literal[
|
||||
]
|
||||
GEMINI_MODELS: list[GeminiModels] = [
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
"gemini-2.5-pro-preview-05-06",
|
||||
|
||||
@@ -618,6 +618,50 @@ class AnthropicCompletion(BaseLLM):
|
||||
return redacted_block
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _convert_image_blocks(content: Any) -> Any:
|
||||
"""Convert OpenAI-style image_url blocks to Anthropic image blocks.
|
||||
|
||||
Upstream code (e.g. StepExecutor) uses the standard ``image_url``
|
||||
format with a ``data:`` URI. Anthropic rejects that — it requires
|
||||
``{"type": "image", "source": {"type": "base64", ...}}``.
|
||||
|
||||
Non-list content and blocks that are not ``image_url`` are passed
|
||||
through unchanged.
|
||||
"""
|
||||
if not isinstance(content, list):
|
||||
return content
|
||||
|
||||
converted: list[dict[str, Any]] = []
|
||||
for block in content:
|
||||
if not isinstance(block, dict) or block.get("type") != "image_url":
|
||||
converted.append(block)
|
||||
continue
|
||||
|
||||
image_info = block.get("image_url", {})
|
||||
url = image_info.get("url", "") if isinstance(image_info, dict) else ""
|
||||
if url.startswith("data:") and ";base64," in url:
|
||||
# Parse data:<media_type>;base64,<data>
|
||||
header, b64_data = url.split(";base64,", 1)
|
||||
media_type = (
|
||||
header.split("data:", 1)[1] if "data:" in header else "image/png"
|
||||
)
|
||||
converted.append(
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": media_type,
|
||||
"data": b64_data,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Non-data URI — pass through as-is (Anthropic supports url source)
|
||||
converted.append(block)
|
||||
|
||||
return converted
|
||||
|
||||
def _format_messages_for_anthropic(
|
||||
self, messages: str | list[LLMMessage]
|
||||
) -> tuple[list[LLMMessage], str | None]:
|
||||
@@ -656,10 +700,11 @@ class AnthropicCompletion(BaseLLM):
|
||||
tool_call_id = message.get("tool_call_id", "")
|
||||
if not tool_call_id:
|
||||
raise ValueError("Tool message missing required tool_call_id")
|
||||
tool_content = self._convert_image_blocks(content) if content else ""
|
||||
tool_result = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_call_id,
|
||||
"content": content if content else "",
|
||||
"content": tool_content,
|
||||
}
|
||||
pending_tool_results.append(tool_result)
|
||||
elif role == "assistant":
|
||||
@@ -718,7 +763,12 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
role_str = role if role is not None else "user"
|
||||
if isinstance(content, list):
|
||||
formatted_messages.append({"role": role_str, "content": content})
|
||||
formatted_messages.append(
|
||||
{
|
||||
"role": role_str,
|
||||
"content": self._convert_image_blocks(content),
|
||||
}
|
||||
)
|
||||
else:
|
||||
content_str = content if content is not None else ""
|
||||
formatted_messages.append(
|
||||
|
||||
@@ -1847,7 +1847,10 @@ class BedrockCompletion(BaseLLM):
|
||||
converse_messages.append({"role": "user", "content": pending_tool_results})
|
||||
|
||||
# CRITICAL: Handle model-specific conversation requirements
|
||||
# Cohere and some other models require conversation to end with user message
|
||||
# Cohere and some other models require conversation to end with user message.
|
||||
# Anthropic models on Bedrock also reject assistant messages in the final
|
||||
# position when tools are present ("pre-filling the assistant response is
|
||||
# not supported").
|
||||
if converse_messages:
|
||||
last_message = converse_messages[-1]
|
||||
if last_message["role"] == "assistant":
|
||||
@@ -1874,6 +1877,20 @@ class BedrockCompletion(BaseLLM):
|
||||
"content": [{"text": "Continue your response."}],
|
||||
}
|
||||
)
|
||||
# Anthropic (Claude) models reject assistant-last messages when
|
||||
# tools are in the request. Append a user message so the
|
||||
# Converse API accepts the payload.
|
||||
elif "anthropic" in self.model.lower() or "claude" in self.model.lower():
|
||||
converse_messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"text": "Please continue and provide your final answer."
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Ensure first message is from user (required by Converse API)
|
||||
if not converse_messages:
|
||||
|
||||
@@ -22,7 +22,6 @@ from crewai.events.types.memory_events import (
|
||||
)
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.memory.analyze import extract_memories_from_content
|
||||
from crewai.memory.recall_flow import RecallFlow
|
||||
from crewai.memory.storage.backend import StorageBackend
|
||||
from crewai.memory.types import (
|
||||
MemoryConfig,
|
||||
@@ -620,6 +619,8 @@ class Memory(BaseModel):
|
||||
)
|
||||
results.sort(key=lambda m: m.score, reverse=True)
|
||||
else:
|
||||
from crewai.memory.recall_flow import RecallFlow
|
||||
|
||||
flow = RecallFlow(
|
||||
storage=self._storage,
|
||||
llm=self._llm,
|
||||
|
||||
@@ -67,6 +67,7 @@ except ImportError:
|
||||
return []
|
||||
|
||||
|
||||
from crewai.types.callback import SerializableCallable
|
||||
from crewai.utilities.guardrail import (
|
||||
process_guardrail,
|
||||
)
|
||||
@@ -124,7 +125,7 @@ class Task(BaseModel):
|
||||
description="Configuration for the agent",
|
||||
default=None,
|
||||
)
|
||||
callback: Any | None = Field(
|
||||
callback: SerializableCallable | None = Field(
|
||||
description="Callback to be executed after the task is completed.", default=None
|
||||
)
|
||||
agent: BaseAgent | None = Field(
|
||||
|
||||
@@ -986,6 +986,22 @@ class Telemetry:
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def env_context_span(self, tool: str) -> None:
|
||||
"""Records the coding tool environment context."""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Environment Context")
|
||||
self._add_attribute(
|
||||
span,
|
||||
"crewai_version",
|
||||
version("crewai"),
|
||||
)
|
||||
self._add_attribute(span, "tool", tool)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def human_feedback_span(
|
||||
self,
|
||||
event_type: str,
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from inspect import Parameter, signature
|
||||
import json
|
||||
import threading
|
||||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
@@ -18,6 +19,7 @@ from pydantic import (
|
||||
BaseModel as PydanticBaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
create_model,
|
||||
field_validator,
|
||||
)
|
||||
@@ -94,6 +96,7 @@ class BaseTool(BaseModel, ABC):
|
||||
default=0,
|
||||
description="Current number of times this tool has been used.",
|
||||
)
|
||||
_usage_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
|
||||
@field_validator("args_schema", mode="before")
|
||||
@classmethod
|
||||
@@ -173,6 +176,25 @@ class BaseTool(BaseModel, ABC):
|
||||
) from e
|
||||
return kwargs
|
||||
|
||||
def _claim_usage(self) -> str | None:
|
||||
"""Atomically check max usage and increment the counter.
|
||||
|
||||
Returns:
|
||||
None if usage was claimed successfully, or an error message
|
||||
string if the tool has reached its usage limit.
|
||||
"""
|
||||
with self._usage_lock:
|
||||
if (
|
||||
self.max_usage_count is not None
|
||||
and self.current_usage_count >= self.max_usage_count
|
||||
):
|
||||
return (
|
||||
f"Tool '{self.name}' has reached its usage limit of "
|
||||
f"{self.max_usage_count} times and cannot be used anymore."
|
||||
)
|
||||
self.current_usage_count += 1
|
||||
return None
|
||||
|
||||
def run(
|
||||
self,
|
||||
*args: Any,
|
||||
@@ -181,13 +203,15 @@ class BaseTool(BaseModel, ABC):
|
||||
if not args:
|
||||
kwargs = self._validate_kwargs(kwargs)
|
||||
|
||||
limit_error = self._claim_usage()
|
||||
if limit_error:
|
||||
return limit_error
|
||||
|
||||
result = self._run(*args, **kwargs)
|
||||
|
||||
if asyncio.iscoroutine(result):
|
||||
result = asyncio.run(result)
|
||||
|
||||
self.current_usage_count += 1
|
||||
|
||||
return result
|
||||
|
||||
async def arun(
|
||||
@@ -206,9 +230,12 @@ class BaseTool(BaseModel, ABC):
|
||||
"""
|
||||
if not args:
|
||||
kwargs = self._validate_kwargs(kwargs)
|
||||
result = await self._arun(*args, **kwargs)
|
||||
self.current_usage_count += 1
|
||||
return result
|
||||
|
||||
limit_error = self._claim_usage()
|
||||
if limit_error:
|
||||
return limit_error
|
||||
|
||||
return await self._arun(*args, **kwargs)
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
@@ -254,6 +281,7 @@ class BaseTool(BaseModel, ABC):
|
||||
result_as_answer=self.result_as_answer,
|
||||
max_usage_count=self.max_usage_count,
|
||||
current_usage_count=self.current_usage_count,
|
||||
cache_function=self.cache_function,
|
||||
)
|
||||
structured_tool._original_tool = self
|
||||
return structured_tool
|
||||
@@ -361,12 +389,15 @@ class Tool(BaseTool, Generic[P, R]):
|
||||
if not args:
|
||||
kwargs = self._validate_kwargs(kwargs) # type: ignore[assignment]
|
||||
|
||||
limit_error = self._claim_usage()
|
||||
if limit_error:
|
||||
return limit_error # type: ignore[return-value]
|
||||
|
||||
result = self.func(*args, **kwargs)
|
||||
|
||||
if asyncio.iscoroutine(result):
|
||||
result = asyncio.run(result)
|
||||
|
||||
self.current_usage_count += 1
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
def _run(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
||||
@@ -393,9 +424,12 @@ class Tool(BaseTool, Generic[P, R]):
|
||||
"""
|
||||
if not args:
|
||||
kwargs = self._validate_kwargs(kwargs) # type: ignore[assignment]
|
||||
result = await self._arun(*args, **kwargs)
|
||||
self.current_usage_count += 1
|
||||
return result
|
||||
|
||||
limit_error = self._claim_usage()
|
||||
if limit_error:
|
||||
return limit_error # type: ignore[return-value]
|
||||
|
||||
return await self._arun(*args, **kwargs)
|
||||
|
||||
async def _arun(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
||||
"""Executes the wrapped function asynchronously.
|
||||
|
||||
@@ -58,6 +58,7 @@ class CrewStructuredTool:
|
||||
result_as_answer: bool = False,
|
||||
max_usage_count: int | None = None,
|
||||
current_usage_count: int = 0,
|
||||
cache_function: Callable[..., bool] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the structured tool.
|
||||
|
||||
@@ -69,6 +70,7 @@ class CrewStructuredTool:
|
||||
result_as_answer: Whether to return the output directly
|
||||
max_usage_count: Maximum number of times this tool can be used. None means unlimited usage.
|
||||
current_usage_count: Current number of times this tool has been used.
|
||||
cache_function: Function to determine if the tool result should be cached.
|
||||
"""
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -78,6 +80,7 @@ class CrewStructuredTool:
|
||||
self.result_as_answer = result_as_answer
|
||||
self.max_usage_count = max_usage_count
|
||||
self.current_usage_count = current_usage_count
|
||||
self.cache_function = cache_function
|
||||
self._original_tool: BaseTool | None = None
|
||||
|
||||
# Validate the function signature matches the schema
|
||||
@@ -86,7 +89,7 @@ class CrewStructuredTool:
|
||||
@classmethod
|
||||
def from_function(
|
||||
cls,
|
||||
func: Callable,
|
||||
func: Callable[..., Any],
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
return_direct: bool = False,
|
||||
@@ -147,7 +150,7 @@ class CrewStructuredTool:
|
||||
@staticmethod
|
||||
def _create_schema_from_function(
|
||||
name: str,
|
||||
func: Callable,
|
||||
func: Callable[..., Any],
|
||||
) -> type[BaseModel]:
|
||||
"""Create a Pydantic schema from a function's signature.
|
||||
|
||||
@@ -182,7 +185,7 @@ class CrewStructuredTool:
|
||||
|
||||
# Create model
|
||||
schema_name = f"{name.title()}Schema"
|
||||
return create_model(schema_name, **fields) # type: ignore[call-overload]
|
||||
return create_model(schema_name, **fields) # type: ignore[call-overload, no-any-return]
|
||||
|
||||
def _validate_function_signature(self) -> None:
|
||||
"""Validate that the function signature matches the args schema."""
|
||||
@@ -210,7 +213,7 @@ class CrewStructuredTool:
|
||||
f"not found in args_schema"
|
||||
)
|
||||
|
||||
def _parse_args(self, raw_args: str | dict) -> dict:
|
||||
def _parse_args(self, raw_args: str | dict[str, Any]) -> dict[str, Any]:
|
||||
"""Parse and validate the input arguments against the schema.
|
||||
|
||||
Args:
|
||||
@@ -234,8 +237,8 @@ class CrewStructuredTool:
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: str | dict,
|
||||
config: dict | None = None,
|
||||
input: str | dict[str, Any],
|
||||
config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Asynchronously invoke the tool.
|
||||
@@ -269,7 +272,7 @@ class CrewStructuredTool:
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
def _run(self, *args, **kwargs) -> Any:
|
||||
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Legacy method for compatibility."""
|
||||
# Convert args/kwargs to our expected format
|
||||
input_dict = dict(zip(self.args_schema.model_fields.keys(), args, strict=False))
|
||||
@@ -277,7 +280,10 @@ class CrewStructuredTool:
|
||||
return self.invoke(input_dict)
|
||||
|
||||
def invoke(
|
||||
self, input: str | dict, config: dict | None = None, **kwargs: Any
|
||||
self,
|
||||
input: str | dict[str, Any],
|
||||
config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Main method for tool execution."""
|
||||
parsed_args = self._parse_args(input)
|
||||
@@ -313,9 +319,10 @@ class CrewStructuredTool:
|
||||
self._original_tool.current_usage_count = self.current_usage_count
|
||||
|
||||
@property
|
||||
def args(self) -> dict:
|
||||
def args(self) -> dict[str, Any]:
|
||||
"""Get the tool's input arguments schema."""
|
||||
return self.args_schema.model_json_schema()["properties"]
|
||||
schema: dict[str, Any] = self.args_schema.model_json_schema()["properties"]
|
||||
return schema
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CrewStructuredTool(name='{sanitize_tool_name(self.name)}', description='{self.description}')"
|
||||
|
||||
@@ -74,9 +74,28 @@
|
||||
"consolidation_user": "New content to consider storing:\n{new_content}\n\nExisting similar memories:\n{records_summary}\n\nReturn the consolidation plan as structured output."
|
||||
},
|
||||
"reasoning": {
|
||||
"initial_plan": "You are {role}, a professional with the following background: {backstory}\n\nYour primary goal is: {goal}\n\nAs {role}, you are creating a strategic plan for a task that requires your expertise and unique perspective.",
|
||||
"refine_plan": "You are {role}, a professional with the following background: {backstory}\n\nYour primary goal is: {goal}\n\nAs {role}, you are refining a strategic plan for a task that requires your expertise and unique perspective.",
|
||||
"create_plan_prompt": "You are {role} with this background: {backstory}\n\nYour primary goal is: {goal}\n\nYou have been assigned the following task:\n{description}\n\nExpected output:\n{expected_output}\n\nAvailable tools: {tools}\n\nBefore executing this task, create a detailed plan that leverages your expertise as {role} and outlines:\n1. Your understanding of the task from your professional perspective\n2. The key steps you'll take to complete it, drawing on your background and skills\n3. How you'll approach any challenges that might arise, considering your expertise\n4. How you'll strategically use the available tools based on your experience, exactly what tools to use and how to use them\n5. The expected outcome and how it aligns with your goal\n\nAfter creating your plan, assess whether you feel ready to execute the task or if you could do better.\nConclude with one of these statements:\n- \"READY: I am ready to execute the task.\"\n- \"NOT READY: I need to refine my plan because [specific reason].\"",
|
||||
"refine_plan_prompt": "You are {role} with this background: {backstory}\n\nYour primary goal is: {goal}\n\nYou created the following plan for this task:\n{current_plan}\n\nHowever, you indicated that you're not ready to execute the task yet.\n\nPlease refine your plan further, drawing on your expertise as {role} to address any gaps or uncertainties. As you refine your plan, be specific about which available tools you will use, how you will use them, and why they are the best choices for each step. Clearly outline your tool usage strategy as part of your improved plan.\n\nAfter refining your plan, assess whether you feel ready to execute the task.\nConclude with one of these statements:\n- \"READY: I am ready to execute the task.\"\n- \"NOT READY: I need to refine my plan further because [specific reason].\""
|
||||
"initial_plan": "You are {role}. Create a focused execution plan using only the essential steps needed.",
|
||||
"refine_plan": "You are {role}. Refine your plan to address the specific gap while keeping it minimal.",
|
||||
"create_plan_prompt": "You are {role}.\n\nTask: {description}\n\nExpected output: {expected_output}\n\nAvailable tools: {tools}\n\nCreate a focused plan with ONLY the essential steps needed. Most tasks require just 2-5 steps. Do NOT pad with unnecessary steps like \"review\", \"verify\", \"document\", or \"finalize\" unless explicitly required.\n\nFor each step, specify the action and which tool to use (if any).\n\nConclude with:\n- \"READY: I am ready to execute the task.\"\n- \"NOT READY: I need to refine my plan because [specific reason].\"",
|
||||
"refine_plan_prompt": "Your plan:\n{current_plan}\n\nYou indicated you're not ready. Address the specific gap while keeping the plan minimal.\n\nConclude with READY or NOT READY."
|
||||
},
|
||||
"planning": {
|
||||
"system_prompt": "You are a strategic planning assistant. Create concrete, executable plans where every step produces a verifiable result.",
|
||||
"create_plan_prompt": "Create an execution plan for the following task:\n\n## Task\n{description}\n\n## Expected Output\n{expected_output}\n\n## Available Tools\n{tools}\n\n## Planning Principles\nFocus on CONCRETE, EXECUTABLE steps. Each step must clearly state WHAT ACTION to take and HOW to verify it succeeded. The number of steps should match the task complexity. Hard limit: {max_steps} steps.\n\n## Rules:\n- Each step must have a clear DONE criterion\n- Do NOT group unrelated actions: if steps can fail independently, keep them separate\n- NO standalone \"thinking\" or \"planning\" steps — act, don't just observe\n- The last step must produce the required output\n\nAfter your plan, state READY or NOT READY.",
|
||||
"refine_plan_prompt": "Your previous plan:\n{current_plan}\n\nYou indicated you weren't ready. Refine your plan to address the specific gap.\n\nKeep the plan minimal - only add steps that directly address the issue.\n\nConclude with READY or NOT READY as before.",
|
||||
"observation_system_prompt": "You are a Planning Agent observing execution progress. After each step completes, you analyze what happened and decide whether the remaining plan is still valid.\n\nReason step-by-step about:\n1. Did this step produce a concrete, verifiable result? (file created, command succeeded, service running, etc.) — or did it only explore without acting?\n2. What new information was learned from this step's result?\n3. Whether the remaining steps still make sense given this new information\n4. What refinements, if any, are needed for upcoming steps\n5. Whether the overall goal has already been achieved\n\nCritical: mark `step_completed_successfully=false` if:\n- The step result is only exploratory (ls, pwd, cat) without producing the required artifact or action\n- A command returned a non-zero exit code and the error was not recovered\n- The step description required creating/building/starting something and the result shows it was not done\n\nBe conservative about triggering full replans — only do so when the remaining plan is fundamentally wrong, not just suboptimal.\n\nIMPORTANT: Set step_completed_successfully=false if:\n- The step's stated goal was NOT achieved (even if other things were done)\n- The first meaningful action returned an error (file not found, command not found, etc.)\n- The result is exploration/discovery output rather than the concrete action the step required\n- The step ran out of attempts without producing the required output\nSet needs_full_replan=true if the current plan's remaining steps reference paths or state that don't exist yet and need to be created first.",
|
||||
"observation_user_prompt": "## Original task\n{task_description}\n\n## Expected output\n{task_goal}\n{completed_summary}\n\n## Just completed step {step_number}\nDescription: {step_description}\nResult: {step_result}\n{remaining_summary}\n\nAnalyze this step's result and provide your observation.",
|
||||
"step_executor_system_prompt": "You are {role}. {backstory}\n\nYour goal: {goal}\n\nYou are executing ONE specific step in a larger plan. Your ONLY job is to fully complete this step — not to plan ahead.\n\nKey rules:\n- **ACT FIRST.** Execute the primary action of this step immediately. Do NOT read or explore files before attempting the main action unless exploration IS the step's goal.\n- If the step says 'run X', run X NOW. If it says 'write file Y', write Y NOW.\n- If the step requires producing an output file (e.g. /app/move.txt, report.jsonl, summary.csv), you MUST write that file using a tool call — do NOT just state the answer in text.\n- You may use tools MULTIPLE TIMES. After each tool use, check the result. If it failed, try a different approach.\n- Only output your Final Answer AFTER the concrete outcome is verified (file written, build succeeded, command exited 0).\n- If a command is not found or a path does not exist, fix it (different PATH, install missing deps, use absolute paths).\n- Do NOT spend more than 3 tool calls on exploration/analysis before attempting the primary action.{tools_section}",
|
||||
"step_executor_tools_section": "\n\nAvailable tools: {tool_names}\n\nYou may call tools multiple times in sequence. Use this format for EACH tool call:\nThought: <what you observed and what you will try next>\nAction: <tool_name>\nAction Input: <input>\n\nAfter observing each result, decide: is the step complete? If yes:\nThought: The step is done because <evidence>\nFinal Answer: <concise summary of what was accomplished and the key result>",
|
||||
"step_executor_user_prompt": "## Current Step\n{step_description}",
|
||||
"step_executor_suggested_tool": "\nSuggested tool: {tool_to_use}",
|
||||
"step_executor_context_header": "\n## Context from previous steps:",
|
||||
"step_executor_context_entry": "Step {step_number} result: {result}",
|
||||
"step_executor_complete_step": "\n**Execute the primary action of this step NOW.** If the step requires writing a file, write it. If it requires running a command, run it. Verify the outcome with a follow-up tool call, then give your Final Answer. Your Final Answer must confirm what was DONE (file created at path X, command succeeded), not just what should be done.",
|
||||
"todo_system_prompt": "You are {role}. Your goal: {goal}\n\nYou are executing a specific step in a multi-step plan. Focus only on completing the current step. Use the suggested tool if one is provided. Be concise and provide clear results that can be used by subsequent steps.",
|
||||
"synthesis_system_prompt": "You are {role}. You have completed a multi-step task. Synthesize the results from all steps into a single, coherent final response that directly addresses the original task. Do NOT list step numbers or say 'Step 1 result'. Produce a clean, polished answer as if you did it all at once.",
|
||||
"synthesis_user_prompt": "## Original Task\n{task_description}\n\n## Results from each step\n{combined_steps}\n\nSynthesize these results into a single, coherent final answer.",
|
||||
"replan_enhancement_prompt": "\n\nIMPORTANT: Previous execution attempt did not fully succeed. Please create a revised plan that accounts for the following context from the previous attempt:\n\n{previous_context}\n\nConsider:\n1. What steps succeeded and can be built upon\n2. What steps failed and why they might have failed\n3. Alternative approaches that might work better\n4. Whether dependencies need to be restructured",
|
||||
"step_executor_task_context": "## Task Context\nThe following is the full task you are helping complete. Keep this in mind — especially any required output files, exact filenames, and expected formats.\n\n{task_context}\n\n---\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
152
lib/crewai/src/crewai/types/callback.py
Normal file
152
lib/crewai/src/crewai/types/callback.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Serializable callback type for Pydantic models.
|
||||
|
||||
Provides a ``SerializableCallable`` type alias that enables full JSON
|
||||
round-tripping of callback fields, e.g. ``"builtins.print"`` ↔ ``print``.
|
||||
Lambdas and closures serialize to a dotted path but cannot be deserialized
|
||||
back — use module-level named functions for checkpointable callbacks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
from typing import Annotated, Any
|
||||
import warnings
|
||||
|
||||
from pydantic import BeforeValidator, WithJsonSchema
|
||||
from pydantic.functional_serializers import PlainSerializer
|
||||
|
||||
|
||||
def _is_non_roundtrippable(fn: object) -> bool:
|
||||
"""Return ``True`` if *fn* cannot survive a serialize/deserialize round-trip.
|
||||
|
||||
Built-in functions, plain module-level functions, and classes produce
|
||||
dotted paths that :func:`_resolve_dotted_path` can reliably resolve.
|
||||
Bound methods, ``functools.partial`` objects, callable class instances,
|
||||
lambdas, and closures all fail or silently change semantics during
|
||||
round-tripping.
|
||||
|
||||
Args:
|
||||
fn: The object to check.
|
||||
|
||||
Returns:
|
||||
``True`` if *fn* would not round-trip through JSON serialization.
|
||||
"""
|
||||
if inspect.isbuiltin(fn) or inspect.isclass(fn):
|
||||
return False
|
||||
if inspect.isfunction(fn):
|
||||
qualname = getattr(fn, "__qualname__", "")
|
||||
return qualname.endswith("<lambda>") or "<locals>" in qualname
|
||||
return True
|
||||
|
||||
|
||||
def string_to_callable(value: Any) -> Callable[..., Any]:
|
||||
"""Convert a dotted path string to the callable it references.
|
||||
|
||||
If *value* is already callable it is returned as-is, with a warning if
|
||||
it cannot survive JSON round-tripping. Otherwise, it is treated as
|
||||
``"module.qualname"`` and resolved via :func:`_resolve_dotted_path`.
|
||||
|
||||
Args:
|
||||
value: A callable or a dotted-path string e.g. ``"builtins.print"``.
|
||||
|
||||
Returns:
|
||||
The resolved callable.
|
||||
|
||||
Raises:
|
||||
ValueError: If *value* is not callable or a resolvable dotted-path string.
|
||||
"""
|
||||
if callable(value):
|
||||
if _is_non_roundtrippable(value):
|
||||
warnings.warn(
|
||||
f"{type(value).__name__} callbacks cannot be serialized "
|
||||
"and will prevent checkpointing. "
|
||||
"Use a module-level named function instead.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return value # type: ignore[no-any-return]
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(
|
||||
f"Expected a callable or dotted-path string, got {type(value).__name__}"
|
||||
)
|
||||
if "." not in value:
|
||||
raise ValueError(
|
||||
f"Invalid callback path {value!r}: expected 'module.name' format"
|
||||
)
|
||||
if not os.environ.get("CREWAI_DESERIALIZE_CALLBACKS"):
|
||||
raise ValueError(
|
||||
f"Refusing to resolve callback path {value!r}: "
|
||||
"set CREWAI_DESERIALIZE_CALLBACKS=1 to allow. "
|
||||
"Only enable this for trusted checkpoint data."
|
||||
)
|
||||
return _resolve_dotted_path(value)
|
||||
|
||||
|
||||
def _resolve_dotted_path(path: str) -> Callable[..., Any]:
|
||||
"""Import a module and walk attribute lookups to resolve a dotted path.
|
||||
|
||||
Handles multi-level qualified names like ``"module.ClassName.method"``
|
||||
by trying progressively shorter module paths and resolving the remainder
|
||||
as chained attribute lookups.
|
||||
|
||||
Args:
|
||||
path: A dotted string e.g. ``"builtins.print"`` or
|
||||
``"mymodule.MyClass.my_method"``.
|
||||
|
||||
Returns:
|
||||
The resolved callable.
|
||||
|
||||
Raises:
|
||||
ValueError: If no valid module can be imported from the path.
|
||||
"""
|
||||
parts = path.split(".")
|
||||
# Try importing progressively shorter prefixes as the module.
|
||||
for i in range(len(parts), 0, -1):
|
||||
module_path = ".".join(parts[:i])
|
||||
try:
|
||||
obj: Any = importlib.import_module(module_path)
|
||||
except (ImportError, TypeError, ValueError):
|
||||
continue
|
||||
# Walk the remaining attribute chain.
|
||||
try:
|
||||
for attr in parts[i:]:
|
||||
obj = getattr(obj, attr)
|
||||
except AttributeError:
|
||||
continue
|
||||
if callable(obj):
|
||||
return obj # type: ignore[no-any-return]
|
||||
raise ValueError(f"Cannot resolve callback {path!r}")
|
||||
|
||||
|
||||
def callable_to_string(fn: Callable[..., Any]) -> str:
|
||||
"""Serialize a callable to its dotted-path string representation.
|
||||
|
||||
Uses ``fn.__module__`` and ``fn.__qualname__`` to produce a string such
|
||||
as ``"builtins.print"``. Lambdas and closures produce paths that contain
|
||||
``<locals>`` and cannot be round-tripped via :func:`string_to_callable`.
|
||||
|
||||
Args:
|
||||
fn: The callable to serialize.
|
||||
|
||||
Returns:
|
||||
A dotted string of the form ``"module.qualname"``.
|
||||
"""
|
||||
module = getattr(fn, "__module__", None)
|
||||
qualname = getattr(fn, "__qualname__", None)
|
||||
if module is None or qualname is None:
|
||||
raise ValueError(
|
||||
f"Cannot serialize {fn!r}: missing __module__ or __qualname__. "
|
||||
"Use a module-level named function for checkpointable callbacks."
|
||||
)
|
||||
return f"{module}.{qualname}"
|
||||
|
||||
|
||||
SerializableCallable = Annotated[
|
||||
Callable[..., Any],
|
||||
BeforeValidator(string_to_callable),
|
||||
PlainSerializer(callable_to_string, return_type=str, when_used="json"),
|
||||
WithJsonSchema({"type": "string"}),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user