mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-20 07:38:12 +00:00
Compare commits
2 Commits
fix/creden
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9db2d44766 | ||
|
|
cf04181190 |
@@ -398,6 +398,7 @@
|
||||
"pages": [
|
||||
"edge/en/enterprise/features/automations",
|
||||
"edge/en/enterprise/features/crew-studio",
|
||||
"edge/en/enterprise/features/merged-step-card",
|
||||
"edge/en/enterprise/features/marketplace",
|
||||
"edge/en/enterprise/features/agent-repositories",
|
||||
"edge/en/enterprise/features/tools-and-integrations",
|
||||
@@ -922,6 +923,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/en/enterprise/features/automations",
|
||||
"v1.14.7/en/enterprise/features/crew-studio",
|
||||
"v1.14.7/en/enterprise/features/merged-step-card",
|
||||
"v1.14.7/en/enterprise/features/marketplace",
|
||||
"v1.14.7/en/enterprise/features/agent-repositories",
|
||||
"v1.14.7/en/enterprise/features/tools-and-integrations",
|
||||
@@ -8548,6 +8550,7 @@
|
||||
"pages": [
|
||||
"edge/pt-BR/enterprise/features/automations",
|
||||
"edge/pt-BR/enterprise/features/crew-studio",
|
||||
"edge/pt-BR/enterprise/features/merged-step-card",
|
||||
"edge/pt-BR/enterprise/features/marketplace",
|
||||
"edge/pt-BR/enterprise/features/agent-repositories",
|
||||
"edge/pt-BR/enterprise/features/tools-and-integrations",
|
||||
@@ -9049,6 +9052,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/pt-BR/enterprise/features/automations",
|
||||
"v1.14.7/pt-BR/enterprise/features/crew-studio",
|
||||
"v1.14.7/pt-BR/enterprise/features/merged-step-card",
|
||||
"v1.14.7/pt-BR/enterprise/features/marketplace",
|
||||
"v1.14.7/pt-BR/enterprise/features/agent-repositories",
|
||||
"v1.14.7/pt-BR/enterprise/features/tools-and-integrations",
|
||||
@@ -16412,6 +16416,7 @@
|
||||
"pages": [
|
||||
"edge/ko/enterprise/features/automations",
|
||||
"edge/ko/enterprise/features/crew-studio",
|
||||
"edge/ko/enterprise/features/merged-step-card",
|
||||
"edge/ko/enterprise/features/marketplace",
|
||||
"edge/ko/enterprise/features/agent-repositories",
|
||||
"edge/ko/enterprise/features/tools-and-integrations",
|
||||
@@ -16925,6 +16930,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/ko/enterprise/features/automations",
|
||||
"v1.14.7/ko/enterprise/features/crew-studio",
|
||||
"v1.14.7/ko/enterprise/features/merged-step-card",
|
||||
"v1.14.7/ko/enterprise/features/marketplace",
|
||||
"v1.14.7/ko/enterprise/features/agent-repositories",
|
||||
"v1.14.7/ko/enterprise/features/tools-and-integrations",
|
||||
@@ -24468,6 +24474,7 @@
|
||||
"pages": [
|
||||
"edge/ar/enterprise/features/automations",
|
||||
"edge/ar/enterprise/features/crew-studio",
|
||||
"edge/ar/enterprise/features/merged-step-card",
|
||||
"edge/ar/enterprise/features/marketplace",
|
||||
"edge/ar/enterprise/features/agent-repositories",
|
||||
"edge/ar/enterprise/features/tools-and-integrations",
|
||||
@@ -24981,6 +24988,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/ar/enterprise/features/automations",
|
||||
"v1.14.7/ar/enterprise/features/crew-studio",
|
||||
"v1.14.7/ar/enterprise/features/merged-step-card",
|
||||
"v1.14.7/ar/enterprise/features/marketplace",
|
||||
"v1.14.7/ar/enterprise/features/agent-repositories",
|
||||
"v1.14.7/ar/enterprise/features/tools-and-integrations",
|
||||
|
||||
87
docs/edge/ar/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/ar/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: بطاقة واحدة لكل خطوة
|
||||
description: "كل خطوة على لوحة Studio هي بطاقة واحدة تجمع بين المهمة والوكيل الذي ينفّذها."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:
|
||||
|
||||
- **المهمة** — ماذا تفعل (الاسم، الوصف، المخرجات المتوقعة، وتنسيق الاستجابة).
|
||||
- **الوكيل** — من ينفّذها (الوكيل المُعيَّن ونموذجه وأدواته).
|
||||
|
||||
الوكيل ليس مشاركًا مستقلاً في سير العمل لديك — بل هو سمة من سمات المهمة: *أي وكيل ينفّذ هذا العمل.* وضع المهمة والوكيل في بطاقة واحدة يجعل هذه العلاقة واضحة، ويحوّل أتمتتك إلى سلسلة واحدة من وحدات العمل من اليسار إلى اليمين يسهل قراءتها بنظرة واحدة.
|
||||
|
||||
<Frame caption="بطاقة واحدة لكل خطوة: المهمة مع ملخص للوكيل المُعيَّن في التذييل.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## على اللوحة
|
||||
|
||||
تعرض كل بطاقة مطوية ما يلي:
|
||||
|
||||
- **اسم المهمة ووصفها** في الأعلى.
|
||||
- **تذييل يلخّص الوكيل المُعيَّن** — الصورة الرمزية والاسم والنموذج والأدوات.
|
||||
|
||||
لا توجد عقدة وكيل منفصلة ولا حافة عمودية من الوكيل ← المهمة. تتصل خطواتك مباشرةً ببعضها البعض بالترتيب الذي تُنفَّذ به.
|
||||
|
||||
## في المحرّر
|
||||
|
||||
افتح بطاقة لتحريرها. العرض الموسّع هو البطاقة نفسها في حالة مفصّلة — وليس شاشة مختلفة — منظّمة في قسمين موسومين بوضوح.
|
||||
|
||||
<Frame caption="المحرّر الموسّع: قسم المهمة مفتوح، والوكيل ملخّص أسفله.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### المهمة — ماذا تفعل
|
||||
|
||||
مفتوحة افتراضيًا، لأنها ما تحرّره عادةً:
|
||||
|
||||
- **الاسم**
|
||||
- **الوصف**
|
||||
- **المخرجات المتوقعة**
|
||||
- **تنسيق الاستجابة** — يظهر هنا لأنه يتحكم تحديدًا في ما تقرأه الخطوات اللاحقة (مثل التوجيه) من هذه الخطوة.
|
||||
|
||||
### الوكيل — من ينفّذها
|
||||
|
||||
يُعرض الوكيل المُعيَّن كملخّص — **الاسم والنموذج والأدوات في سطر واحد**. ويُحفَظ إعداده الأعمق خلف قسمين قابلين للطي:
|
||||
|
||||
- **الدور والهدف والخلفية**
|
||||
- **إعدادات الوكيل** — الاستدلال، الحد الأقصى لمحاولات الاستدلال، السماح بالتفويض، الحد الأقصى للتكرارات، وإعدادات LLM.
|
||||
|
||||
<Tip>
|
||||
الإعداد الكامل للوكيل — الدور، الهدف، الخلفية، النموذج، الأدوات، إعدادات LLM، وكامل كتلة إعدادات الوكيل — موجود خلف القسمين القابلين للطي **الدور والهدف والخلفية** و**إعدادات الوكيل**، منظّمًا حسب عدد مرّات تحريرك له.
|
||||
</Tip>
|
||||
|
||||
## التبديل مقابل تحرير الوكيل
|
||||
|
||||
هناك طريقتان متمايزتان للتعامل مع الوكيل في البطاقة، وكل منهما تؤدي وظيفة مختلفة:
|
||||
|
||||
- **التبديل (Swap)** يعيد تعيين *أي* وكيل ينفّذ هذه المهمة. استخدم عنصر التحكم **تبديل** لاختيار وكيل مختلف من هذا المشروع، أو اختيار واحد من مستودع الوكلاء، أو إنشاء وكيل جديد. هذا مقصور على نطاق المهمة.
|
||||
- **تحرير** الوكيل — بفتح **الدور والهدف والخلفية** أو **إعدادات الوكيل** — يغيّر الوكيل *نفسه*.
|
||||
|
||||
<Frame caption="التبديل يغيّر الوكيل الذي ينفّذ المهمة.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**الوكلاء قابلون لإعادة الاستخدام ومشتركون.** يمكن للوكيل نفسه تنفيذ أكثر من مهمة عبر مشروعك. تحرير دور الوكيل أو خلفيته أو إعداداته يحدّث ذلك الوكيل **في كل مكان يُستخدم فيه** — وليس فقط في البطاقة التي فتحتها. إذا أردت تطبيق تغيير على خطوة واحدة فقط، فقم **بالتبديل** إلى وكيل مختلف بدلاً من تحرير الوكيل المشترك.
|
||||
</Warning>
|
||||
|
||||
## ذات صلة
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ar/enterprise/features/crew-studio" icon="pencil">
|
||||
أنشئ الأتمتة بمساعدة الذكاء الاصطناعي ومحرّر مرئي.
|
||||
</Card>
|
||||
<Card title="مستودعات الوكلاء" href="/ar/enterprise/features/agent-repositories" icon="users">
|
||||
إدارة الوكلاء وإعادة استخدامهم عبر أتمتتك.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -39,6 +39,7 @@ The Enterprise Tools Repository includes:
|
||||
- **Error Handling**: Incorporates robust error handling mechanisms to ensure smooth operation.
|
||||
- **Caching Mechanism**: Features intelligent caching to optimize performance and reduce redundant operations.
|
||||
- **Asynchronous Support**: Handles both synchronous and asynchronous tools, enabling non-blocking operations.
|
||||
- **Typed Outputs**: Uses optional Pydantic models to give agents clear JSON fields while direct Python calls still receive the tool's normal return value.
|
||||
|
||||
## Using CrewAI Tools
|
||||
|
||||
@@ -184,6 +185,55 @@ class MyCustomTool(BaseTool):
|
||||
return "Tool's result"
|
||||
```
|
||||
|
||||
### Typed Tool Outputs
|
||||
|
||||
When a tool returns structured data, define a Pydantic output model. This gives the agent field names it can trust, such as `sku`, `quantity`, or `needs_reorder`.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent a JSON string based on the output model.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str
|
||||
quantity: int
|
||||
needs_reorder: bool
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Checks current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
tool = InventoryTool()
|
||||
|
||||
# Direct calls receive the raw Pydantic object.
|
||||
result = tool.run(sku="SKU-123")
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
To send Markdown or another short text format to the agent, override `format_output_for_agent`. Direct calls to `tool.run(...)` still return the normal Python value.
|
||||
|
||||
```python Code
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Checks current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = InventoryResult.model_validate(raw_result)
|
||||
status = "reorder needed" if result.needs_reorder else "stock is healthy"
|
||||
return f"{result.sku}: {result.quantity} units. {status}."
|
||||
```
|
||||
|
||||
If you do not override `format_output_for_agent`, typed outputs are sent to the agent as JSON. Plain string results work as before.
|
||||
|
||||
## Asynchronous Tool Support
|
||||
|
||||
CrewAI supports asynchronous tools, allowing you to implement tools that perform non-blocking operations like network requests, file I/O, or other async operations without blocking the main execution thread.
|
||||
|
||||
87
docs/edge/en/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/en/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: One Card per Step
|
||||
description: "Each step on the Studio canvas is a single card that combines the task and the agent that performs it."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:
|
||||
|
||||
- **The task** — what to do (name, description, expected output, and response format).
|
||||
- **The agent** — who does it (the assigned agent, its model, and its tools).
|
||||
|
||||
An agent isn't an independent participant in your workflow — it's an attribute of the task: *which agent performs this work.* Putting the task and its agent on one card makes that relationship explicit and turns your automation into a single, left-to-right chain of work units that's easier to read at a glance.
|
||||
|
||||
<Frame caption="One card per step: the task with its assigned agent summarized in the footer.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## On the canvas
|
||||
|
||||
Each collapsed card shows:
|
||||
|
||||
- The **task name and description** at the top.
|
||||
- A **footer summarizing the assigned agent** — avatar, name, model, and tools.
|
||||
|
||||
There's no separate agent node and no vertical agent → task edge. Your steps connect directly to one another in the order they run.
|
||||
|
||||
## In the editor
|
||||
|
||||
Open a card to edit it. The expanded view is the same card in a detailed state — not a different screen — organized into two clearly labeled sections.
|
||||
|
||||
<Frame caption="The expanded editor: the task section open, the agent summarized below it.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### The task — what to do
|
||||
|
||||
Open by default, since this is what you usually edit:
|
||||
|
||||
- **Name**
|
||||
- **Description**
|
||||
- **Expected Output**
|
||||
- **Response Format** — surfaced here because it controls exactly what downstream steps (such as routing) read from this step.
|
||||
|
||||
### The agent — who does it
|
||||
|
||||
The assigned agent is shown as a summary — **name, model, and tools inline**. Its deeper configuration is preserved behind two disclosures:
|
||||
|
||||
- **Role, goal & backstory**
|
||||
- **Agent settings** — reasoning, max reasoning attempts, allow delegation, max iterations, and LLM settings.
|
||||
|
||||
<Tip>
|
||||
An agent's full configuration — Role, Goal, Backstory, Model, Tools, LLM Settings, and the complete Agent Settings block — lives behind the **Role, goal & backstory** and **Agent settings** disclosures, organized by how often you edit it.
|
||||
</Tip>
|
||||
|
||||
## Swapping vs. editing the agent
|
||||
|
||||
There are two distinct ways to work with the agent on a card, and they do different things:
|
||||
|
||||
- **Swap** reassigns *which* agent performs this task. Use the **Swap** control to pick a different agent from this project, choose one from your Agent Repository, or create a new agent. This is scoped to the task.
|
||||
- **Editing** the agent — opening **Role, goal & backstory** or **Agent settings** — changes the agent *itself*.
|
||||
|
||||
<Frame caption="Swap changes which agent performs the task.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Agents are reusable and shared.** The same agent can perform more than one task across your project. Editing an agent's role, backstory, or settings updates that agent **everywhere it's used** — not just on the card you opened. If you want a change to apply to only one step, **Swap** in a different agent instead of editing the shared one.
|
||||
</Warning>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/en/enterprise/features/crew-studio" icon="pencil">
|
||||
Build automations with AI assistance and a visual editor.
|
||||
</Card>
|
||||
<Card title="Agent Repositories" href="/en/enterprise/features/agent-repositories" icon="users">
|
||||
Manage and reuse agents across your automations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -65,7 +65,7 @@ Regardless of which approach you use, your tool must:
|
||||
- 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).
|
||||
- Return a **string** result, or define an optional Pydantic output schema for structured results.
|
||||
|
||||
### Optional: Async Support
|
||||
|
||||
@@ -104,6 +104,67 @@ class TranslateInput(BaseModel):
|
||||
|
||||
Explicit schemas are recommended for published tools — they produce better agent behavior and clearer documentation for your users.
|
||||
|
||||
### Optional: Typed Outputs with `result_schema`
|
||||
|
||||
If your tool returns structured data, define a Pydantic output model. This is a good default for published tools because users and agents can rely on named fields.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent JSON based on the output model.
|
||||
|
||||
CrewAI can infer the output schema from a Pydantic return annotation:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateResult(BaseModel):
|
||||
latitude: float = Field(..., description="Latitude in decimal degrees.")
|
||||
longitude: float = Field(..., description="Longitude in decimal degrees.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
|
||||
def _run(self, address: str) -> GeolocateResult:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
|
||||
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
|
||||
```
|
||||
|
||||
Set `result_schema` explicitly when your tool returns a dictionary:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
result_schema: type[BaseModel] = GeolocateResult
|
||||
|
||||
def _run(self, address: str) -> dict[str, float]:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return {"latitude": 38.8977, "longitude": -77.0365}
|
||||
return {"latitude": 40.7128, "longitude": -74.0060}
|
||||
```
|
||||
|
||||
If agents should receive a short text summary instead of JSON, override `format_output_for_agent` on your `BaseTool` subclass.
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
|
||||
def _run(self, address: str) -> GeolocateResult:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
|
||||
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = GeolocateResult.model_validate(raw_result)
|
||||
return f"Latitude {result.latitude}, longitude {result.longitude}"
|
||||
```
|
||||
|
||||
The override only changes what the agent sees. Direct users of your package still receive the normal value from `tool.run(...)`.
|
||||
|
||||
### Optional: Environment Variables
|
||||
|
||||
If your tool requires API keys or other configuration, declare them with `env_vars` so users know what to set:
|
||||
@@ -241,4 +302,4 @@ agent = Agent(
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
@@ -53,6 +53,111 @@ def my_simple_tool(question: str) -> str:
|
||||
return "Tool output"
|
||||
```
|
||||
|
||||
### Best Practice: Define Typed Outputs
|
||||
|
||||
When a tool returns structured data, define a Pydantic output model. This helps the agent read the result as clear fields instead of guessing from plain text.
|
||||
|
||||
Typed outputs are useful for results with stable fields, such as IDs, status values, scores, prices, or lists. Plain strings are still fine for short prose results.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses a typed tool, CrewAI sends the agent JSON based on the output model.
|
||||
|
||||
#### Return a Pydantic Model
|
||||
|
||||
CrewAI infers the output schema when your `BaseTool` has a Pydantic return annotation.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
quantity: int = Field(description="Units available.")
|
||||
needs_reorder: bool = Field(description="Whether the item should be reordered.")
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Check current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
tool = InventoryTool()
|
||||
result = tool.run(sku="SKU-123")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
When an agent calls `InventoryTool`, it receives JSON like this:
|
||||
|
||||
```json
|
||||
{"sku":"SKU-123","quantity":14,"needs_reorder":false}
|
||||
```
|
||||
|
||||
#### Use `result_schema` with Dictionary Results
|
||||
|
||||
If your tool returns a dictionary, set `result_schema` explicitly. You can do this on a `BaseTool` subclass or with the `@tool` decorator:
|
||||
|
||||
```python Code
|
||||
from crewai.tools import tool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ProductResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
name: str = Field(description="The product name.")
|
||||
in_stock: bool = Field(description="Whether the product is available.")
|
||||
|
||||
@tool("Product Lookup", result_schema=ProductResult)
|
||||
def product_lookup(sku: str) -> dict[str, object]:
|
||||
"""Look up product availability by SKU."""
|
||||
catalog = {
|
||||
"SKU-123": ("Noise-canceling headset", True),
|
||||
"SKU-456": ("USB-C dock", False),
|
||||
}
|
||||
name, in_stock = catalog.get(sku, ("Unknown product", False))
|
||||
return {
|
||||
"sku": sku,
|
||||
"name": name,
|
||||
"in_stock": in_stock,
|
||||
}
|
||||
```
|
||||
|
||||
#### Customize the Text Sent to the Agent
|
||||
|
||||
By default, typed tool outputs are sent to the agent as JSON. If the agent should receive a short summary instead, subclass `BaseTool` and override `format_output_for_agent`.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
quantity: int = Field(description="Units available.")
|
||||
needs_reorder: bool = Field(description="Whether the item should be reordered.")
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Check current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = InventoryResult.model_validate(raw_result)
|
||||
status = "reorder needed" if result.needs_reorder else "stock is healthy"
|
||||
return f"{result.sku}: {result.quantity} units. {status}."
|
||||
|
||||
tool = InventoryTool()
|
||||
result = tool.run(sku="SKU-123")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
The override only changes what the agent sees. Direct calls to `tool.run(...)` still return the normal Python value.
|
||||
|
||||
### Defining a Cache Function for the Tool
|
||||
|
||||
To optimize tool performance with caching, define custom caching strategies using the `cache_function` attribute.
|
||||
|
||||
@@ -195,9 +195,12 @@ class ToolCallHookContext:
|
||||
agent: Agent | None # Agent executing
|
||||
task: Task | None # Current task
|
||||
crew: Crew | None # Crew instance
|
||||
tool_result: str | None # Tool result (after hooks)
|
||||
tool_result: str | None # Agent-facing result string (after hooks)
|
||||
raw_tool_result: Any | None # Raw Python result (after hooks)
|
||||
```
|
||||
|
||||
For typed tool outputs, `tool_result` is the string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. `raw_tool_result` is the original Python value returned by the tool.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Safety and Validation
|
||||
|
||||
@@ -60,9 +60,12 @@ class ToolCallHookContext:
|
||||
agent: Agent | BaseAgent | None # Agent executing the tool
|
||||
task: Task | None # Current task
|
||||
crew: Crew | None # Crew instance
|
||||
tool_result: str | None # Tool result (after hooks only)
|
||||
tool_result: str | None # Agent-facing result string (after hooks only)
|
||||
raw_tool_result: Any | None # Raw Python result (after hooks only)
|
||||
```
|
||||
|
||||
For typed tool outputs, `tool_result` is the string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. Use `raw_tool_result` when your hook needs the typed object or dictionary.
|
||||
|
||||
### Modifying Tool Inputs
|
||||
|
||||
**Important:** Always modify tool inputs in-place:
|
||||
|
||||
87
docs/edge/ko/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/ko/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 단계당 하나의 카드
|
||||
description: "Studio 캔버스의 각 단계는 작업과 이를 수행하는 에이전트를 하나로 결합한 단일 카드입니다."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
|
||||
</Note>
|
||||
|
||||
## 개요
|
||||
|
||||
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:
|
||||
|
||||
- **작업(Task)** — 무엇을 할지(이름, 설명, 예상 출력, 응답 형식).
|
||||
- **에이전트(Agent)** — 누가 수행하는지(할당된 에이전트, 모델, 도구).
|
||||
|
||||
에이전트는 워크플로의 독립적인 참여자가 아니라 작업의 속성, 즉 *이 작업을 어떤 에이전트가 수행하는지*를 나타냅니다. 작업과 에이전트를 하나의 카드에 두면 이 관계가 명확해지고, 자동화가 왼쪽에서 오른쪽으로 이어지는 단일 작업 단위 체인이 되어 한눈에 읽기 쉬워집니다.
|
||||
|
||||
<Frame caption="단계당 하나의 카드: 작업과 푸터에 요약된 할당 에이전트.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## 캔버스에서
|
||||
|
||||
접힌 각 카드는 다음을 표시합니다:
|
||||
|
||||
- 상단의 **작업 이름과 설명**.
|
||||
- **할당된 에이전트를 요약한 푸터** — 아바타, 이름, 모델, 도구.
|
||||
|
||||
별도의 에이전트 노드나 에이전트 → 작업 세로 연결선이 없습니다. 각 단계는 실행 순서대로 서로 직접 연결됩니다.
|
||||
|
||||
## 에디터에서
|
||||
|
||||
카드를 열어 편집합니다. 확장된 보기는 다른 화면이 아니라 동일한 카드의 상세 상태이며, 명확하게 구분된 두 개의 섹션으로 구성됩니다.
|
||||
|
||||
<Frame caption="확장된 에디터: 작업 섹션이 열려 있고 그 아래에 에이전트가 요약되어 있습니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### 작업 — 무엇을 할지
|
||||
|
||||
가장 자주 편집하는 항목이므로 기본적으로 열려 있습니다:
|
||||
|
||||
- **이름**
|
||||
- **설명**
|
||||
- **예상 출력**
|
||||
- **응답 형식** — 다운스트림 단계(예: 라우팅)가 이 단계에서 무엇을 읽을지 정확히 제어하므로 여기에 표시됩니다.
|
||||
|
||||
### 에이전트 — 누가 수행하는지
|
||||
|
||||
할당된 에이전트는 요약으로 표시됩니다 — **이름, 모델, 도구가 인라인으로** 표시됩니다. 더 깊은 구성은 두 개의 접이식 섹션 뒤에 보존됩니다:
|
||||
|
||||
- **역할, 목표 및 배경 스토리**
|
||||
- **에이전트 설정** — 추론, 최대 추론 시도 횟수, 위임 허용, 최대 반복 횟수, LLM 설정.
|
||||
|
||||
<Tip>
|
||||
에이전트의 전체 구성 — 역할, 목표, 배경 스토리, 모델, 도구, LLM 설정 및 전체 에이전트 설정 블록 — 은 **역할, 목표 및 배경 스토리**와 **에이전트 설정** 접이식 섹션 뒤에 편집 빈도에 따라 정리되어 있습니다.
|
||||
</Tip>
|
||||
|
||||
## 에이전트 교체 vs. 편집
|
||||
|
||||
카드에서 에이전트를 다루는 방식은 두 가지로 구분되며, 각각 다른 작업을 수행합니다:
|
||||
|
||||
- **교체(Swap)** 는 *어떤* 에이전트가 이 작업을 수행할지 재할당합니다. **교체** 컨트롤을 사용하여 이 프로젝트의 다른 에이전트를 선택하거나, 에이전트 저장소에서 선택하거나, 새 에이전트를 만들 수 있습니다. 이는 작업 범위로 한정됩니다.
|
||||
- 에이전트 **편집** — **역할, 목표 및 배경 스토리** 또는 **에이전트 설정** 을 여는 것 — 은 에이전트 *자체*를 변경합니다.
|
||||
|
||||
<Frame caption="교체는 작업을 수행할 에이전트를 변경합니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**에이전트는 재사용 가능하며 공유됩니다.** 동일한 에이전트가 프로젝트 전반에서 둘 이상의 작업을 수행할 수 있습니다. 에이전트의 역할, 배경 스토리 또는 설정을 편집하면 열어 본 카드뿐만 아니라 **해당 에이전트가 사용되는 모든 곳**에서 업데이트됩니다. 변경 사항을 하나의 단계에만 적용하려면 공유 에이전트를 편집하지 말고 다른 에이전트로 **교체**하세요.
|
||||
</Warning>
|
||||
|
||||
## 관련 항목
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ko/enterprise/features/crew-studio" icon="pencil">
|
||||
AI 지원과 비주얼 에디터로 자동화를 구축합니다.
|
||||
</Card>
|
||||
<Card title="에이전트 저장소" href="/ko/enterprise/features/agent-repositories" icon="users">
|
||||
자동화 전반에서 에이전트를 관리하고 재사용합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/edge/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Um Card por Etapa
|
||||
description: "Cada etapa no canvas do Studio é um único card que combina a tarefa e o agente que a executa."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
|
||||
</Note>
|
||||
|
||||
## Visão geral
|
||||
|
||||
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:
|
||||
|
||||
- **A tarefa** — o que fazer (nome, descrição, saída esperada e formato da resposta).
|
||||
- **O agente** — quem faz (o agente atribuído, seu modelo e suas ferramentas).
|
||||
|
||||
Um agente não é um participante independente do seu fluxo de trabalho — ele é um atributo da tarefa: *qual agente executa este trabalho.* Colocar a tarefa e seu agente em um único card torna essa relação explícita e transforma sua automação em uma única cadeia de unidades de trabalho, da esquerda para a direita, mais fácil de ler em uma olhada.
|
||||
|
||||
<Frame caption="Um card por etapa: a tarefa com o agente atribuído resumido no rodapé.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## No canvas
|
||||
|
||||
Cada card recolhido mostra:
|
||||
|
||||
- O **nome e a descrição da tarefa** no topo.
|
||||
- Um **rodapé resumindo o agente atribuído** — avatar, nome, modelo e ferramentas.
|
||||
|
||||
Não há nó de agente separado nem aresta vertical de agente → tarefa. Suas etapas se conectam diretamente umas às outras na ordem em que são executadas.
|
||||
|
||||
## No editor
|
||||
|
||||
Abra um card para editá-lo. A visão expandida é o mesmo card em um estado detalhado — não uma tela diferente — organizada em duas seções claramente identificadas.
|
||||
|
||||
<Frame caption="O editor expandido: a seção da tarefa aberta, com o agente resumido abaixo.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### A tarefa — o que fazer
|
||||
|
||||
Aberta por padrão, já que é o que você costuma editar:
|
||||
|
||||
- **Nome**
|
||||
- **Descrição**
|
||||
- **Saída Esperada**
|
||||
- **Formato da Resposta** — exibido aqui porque controla exatamente o que as etapas seguintes (como o roteamento) leem desta etapa.
|
||||
|
||||
### O agente — quem faz
|
||||
|
||||
O agente atribuído é mostrado como um resumo — **nome, modelo e ferramentas em linha**. Sua configuração mais detalhada é preservada por trás de duas seções recolhíveis:
|
||||
|
||||
- **Papel, objetivo e história**
|
||||
- **Configurações do agente** — raciocínio, máximo de tentativas de raciocínio, permitir delegação, máximo de iterações e configurações de LLM.
|
||||
|
||||
<Tip>
|
||||
A configuração completa de um agente — Papel, Objetivo, História, Modelo, Ferramentas, Configurações de LLM e todo o bloco de Configurações do agente — fica por trás das seções recolhíveis **Papel, objetivo e história** e **Configurações do agente**, organizada pela frequência com que você a edita.
|
||||
</Tip>
|
||||
|
||||
## Trocar vs. editar o agente
|
||||
|
||||
Há duas maneiras distintas de trabalhar com o agente em um card, e elas fazem coisas diferentes:
|
||||
|
||||
- **Trocar** reatribui *qual* agente executa esta tarefa. Use o controle **Trocar** para escolher um agente diferente deste projeto, selecionar um do seu Repositório de Agentes ou criar um novo agente. Isso tem escopo limitado à tarefa.
|
||||
- **Editar** o agente — abrindo **Papel, objetivo e história** ou **Configurações do agente** — altera o agente *em si*.
|
||||
|
||||
<Frame caption="Trocar muda qual agente executa a tarefa.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Os agentes são reutilizáveis e compartilhados.** O mesmo agente pode executar mais de uma tarefa em todo o seu projeto. Editar o papel, a história ou as configurações de um agente atualiza esse agente **em todos os lugares onde ele é usado** — não apenas no card que você abriu. Se quiser que uma alteração se aplique a apenas uma etapa, **Troque** por um agente diferente em vez de editar o agente compartilhado.
|
||||
</Warning>
|
||||
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/pt-BR/enterprise/features/crew-studio" icon="pencil">
|
||||
Crie automações com assistência de IA e um editor visual.
|
||||
</Card>
|
||||
<Card title="Repositórios de Agentes" href="/pt-BR/enterprise/features/agent-repositories" icon="users">
|
||||
Gerencie e reutilize agentes em suas automações.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
BIN
docs/images/enterprise/merged-step-card-canvas.png
Normal file
BIN
docs/images/enterprise/merged-step-card-canvas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
BIN
docs/images/enterprise/merged-step-card-editor.png
Normal file
BIN
docs/images/enterprise/merged-step-card-editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
BIN
docs/images/enterprise/merged-step-card-swap-agent.png
Normal file
BIN
docs/images/enterprise/merged-step-card-swap-agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
87
docs/v1.14.7/ar/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/ar/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: بطاقة واحدة لكل خطوة
|
||||
description: "كل خطوة على لوحة Studio هي بطاقة واحدة تجمع بين المهمة والوكيل الذي ينفّذها."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:
|
||||
|
||||
- **المهمة** — ماذا تفعل (الاسم، الوصف، المخرجات المتوقعة، وتنسيق الاستجابة).
|
||||
- **الوكيل** — من ينفّذها (الوكيل المُعيَّن ونموذجه وأدواته).
|
||||
|
||||
الوكيل ليس مشاركًا مستقلاً في سير العمل لديك — بل هو سمة من سمات المهمة: *أي وكيل ينفّذ هذا العمل.* وضع المهمة والوكيل في بطاقة واحدة يجعل هذه العلاقة واضحة، ويحوّل أتمتتك إلى سلسلة واحدة من وحدات العمل من اليسار إلى اليمين يسهل قراءتها بنظرة واحدة.
|
||||
|
||||
<Frame caption="بطاقة واحدة لكل خطوة: المهمة مع ملخص للوكيل المُعيَّن في التذييل.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## على اللوحة
|
||||
|
||||
تعرض كل بطاقة مطوية ما يلي:
|
||||
|
||||
- **اسم المهمة ووصفها** في الأعلى.
|
||||
- **تذييل يلخّص الوكيل المُعيَّن** — الصورة الرمزية والاسم والنموذج والأدوات.
|
||||
|
||||
لا توجد عقدة وكيل منفصلة ولا حافة عمودية من الوكيل ← المهمة. تتصل خطواتك مباشرةً ببعضها البعض بالترتيب الذي تُنفَّذ به.
|
||||
|
||||
## في المحرّر
|
||||
|
||||
افتح بطاقة لتحريرها. العرض الموسّع هو البطاقة نفسها في حالة مفصّلة — وليس شاشة مختلفة — منظّمة في قسمين موسومين بوضوح.
|
||||
|
||||
<Frame caption="المحرّر الموسّع: قسم المهمة مفتوح، والوكيل ملخّص أسفله.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### المهمة — ماذا تفعل
|
||||
|
||||
مفتوحة افتراضيًا، لأنها ما تحرّره عادةً:
|
||||
|
||||
- **الاسم**
|
||||
- **الوصف**
|
||||
- **المخرجات المتوقعة**
|
||||
- **تنسيق الاستجابة** — يظهر هنا لأنه يتحكم تحديدًا في ما تقرأه الخطوات اللاحقة (مثل التوجيه) من هذه الخطوة.
|
||||
|
||||
### الوكيل — من ينفّذها
|
||||
|
||||
يُعرض الوكيل المُعيَّن كملخّص — **الاسم والنموذج والأدوات في سطر واحد**. ويُحفَظ إعداده الأعمق خلف قسمين قابلين للطي:
|
||||
|
||||
- **الدور والهدف والخلفية**
|
||||
- **إعدادات الوكيل** — الاستدلال، الحد الأقصى لمحاولات الاستدلال، السماح بالتفويض، الحد الأقصى للتكرارات، وإعدادات LLM.
|
||||
|
||||
<Tip>
|
||||
الإعداد الكامل للوكيل — الدور، الهدف، الخلفية، النموذج، الأدوات، إعدادات LLM، وكامل كتلة إعدادات الوكيل — موجود خلف القسمين القابلين للطي **الدور والهدف والخلفية** و**إعدادات الوكيل**، منظّمًا حسب عدد مرّات تحريرك له.
|
||||
</Tip>
|
||||
|
||||
## التبديل مقابل تحرير الوكيل
|
||||
|
||||
هناك طريقتان متمايزتان للتعامل مع الوكيل في البطاقة، وكل منهما تؤدي وظيفة مختلفة:
|
||||
|
||||
- **التبديل (Swap)** يعيد تعيين *أي* وكيل ينفّذ هذه المهمة. استخدم عنصر التحكم **تبديل** لاختيار وكيل مختلف من هذا المشروع، أو اختيار واحد من مستودع الوكلاء، أو إنشاء وكيل جديد. هذا مقصور على نطاق المهمة.
|
||||
- **تحرير** الوكيل — بفتح **الدور والهدف والخلفية** أو **إعدادات الوكيل** — يغيّر الوكيل *نفسه*.
|
||||
|
||||
<Frame caption="التبديل يغيّر الوكيل الذي ينفّذ المهمة.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**الوكلاء قابلون لإعادة الاستخدام ومشتركون.** يمكن للوكيل نفسه تنفيذ أكثر من مهمة عبر مشروعك. تحرير دور الوكيل أو خلفيته أو إعداداته يحدّث ذلك الوكيل **في كل مكان يُستخدم فيه** — وليس فقط في البطاقة التي فتحتها. إذا أردت تطبيق تغيير على خطوة واحدة فقط، فقم **بالتبديل** إلى وكيل مختلف بدلاً من تحرير الوكيل المشترك.
|
||||
</Warning>
|
||||
|
||||
## ذات صلة
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ar/enterprise/features/crew-studio" icon="pencil">
|
||||
أنشئ الأتمتة بمساعدة الذكاء الاصطناعي ومحرّر مرئي.
|
||||
</Card>
|
||||
<Card title="مستودعات الوكلاء" href="/ar/enterprise/features/agent-repositories" icon="users">
|
||||
إدارة الوكلاء وإعادة استخدامهم عبر أتمتتك.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/v1.14.7/en/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/en/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: One Card per Step
|
||||
description: "Each step on the Studio canvas is a single card that combines the task and the agent that performs it."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:
|
||||
|
||||
- **The task** — what to do (name, description, expected output, and response format).
|
||||
- **The agent** — who does it (the assigned agent, its model, and its tools).
|
||||
|
||||
An agent isn't an independent participant in your workflow — it's an attribute of the task: *which agent performs this work.* Putting the task and its agent on one card makes that relationship explicit and turns your automation into a single, left-to-right chain of work units that's easier to read at a glance.
|
||||
|
||||
<Frame caption="One card per step: the task with its assigned agent summarized in the footer.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## On the canvas
|
||||
|
||||
Each collapsed card shows:
|
||||
|
||||
- The **task name and description** at the top.
|
||||
- A **footer summarizing the assigned agent** — avatar, name, model, and tools.
|
||||
|
||||
There's no separate agent node and no vertical agent → task edge. Your steps connect directly to one another in the order they run.
|
||||
|
||||
## In the editor
|
||||
|
||||
Open a card to edit it. The expanded view is the same card in a detailed state — not a different screen — organized into two clearly labeled sections.
|
||||
|
||||
<Frame caption="The expanded editor: the task section open, the agent summarized below it.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### The task — what to do
|
||||
|
||||
Open by default, since this is what you usually edit:
|
||||
|
||||
- **Name**
|
||||
- **Description**
|
||||
- **Expected Output**
|
||||
- **Response Format** — surfaced here because it controls exactly what downstream steps (such as routing) read from this step.
|
||||
|
||||
### The agent — who does it
|
||||
|
||||
The assigned agent is shown as a summary — **name, model, and tools inline**. Its deeper configuration is preserved behind two disclosures:
|
||||
|
||||
- **Role, goal & backstory**
|
||||
- **Agent settings** — reasoning, max reasoning attempts, allow delegation, max iterations, and LLM settings.
|
||||
|
||||
<Tip>
|
||||
An agent's full configuration — Role, Goal, Backstory, Model, Tools, LLM Settings, and the complete Agent Settings block — lives behind the **Role, goal & backstory** and **Agent settings** disclosures, organized by how often you edit it.
|
||||
</Tip>
|
||||
|
||||
## Swapping vs. editing the agent
|
||||
|
||||
There are two distinct ways to work with the agent on a card, and they do different things:
|
||||
|
||||
- **Swap** reassigns *which* agent performs this task. Use the **Swap** control to pick a different agent from this project, choose one from your Agent Repository, or create a new agent. This is scoped to the task.
|
||||
- **Editing** the agent — opening **Role, goal & backstory** or **Agent settings** — changes the agent *itself*.
|
||||
|
||||
<Frame caption="Swap changes which agent performs the task.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Agents are reusable and shared.** The same agent can perform more than one task across your project. Editing an agent's role, backstory, or settings updates that agent **everywhere it's used** — not just on the card you opened. If you want a change to apply to only one step, **Swap** in a different agent instead of editing the shared one.
|
||||
</Warning>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/en/enterprise/features/crew-studio" icon="pencil">
|
||||
Build automations with AI assistance and a visual editor.
|
||||
</Card>
|
||||
<Card title="Agent Repositories" href="/en/enterprise/features/agent-repositories" icon="users">
|
||||
Manage and reuse agents across your automations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/v1.14.7/ko/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/ko/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 단계당 하나의 카드
|
||||
description: "Studio 캔버스의 각 단계는 작업과 이를 수행하는 에이전트를 하나로 결합한 단일 카드입니다."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
|
||||
</Note>
|
||||
|
||||
## 개요
|
||||
|
||||
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:
|
||||
|
||||
- **작업(Task)** — 무엇을 할지(이름, 설명, 예상 출력, 응답 형식).
|
||||
- **에이전트(Agent)** — 누가 수행하는지(할당된 에이전트, 모델, 도구).
|
||||
|
||||
에이전트는 워크플로의 독립적인 참여자가 아니라 작업의 속성, 즉 *이 작업을 어떤 에이전트가 수행하는지*를 나타냅니다. 작업과 에이전트를 하나의 카드에 두면 이 관계가 명확해지고, 자동화가 왼쪽에서 오른쪽으로 이어지는 단일 작업 단위 체인이 되어 한눈에 읽기 쉬워집니다.
|
||||
|
||||
<Frame caption="단계당 하나의 카드: 작업과 푸터에 요약된 할당 에이전트.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## 캔버스에서
|
||||
|
||||
접힌 각 카드는 다음을 표시합니다:
|
||||
|
||||
- 상단의 **작업 이름과 설명**.
|
||||
- **할당된 에이전트를 요약한 푸터** — 아바타, 이름, 모델, 도구.
|
||||
|
||||
별도의 에이전트 노드나 에이전트 → 작업 세로 연결선이 없습니다. 각 단계는 실행 순서대로 서로 직접 연결됩니다.
|
||||
|
||||
## 에디터에서
|
||||
|
||||
카드를 열어 편집합니다. 확장된 보기는 다른 화면이 아니라 동일한 카드의 상세 상태이며, 명확하게 구분된 두 개의 섹션으로 구성됩니다.
|
||||
|
||||
<Frame caption="확장된 에디터: 작업 섹션이 열려 있고 그 아래에 에이전트가 요약되어 있습니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### 작업 — 무엇을 할지
|
||||
|
||||
가장 자주 편집하는 항목이므로 기본적으로 열려 있습니다:
|
||||
|
||||
- **이름**
|
||||
- **설명**
|
||||
- **예상 출력**
|
||||
- **응답 형식** — 다운스트림 단계(예: 라우팅)가 이 단계에서 무엇을 읽을지 정확히 제어하므로 여기에 표시됩니다.
|
||||
|
||||
### 에이전트 — 누가 수행하는지
|
||||
|
||||
할당된 에이전트는 요약으로 표시됩니다 — **이름, 모델, 도구가 인라인으로** 표시됩니다. 더 깊은 구성은 두 개의 접이식 섹션 뒤에 보존됩니다:
|
||||
|
||||
- **역할, 목표 및 배경 스토리**
|
||||
- **에이전트 설정** — 추론, 최대 추론 시도 횟수, 위임 허용, 최대 반복 횟수, LLM 설정.
|
||||
|
||||
<Tip>
|
||||
에이전트의 전체 구성 — 역할, 목표, 배경 스토리, 모델, 도구, LLM 설정 및 전체 에이전트 설정 블록 — 은 **역할, 목표 및 배경 스토리**와 **에이전트 설정** 접이식 섹션 뒤에 편집 빈도에 따라 정리되어 있습니다.
|
||||
</Tip>
|
||||
|
||||
## 에이전트 교체 vs. 편집
|
||||
|
||||
카드에서 에이전트를 다루는 방식은 두 가지로 구분되며, 각각 다른 작업을 수행합니다:
|
||||
|
||||
- **교체(Swap)** 는 *어떤* 에이전트가 이 작업을 수행할지 재할당합니다. **교체** 컨트롤을 사용하여 이 프로젝트의 다른 에이전트를 선택하거나, 에이전트 저장소에서 선택하거나, 새 에이전트를 만들 수 있습니다. 이는 작업 범위로 한정됩니다.
|
||||
- 에이전트 **편집** — **역할, 목표 및 배경 스토리** 또는 **에이전트 설정** 을 여는 것 — 은 에이전트 *자체*를 변경합니다.
|
||||
|
||||
<Frame caption="교체는 작업을 수행할 에이전트를 변경합니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**에이전트는 재사용 가능하며 공유됩니다.** 동일한 에이전트가 프로젝트 전반에서 둘 이상의 작업을 수행할 수 있습니다. 에이전트의 역할, 배경 스토리 또는 설정을 편집하면 열어 본 카드뿐만 아니라 **해당 에이전트가 사용되는 모든 곳**에서 업데이트됩니다. 변경 사항을 하나의 단계에만 적용하려면 공유 에이전트를 편집하지 말고 다른 에이전트로 **교체**하세요.
|
||||
</Warning>
|
||||
|
||||
## 관련 항목
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ko/enterprise/features/crew-studio" icon="pencil">
|
||||
AI 지원과 비주얼 에디터로 자동화를 구축합니다.
|
||||
</Card>
|
||||
<Card title="에이전트 저장소" href="/ko/enterprise/features/agent-repositories" icon="users">
|
||||
자동화 전반에서 에이전트를 관리하고 재사용합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/v1.14.7/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Um Card por Etapa
|
||||
description: "Cada etapa no canvas do Studio é um único card que combina a tarefa e o agente que a executa."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
|
||||
</Note>
|
||||
|
||||
## Visão geral
|
||||
|
||||
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:
|
||||
|
||||
- **A tarefa** — o que fazer (nome, descrição, saída esperada e formato da resposta).
|
||||
- **O agente** — quem faz (o agente atribuído, seu modelo e suas ferramentas).
|
||||
|
||||
Um agente não é um participante independente do seu fluxo de trabalho — ele é um atributo da tarefa: *qual agente executa este trabalho.* Colocar a tarefa e seu agente em um único card torna essa relação explícita e transforma sua automação em uma única cadeia de unidades de trabalho, da esquerda para a direita, mais fácil de ler em uma olhada.
|
||||
|
||||
<Frame caption="Um card por etapa: a tarefa com o agente atribuído resumido no rodapé.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## No canvas
|
||||
|
||||
Cada card recolhido mostra:
|
||||
|
||||
- O **nome e a descrição da tarefa** no topo.
|
||||
- Um **rodapé resumindo o agente atribuído** — avatar, nome, modelo e ferramentas.
|
||||
|
||||
Não há nó de agente separado nem aresta vertical de agente → tarefa. Suas etapas se conectam diretamente umas às outras na ordem em que são executadas.
|
||||
|
||||
## No editor
|
||||
|
||||
Abra um card para editá-lo. A visão expandida é o mesmo card em um estado detalhado — não uma tela diferente — organizada em duas seções claramente identificadas.
|
||||
|
||||
<Frame caption="O editor expandido: a seção da tarefa aberta, com o agente resumido abaixo.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### A tarefa — o que fazer
|
||||
|
||||
Aberta por padrão, já que é o que você costuma editar:
|
||||
|
||||
- **Nome**
|
||||
- **Descrição**
|
||||
- **Saída Esperada**
|
||||
- **Formato da Resposta** — exibido aqui porque controla exatamente o que as etapas seguintes (como o roteamento) leem desta etapa.
|
||||
|
||||
### O agente — quem faz
|
||||
|
||||
O agente atribuído é mostrado como um resumo — **nome, modelo e ferramentas em linha**. Sua configuração mais detalhada é preservada por trás de duas seções recolhíveis:
|
||||
|
||||
- **Papel, objetivo e história**
|
||||
- **Configurações do agente** — raciocínio, máximo de tentativas de raciocínio, permitir delegação, máximo de iterações e configurações de LLM.
|
||||
|
||||
<Tip>
|
||||
A configuração completa de um agente — Papel, Objetivo, História, Modelo, Ferramentas, Configurações de LLM e todo o bloco de Configurações do agente — fica por trás das seções recolhíveis **Papel, objetivo e história** e **Configurações do agente**, organizada pela frequência com que você a edita.
|
||||
</Tip>
|
||||
|
||||
## Trocar vs. editar o agente
|
||||
|
||||
Há duas maneiras distintas de trabalhar com o agente em um card, e elas fazem coisas diferentes:
|
||||
|
||||
- **Trocar** reatribui *qual* agente executa esta tarefa. Use o controle **Trocar** para escolher um agente diferente deste projeto, selecionar um do seu Repositório de Agentes ou criar um novo agente. Isso tem escopo limitado à tarefa.
|
||||
- **Editar** o agente — abrindo **Papel, objetivo e história** ou **Configurações do agente** — altera o agente *em si*.
|
||||
|
||||
<Frame caption="Trocar muda qual agente executa a tarefa.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Os agentes são reutilizáveis e compartilhados.** O mesmo agente pode executar mais de uma tarefa em todo o seu projeto. Editar o papel, a história ou as configurações de um agente atualiza esse agente **em todos os lugares onde ele é usado** — não apenas no card que você abriu. Se quiser que uma alteração se aplique a apenas uma etapa, **Troque** por um agente diferente em vez de editar o agente compartilhado.
|
||||
</Warning>
|
||||
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/pt-BR/enterprise/features/crew-studio" icon="pencil">
|
||||
Crie automações com assistência de IA e um editor visual.
|
||||
</Card>
|
||||
<Card title="Repositórios de Agentes" href="/pt-BR/enterprise/features/agent-repositories" icon="users">
|
||||
Gerencie e reutilize agentes em suas automações.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,8 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
@@ -149,55 +146,3 @@ class TestSettings(unittest.TestCase):
|
||||
|
||||
settings = Settings(config_path=self.config_path)
|
||||
self.assertIsNone(settings.tool_repository_username)
|
||||
|
||||
|
||||
class TestSettingsFilePermissions(unittest.TestCase):
|
||||
"""Regression tests: credentials in settings.json must not be world-readable."""
|
||||
|
||||
def setUp(self):
|
||||
self.test_dir = Path(tempfile.mkdtemp())
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "POSIX permission semantics")
|
||||
def test_dump_writes_owner_only_file(self):
|
||||
config_path = self.test_dir / "settings.json"
|
||||
old_umask = os.umask(0o022)
|
||||
try:
|
||||
settings = Settings(
|
||||
config_path=config_path, tool_repository_password="hunter2"
|
||||
)
|
||||
settings.dump()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
mode = stat.S_IMODE(config_path.stat().st_mode)
|
||||
self.assertEqual(mode, 0o600, f"expected 0o600, got {oct(mode)}")
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "POSIX permission semantics")
|
||||
def test_dedicated_config_dir_is_owner_only(self):
|
||||
config_path = self.test_dir / "crewai" / "settings.json"
|
||||
old_umask = os.umask(0o022)
|
||||
try:
|
||||
Settings(config_path=config_path, tool_repository_username="u")
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
mode = stat.S_IMODE(config_path.parent.stat().st_mode)
|
||||
self.assertEqual(mode, 0o700, f"expected 0o700, got {oct(mode)}")
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "POSIX permission semantics")
|
||||
def test_shared_fallback_dir_is_not_chmodded(self):
|
||||
"""The system temp dir (a fallback parent) must never be globally chmod'd."""
|
||||
from crewai_core.settings import _ensure_dir_mode
|
||||
|
||||
tmp_root = Path(tempfile.gettempdir())
|
||||
before = stat.S_IMODE(tmp_root.stat().st_mode)
|
||||
_ensure_dir_mode(tmp_root)
|
||||
after = stat.S_IMODE(tmp_root.stat().st_mode)
|
||||
self.assertEqual(before, after)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Tests for TokenManager with atomic file operations."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
@@ -288,50 +285,5 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
tm._delete_secure_file("nonexistent.txt")
|
||||
|
||||
|
||||
class TestSecureStoragePathPermissions(unittest.TestCase):
|
||||
"""Test that the credential directory is created with restrictive permissions."""
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "POSIX permission semantics")
|
||||
def test_storage_path_is_owner_only(self) -> None:
|
||||
"""The credential directory must be mode 0o700 even under a permissive umask."""
|
||||
with tempfile.TemporaryDirectory() as base:
|
||||
old_umask = os.umask(0o022)
|
||||
try:
|
||||
with (
|
||||
patch("crewai_core.token_manager.sys.platform", "linux"),
|
||||
patch(
|
||||
"crewai_core.token_manager.os.path.expanduser",
|
||||
return_value=base,
|
||||
),
|
||||
):
|
||||
storage_path = TokenManager._get_secure_storage_path()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
self.assertTrue(storage_path.is_dir())
|
||||
mode = stat.S_IMODE(storage_path.stat().st_mode)
|
||||
self.assertEqual(mode, 0o700, f"expected 0o700, got {oct(mode)}")
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "POSIX permission semantics")
|
||||
def test_existing_loose_dir_is_tightened(self) -> None:
|
||||
"""A pre-existing world-traversable directory is corrected to 0o700."""
|
||||
with tempfile.TemporaryDirectory() as base:
|
||||
loose = Path(base) / "crewai" / "credentials"
|
||||
loose.mkdir(parents=True)
|
||||
loose.chmod(0o755)
|
||||
|
||||
with (
|
||||
patch("crewai_core.token_manager.sys.platform", "linux"),
|
||||
patch(
|
||||
"crewai_core.token_manager.os.path.expanduser",
|
||||
return_value=base,
|
||||
),
|
||||
):
|
||||
storage_path = TokenManager._get_secure_storage_path()
|
||||
|
||||
mode = stat.S_IMODE(storage_path.stat().st_mode)
|
||||
self.assertEqual(mode, 0o700, f"expected 0o700, got {oct(mode)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
@@ -26,41 +25,6 @@ logger = getLogger(__name__)
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json"
|
||||
|
||||
|
||||
def _ensure_dir_mode(directory: Path) -> None:
|
||||
"""Tighten a dedicated config directory to 0o700.
|
||||
|
||||
Skips directories shared with other users or content (the system temp dir
|
||||
and the current working directory), which are used as best-effort fallbacks
|
||||
by :func:`get_writable_config_path` and must not be globally chmod'd. Secret
|
||||
files written there are still protected by their own 0o600 mode.
|
||||
"""
|
||||
try:
|
||||
shared = {Path(tempfile.gettempdir()).resolve(), Path.cwd().resolve()}
|
||||
if directory.resolve() in shared:
|
||||
return
|
||||
directory.chmod(0o700)
|
||||
except OSError as e:
|
||||
logger.debug(
|
||||
"Could not enforce 0o700 on config directory %s (best-effort): %s",
|
||||
directory,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
def _write_secure_json(path: Path, data: dict[str, Any]) -> None:
|
||||
"""Atomically write ``data`` as JSON to ``path`` with owner-only (0o600) mode."""
|
||||
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
os.chmod(tmp, 0o600)
|
||||
os.replace(tmp, path)
|
||||
except BaseException:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
raise
|
||||
|
||||
|
||||
def get_writable_config_path() -> Path | None:
|
||||
"""Find a writable location for the config file with fallback options.
|
||||
|
||||
@@ -79,7 +43,6 @@ def get_writable_config_path() -> Path | None:
|
||||
for config_path in fallback_paths:
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_ensure_dir_mode(config_path.parent)
|
||||
test_file = config_path.parent / ".crewai_write_test"
|
||||
try:
|
||||
test_file.write_text("test")
|
||||
@@ -190,7 +153,6 @@ class Settings(BaseModel):
|
||||
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_ensure_dir_mode(config_path.parent)
|
||||
except Exception:
|
||||
merged_data = {**data}
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
@@ -232,7 +194,8 @@ class Settings(BaseModel):
|
||||
existing_data = {}
|
||||
|
||||
updated_data = {**existing_data, **self.model_dump(exclude_unset=True)}
|
||||
_write_secure_json(self.config_path, updated_data)
|
||||
with self.config_path.open("w") as f:
|
||||
json.dump(updated_data, f, indent=4)
|
||||
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
@@ -95,14 +95,6 @@ class TokenManager:
|
||||
storage_path = Path(base_path) / app_name
|
||||
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
# Enforce the documented 0o700 mode: mkdir is subject to umask and does
|
||||
# not adjust the mode of a pre-existing directory, so chmod explicitly.
|
||||
try:
|
||||
storage_path.chmod(0o700)
|
||||
except OSError:
|
||||
# Best-effort permission hardening only: some platforms/filesystems
|
||||
# may reject chmod here, and token operations should still proceed.
|
||||
pass
|
||||
|
||||
return storage_path
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ postgresql = [
|
||||
]
|
||||
bedrock = [
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"bedrock-agentcore>=0.1.0",
|
||||
"bedrock-agentcore>=1.7.0,<1.8.0",
|
||||
"playwright>=1.52.0",
|
||||
"nest-asyncio>=1.6.0",
|
||||
]
|
||||
|
||||
@@ -78,8 +78,8 @@ qdrant = [
|
||||
"qdrant-client[fastembed]~=1.14.3",
|
||||
]
|
||||
aws = [
|
||||
"boto3~=1.42.79",
|
||||
"aiobotocore~=3.4.0",
|
||||
"boto3~=1.42.90",
|
||||
"aiobotocore~=3.5.0",
|
||||
]
|
||||
watson = [
|
||||
"ibm-watsonx-ai~=1.3.39",
|
||||
@@ -91,7 +91,7 @@ litellm = [
|
||||
"litellm>=1.84.0,<2",
|
||||
]
|
||||
bedrock = [
|
||||
"boto3~=1.42.79",
|
||||
"boto3~=1.42.90",
|
||||
]
|
||||
google-genai = [
|
||||
"google-genai~=1.65.0",
|
||||
|
||||
@@ -57,6 +57,7 @@ from crewai.utilities.agent_utils import (
|
||||
convert_tools_to_openai_schema,
|
||||
enforce_rpm_limit,
|
||||
format_message_for_llm,
|
||||
format_native_tool_output_for_agent,
|
||||
get_llm_response,
|
||||
handle_agent_action_core,
|
||||
handle_context_length,
|
||||
@@ -907,19 +908,31 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
):
|
||||
max_usage_reached = True
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
output_tool = original_tool or structured_tool
|
||||
|
||||
from_cache = False
|
||||
result: str = "Tool not found"
|
||||
raw_tool_result: Any = result
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
if self.tools_handler and self.tools_handler.cache and output_tool is not None:
|
||||
cached_result = self.tools_handler.cache.read(
|
||||
tool=func_name, input=input_str
|
||||
)
|
||||
if cached_result is not None:
|
||||
result = (
|
||||
str(cached_result)
|
||||
if not isinstance(cached_result, str)
|
||||
else cached_result
|
||||
)
|
||||
raw_tool_result = cached_result
|
||||
result = format_native_tool_output_for_agent(output_tool, cached_result)
|
||||
from_cache = True
|
||||
|
||||
agent_key = getattr(self.agent, "key", "unknown") if self.agent else "unknown"
|
||||
@@ -938,18 +951,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
track_delegation_if_needed(func_name, args_dict or {}, self.task)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
hook_blocked = False
|
||||
before_hook_context = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
@@ -975,11 +976,18 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
raw_tool_result = result
|
||||
elif max_usage_reached and original_tool:
|
||||
result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore."
|
||||
elif not from_cache and func_name in available_functions:
|
||||
raw_tool_result = result
|
||||
elif (
|
||||
not from_cache
|
||||
and func_name in available_functions
|
||||
and output_tool is not None
|
||||
):
|
||||
try:
|
||||
raw_result = available_functions[func_name](**(args_dict or {}))
|
||||
raw_tool_result = raw_result
|
||||
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
should_cache = True
|
||||
@@ -996,11 +1004,10 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
|
||||
result = (
|
||||
str(raw_result) if not isinstance(raw_result, str) else raw_result
|
||||
)
|
||||
result = format_native_tool_output_for_agent(output_tool, raw_result)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool: {e}"
|
||||
raw_tool_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
crewai_event_bus.emit(
|
||||
@@ -1024,6 +1031,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
task=self.task,
|
||||
crew=self.crew,
|
||||
tool_result=result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
try:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -25,14 +26,14 @@ class ToolsHandler(BaseModel):
|
||||
def on_tool_use(
|
||||
self,
|
||||
calling: ToolCalling | InstructorToolCalling,
|
||||
output: str,
|
||||
output: Any,
|
||||
should_cache: bool = True,
|
||||
) -> None:
|
||||
"""Run when tool ends running.
|
||||
|
||||
Args:
|
||||
calling: The tool calling instance.
|
||||
output: The output from the tool execution.
|
||||
output: The raw output from the tool execution.
|
||||
should_cache: Whether to cache the tool output.
|
||||
"""
|
||||
self.last_used_tool = calling
|
||||
|
||||
@@ -80,6 +80,7 @@ from crewai.utilities.agent_utils import (
|
||||
enforce_rpm_limit,
|
||||
extract_tool_call_info,
|
||||
format_message_for_llm,
|
||||
format_native_tool_output_for_agent,
|
||||
get_llm_response,
|
||||
handle_agent_action_core,
|
||||
handle_context_length,
|
||||
@@ -1905,19 +1906,32 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
):
|
||||
max_usage_reached = True
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
output_tool = original_tool or structured_tool
|
||||
|
||||
# Check cache before executing
|
||||
from_cache = False
|
||||
result = "Tool not found"
|
||||
raw_tool_result: Any = result
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
if self.tools_handler and self.tools_handler.cache and output_tool is not None:
|
||||
cached_result = self.tools_handler.cache.read(
|
||||
tool=func_name, input=input_str
|
||||
)
|
||||
if cached_result is not None:
|
||||
result = (
|
||||
str(cached_result)
|
||||
if not isinstance(cached_result, str)
|
||||
else cached_result
|
||||
)
|
||||
raw_tool_result = cached_result
|
||||
result = format_native_tool_output_for_agent(output_tool, cached_result)
|
||||
from_cache = True
|
||||
|
||||
# Emit tool usage started event
|
||||
@@ -1936,18 +1950,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
|
||||
track_delegation_if_needed(func_name, args_dict, self.task)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
hook_blocked = False
|
||||
before_hook_context = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
@@ -1973,12 +1975,13 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
elif not from_cache and not max_usage_reached:
|
||||
result = "Tool not found"
|
||||
raw_tool_result = result
|
||||
elif not from_cache and not max_usage_reached and output_tool is not None:
|
||||
if func_name in self._available_functions:
|
||||
try:
|
||||
tool_func = self._available_functions[func_name]
|
||||
raw_result = tool_func(**args_dict)
|
||||
raw_tool_result = raw_result
|
||||
|
||||
# Add to cache after successful execution (before string conversion)
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
@@ -1992,14 +1995,12 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
|
||||
# Convert to string for message
|
||||
result = (
|
||||
str(raw_result)
|
||||
if not isinstance(raw_result, str)
|
||||
else raw_result
|
||||
result = format_native_tool_output_for_agent(
|
||||
output_tool, raw_result
|
||||
)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool: {e}"
|
||||
raw_tool_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
# Emit tool usage error event
|
||||
@@ -2021,6 +2022,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore."
|
||||
else:
|
||||
result = f"Tool '{func_name}' has reached its maximum usage limit and cannot be used anymore."
|
||||
raw_tool_result = result
|
||||
|
||||
# Execute after_tool_call hooks (even if blocked, to allow logging/monitoring)
|
||||
after_hook_context = ToolCallHookContext(
|
||||
@@ -2031,6 +2033,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
task=self.task,
|
||||
crew=self.crew,
|
||||
tool_result=result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
try:
|
||||
|
||||
@@ -40,6 +40,8 @@ class ToolCallHookContext:
|
||||
crew: Crew instance (may be None)
|
||||
tool_result: Tool execution result (only set for after_tool_call hooks).
|
||||
Can be modified by returning a new string from after_tool_call hook.
|
||||
raw_tool_result: Raw Python tool execution result (only set for
|
||||
after_tool_call hooks). This is not modified by after hooks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -51,6 +53,7 @@ class ToolCallHookContext:
|
||||
task: Task | None = None,
|
||||
crew: Crew | None = None,
|
||||
tool_result: str | None = None,
|
||||
raw_tool_result: Any | None = None,
|
||||
) -> None:
|
||||
"""Initialize tool call hook context.
|
||||
|
||||
@@ -62,6 +65,7 @@ class ToolCallHookContext:
|
||||
task: Optional current task
|
||||
crew: Optional crew instance
|
||||
tool_result: Optional tool result (for after hooks)
|
||||
raw_tool_result: Optional raw tool result (for after hooks)
|
||||
"""
|
||||
self.tool_name = tool_name
|
||||
self.tool_input = tool_input
|
||||
@@ -70,6 +74,7 @@ class ToolCallHookContext:
|
||||
self.task = task
|
||||
self.crew = crew
|
||||
self.tool_result = tool_result
|
||||
self.raw_tool_result = raw_tool_result
|
||||
|
||||
def request_human_input(
|
||||
self,
|
||||
|
||||
@@ -33,6 +33,8 @@ from typing_extensions import TypeIs
|
||||
from crewai.tools.structured_tool import (
|
||||
CrewStructuredTool,
|
||||
_deserialize_schema,
|
||||
_format_tool_output_for_agent,
|
||||
_infer_result_schema_from_callable,
|
||||
_serialize_schema,
|
||||
build_schema_hint,
|
||||
)
|
||||
@@ -149,6 +151,11 @@ class BaseTool(BaseModel, ABC):
|
||||
validate_default=True,
|
||||
description="The schema for the arguments that the tool accepts.",
|
||||
)
|
||||
result_schema: type[PydanticBaseModel] | None = Field(
|
||||
default=None,
|
||||
validate_default=True,
|
||||
description="The schema for the output that the tool returns.",
|
||||
)
|
||||
|
||||
@field_serializer("args_schema", when_used="json")
|
||||
def _serialize_args_schema(
|
||||
@@ -156,6 +163,12 @@ class BaseTool(BaseModel, ABC):
|
||||
) -> dict[str, Any] | None:
|
||||
return _serialize_schema(schema)
|
||||
|
||||
@field_serializer("result_schema", when_used="json")
|
||||
def _serialize_result_schema(
|
||||
self, schema: type[PydanticBaseModel] | None
|
||||
) -> dict[str, Any] | None:
|
||||
return _serialize_schema(schema)
|
||||
|
||||
description_updated: bool = Field(
|
||||
default=False, description="Flag to check if the description has been updated."
|
||||
)
|
||||
@@ -233,6 +246,17 @@ class BaseTool(BaseModel, ABC):
|
||||
|
||||
return create_model(f"{cls.__name__}Schema", **fields)
|
||||
|
||||
@field_validator("result_schema", mode="before")
|
||||
@classmethod
|
||||
def _default_result_schema(
|
||||
cls, v: type[PydanticBaseModel] | dict[str, Any] | None
|
||||
) -> type[PydanticBaseModel] | None:
|
||||
if isinstance(v, dict):
|
||||
return _deserialize_schema(v)
|
||||
if v is not None:
|
||||
return v
|
||||
return _infer_result_schema_from_callable(cls._run)
|
||||
|
||||
@field_validator("max_usage_count", mode="before")
|
||||
@classmethod
|
||||
def validate_max_usage_count(cls, v: int | None) -> int | None:
|
||||
@@ -340,6 +364,10 @@ class BaseTool(BaseModel, ABC):
|
||||
"Override _arun for async support or use run() for sync execution."
|
||||
)
|
||||
|
||||
def format_output_for_agent(self, raw_result: Any) -> str:
|
||||
"""Format a raw tool result into the string representation sent to an agent."""
|
||||
return _format_tool_output_for_agent(self, raw_result)
|
||||
|
||||
def reset_usage_count(self) -> None:
|
||||
"""Reset the current usage count to zero."""
|
||||
self.current_usage_count = 0
|
||||
@@ -369,6 +397,7 @@ class BaseTool(BaseModel, ABC):
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
args_schema=self.args_schema,
|
||||
result_schema=self.result_schema,
|
||||
func=self._run,
|
||||
result_as_answer=self.result_as_answer,
|
||||
max_usage_count=self.max_usage_count,
|
||||
@@ -390,6 +419,9 @@ class BaseTool(BaseModel, ABC):
|
||||
raise ValueError("The provided tool must have a callable 'func' attribute.")
|
||||
|
||||
args_schema = getattr(tool, "args_schema", None)
|
||||
result_schema = getattr(tool, "result_schema", None)
|
||||
if result_schema is None:
|
||||
result_schema = _infer_result_schema_from_callable(tool.func)
|
||||
|
||||
if args_schema is None:
|
||||
func_signature = signature(tool.func)
|
||||
@@ -420,6 +452,7 @@ class BaseTool(BaseModel, ABC):
|
||||
description=getattr(tool, "description", ""),
|
||||
func=tool.func,
|
||||
args_schema=args_schema,
|
||||
result_schema=result_schema,
|
||||
)
|
||||
|
||||
def _set_args_schema(self) -> None:
|
||||
@@ -568,6 +601,9 @@ class Tool(BaseTool, Generic[P, R]):
|
||||
raise ValueError("The provided tool must have a callable 'func' attribute.")
|
||||
|
||||
args_schema = getattr(tool, "args_schema", None)
|
||||
result_schema = getattr(tool, "result_schema", None)
|
||||
if result_schema is None:
|
||||
result_schema = _infer_result_schema_from_callable(tool.func)
|
||||
|
||||
if args_schema is None:
|
||||
func_signature = signature(tool.func)
|
||||
@@ -598,6 +634,7 @@ class Tool(BaseTool, Generic[P, R]):
|
||||
description=getattr(tool, "description", ""),
|
||||
func=tool.func,
|
||||
args_schema=args_schema,
|
||||
result_schema=result_schema,
|
||||
)
|
||||
|
||||
|
||||
@@ -621,6 +658,7 @@ def tool(
|
||||
name: str,
|
||||
/,
|
||||
*,
|
||||
result_schema: type[BaseModel] | None = ...,
|
||||
result_as_answer: bool = ...,
|
||||
max_usage_count: int | None = ...,
|
||||
) -> Callable[[Callable[P2, R2]], Tool[P2, R2]]: ...
|
||||
@@ -629,6 +667,7 @@ def tool(
|
||||
@overload
|
||||
def tool(
|
||||
*,
|
||||
result_schema: type[BaseModel] | None = ...,
|
||||
result_as_answer: bool = ...,
|
||||
max_usage_count: int | None = ...,
|
||||
) -> Callable[[Callable[P2, R2]], Tool[P2, R2]]: ...
|
||||
@@ -636,6 +675,7 @@ def tool(
|
||||
|
||||
def tool(
|
||||
*args: Callable[P2, R2] | str,
|
||||
result_schema: type[BaseModel] | None = None,
|
||||
result_as_answer: bool = False,
|
||||
max_usage_count: int | None = None,
|
||||
) -> Tool[P2, R2] | Callable[[Callable[P2, R2]], Tool[P2, R2]]:
|
||||
@@ -649,6 +689,7 @@ def tool(
|
||||
Args:
|
||||
*args: Either the function to decorate or a custom tool name.
|
||||
result_as_answer: If True, the tool result becomes the final agent answer.
|
||||
result_schema: Optional schema for the output that the tool returns.
|
||||
max_usage_count: Maximum times this tool can be used. None means unlimited.
|
||||
|
||||
Returns:
|
||||
@@ -690,12 +731,16 @@ def tool(
|
||||
|
||||
class_name = "".join(tool_name.split()).title()
|
||||
args_schema = create_model(class_name, **fields)
|
||||
resolved_result_schema = (
|
||||
result_schema or _infer_result_schema_from_callable(f)
|
||||
)
|
||||
|
||||
return Tool(
|
||||
name=tool_name,
|
||||
description=f.__doc__,
|
||||
func=f,
|
||||
args_schema=args_schema,
|
||||
result_schema=resolved_result_schema,
|
||||
result_as_answer=result_as_answer,
|
||||
max_usage_count=max_usage_count,
|
||||
current_usage_count=0,
|
||||
|
||||
@@ -5,7 +5,8 @@ from collections.abc import Callable
|
||||
import inspect
|
||||
import json
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, Annotated, Any, get_type_hints
|
||||
from typing import TYPE_CHECKING, Annotated, Any, cast, get_type_hints
|
||||
import warnings
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -36,6 +37,52 @@ def _deserialize_schema(v: Any) -> type[BaseModel] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _infer_result_schema_from_callable(
|
||||
func: Callable[..., Any],
|
||||
) -> type[BaseModel] | None:
|
||||
try:
|
||||
return_annotation = get_type_hints(func).get("return", inspect.Signature.empty)
|
||||
except Exception:
|
||||
return_annotation = inspect.signature(func).return_annotation
|
||||
|
||||
if isinstance(return_annotation, type) and issubclass(return_annotation, BaseModel):
|
||||
return return_annotation
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _format_tool_output_for_agent(tool: Any, raw_result: Any) -> str:
|
||||
original_tool = getattr(tool, "_original_tool", None)
|
||||
if original_tool is not None:
|
||||
return cast(str, original_tool.format_output_for_agent(raw_result))
|
||||
|
||||
result_schema = getattr(tool, "result_schema", None)
|
||||
if not (isinstance(result_schema, type) and issubclass(result_schema, BaseModel)):
|
||||
return str(raw_result)
|
||||
|
||||
try:
|
||||
validation_input = raw_result
|
||||
if isinstance(raw_result, BaseModel) and not isinstance(
|
||||
raw_result, result_schema
|
||||
):
|
||||
validation_input = raw_result.model_dump()
|
||||
|
||||
validated = result_schema.model_validate(validation_input)
|
||||
return validated.model_dump_json()
|
||||
except Exception as exc:
|
||||
warnings.warn(
|
||||
(
|
||||
f"Failed to validate or serialize output from tool "
|
||||
f"'{getattr(tool, 'name', '<unknown>')}' using result_schema "
|
||||
f"'{result_schema.__name__}': {exc.__class__.__name__}. "
|
||||
"Falling back to str(raw_result)."
|
||||
),
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return str(raw_result)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
@@ -81,6 +128,11 @@ class CrewStructuredTool(BaseModel):
|
||||
BeforeValidator(_deserialize_schema),
|
||||
PlainSerializer(_serialize_schema),
|
||||
] = Field(default=None)
|
||||
result_schema: Annotated[
|
||||
type[BaseModel] | None,
|
||||
BeforeValidator(_deserialize_schema),
|
||||
PlainSerializer(_serialize_schema),
|
||||
] = Field(default=None)
|
||||
func: Any = Field(default=None, exclude=True)
|
||||
result_as_answer: bool = Field(default=False)
|
||||
max_usage_count: int | None = Field(default=None)
|
||||
@@ -103,6 +155,7 @@ class CrewStructuredTool(BaseModel):
|
||||
description: str | None = None,
|
||||
return_direct: bool = False,
|
||||
args_schema: type[BaseModel] | None = None,
|
||||
result_schema: type[BaseModel] | None = None,
|
||||
infer_schema: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> CrewStructuredTool:
|
||||
@@ -114,6 +167,7 @@ class CrewStructuredTool(BaseModel):
|
||||
description: The description of the tool. Defaults to the function docstring
|
||||
return_direct: Whether to return the output directly
|
||||
args_schema: Optional schema for the function arguments
|
||||
result_schema: Optional schema for the function output
|
||||
infer_schema: Whether to infer the schema from the function signature
|
||||
**kwargs: Additional arguments to pass to the tool
|
||||
|
||||
@@ -149,10 +203,16 @@ class CrewStructuredTool(BaseModel):
|
||||
name=name,
|
||||
description=description,
|
||||
args_schema=schema,
|
||||
result_schema=result_schema or _infer_result_schema_from_callable(func),
|
||||
func=func,
|
||||
result_as_answer=return_direct,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def format_output_for_agent(self, raw_result: Any) -> str:
|
||||
"""Format a raw tool result into the string representation sent to an agent."""
|
||||
return _format_tool_output_for_agent(self, raw_result)
|
||||
|
||||
@staticmethod
|
||||
def _create_schema_from_function(
|
||||
name: str,
|
||||
|
||||
@@ -62,6 +62,9 @@ OPENAI_BIGGER_MODELS: list[
|
||||
]
|
||||
|
||||
|
||||
_RAW_RESULT_UNSET = object()
|
||||
|
||||
|
||||
class ToolUsageError(Exception):
|
||||
"""Exception raised for errors in the tool usage."""
|
||||
|
||||
@@ -106,6 +109,7 @@ class ToolUsage:
|
||||
self.action = action
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.fingerprint_context = fingerprint_context or {}
|
||||
self.last_raw_result: Any = _RAW_RESULT_UNSET
|
||||
|
||||
if (
|
||||
self.function_calling_llm
|
||||
@@ -120,6 +124,11 @@ class ToolUsage:
|
||||
"""Parse the tool string and return the tool calling."""
|
||||
return self._tool_calling(tool_string)
|
||||
|
||||
def get_last_raw_result(self, fallback: Any) -> Any:
|
||||
if self.last_raw_result is _RAW_RESULT_UNSET:
|
||||
return fallback
|
||||
return self.last_raw_result
|
||||
|
||||
def use(
|
||||
self, calling: ToolCalling | InstructorToolCalling, tool_string: str
|
||||
) -> str:
|
||||
@@ -231,6 +240,7 @@ class ToolUsage:
|
||||
result = I18N_DEFAULT.errors("task_repeated_usage").format(
|
||||
tool_names=self.tools_names
|
||||
)
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_repeated_usage(
|
||||
llm=self.function_calling_llm,
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
@@ -298,6 +308,7 @@ class ToolUsage:
|
||||
)
|
||||
if usage_limit_error:
|
||||
result = usage_limit_error
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
|
||||
result = self._format_result(result=result)
|
||||
elif result is None:
|
||||
@@ -359,7 +370,10 @@ class ToolUsage:
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
attempts=self._run_attempts,
|
||||
)
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
data = {
|
||||
"result": result,
|
||||
"tool_name": sanitize_tool_name(tool.name),
|
||||
@@ -421,6 +435,7 @@ class ToolUsage:
|
||||
result = ToolUsageError(
|
||||
f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
|
||||
).message
|
||||
self.last_raw_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
@@ -430,7 +445,10 @@ class ToolUsage:
|
||||
self.task.increment_tools_errors()
|
||||
should_retry = True
|
||||
else:
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
|
||||
finally:
|
||||
if started_event_emitted and not error_event_emitted:
|
||||
@@ -460,6 +478,7 @@ class ToolUsage:
|
||||
result = I18N_DEFAULT.errors("task_repeated_usage").format(
|
||||
tool_names=self.tools_names
|
||||
)
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_repeated_usage(
|
||||
llm=self.function_calling_llm,
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
@@ -529,6 +548,7 @@ class ToolUsage:
|
||||
)
|
||||
if usage_limit_error:
|
||||
result = usage_limit_error
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
|
||||
result = self._format_result(result=result)
|
||||
elif result is None:
|
||||
@@ -590,7 +610,10 @@ class ToolUsage:
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
attempts=self._run_attempts,
|
||||
)
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
data = {
|
||||
"result": result,
|
||||
"tool_name": sanitize_tool_name(tool.name),
|
||||
@@ -652,6 +675,7 @@ class ToolUsage:
|
||||
result = ToolUsageError(
|
||||
f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
|
||||
).message
|
||||
self.last_raw_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
@@ -661,7 +685,10 @@ class ToolUsage:
|
||||
self.task.increment_tools_errors()
|
||||
should_retry = True
|
||||
else:
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
|
||||
finally:
|
||||
if started_event_emitted and not error_event_emitted:
|
||||
|
||||
@@ -1383,6 +1383,19 @@ class NativeToolCallResult:
|
||||
tool_message: LLMMessage = field(default_factory=dict) # type: ignore[assignment]
|
||||
|
||||
|
||||
def format_native_tool_output_for_agent(tool: Any, raw_result: Any) -> str:
|
||||
"""Format native tool output when a tool explicitly defines a formatter."""
|
||||
formatter = inspect.getattr_static(tool, "format_output_for_agent", None)
|
||||
if formatter is None:
|
||||
return str(raw_result)
|
||||
|
||||
runtime_formatter = getattr(tool, "format_output_for_agent", None)
|
||||
if not callable(runtime_formatter):
|
||||
return str(raw_result)
|
||||
|
||||
return str(runtime_formatter(raw_result))
|
||||
|
||||
|
||||
def execute_single_native_tool_call(
|
||||
tool_call: Any,
|
||||
*,
|
||||
@@ -1456,18 +1469,24 @@ def execute_single_native_tool_call(
|
||||
original_tool = tool
|
||||
break
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
for structured in structured_tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
output_tool = original_tool or structured_tool
|
||||
|
||||
from_cache = False
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
result = "Tool not found"
|
||||
raw_tool_result: Any = result
|
||||
|
||||
if tools_handler and tools_handler.cache:
|
||||
if tools_handler and tools_handler.cache and output_tool is not None:
|
||||
cached_result = tools_handler.cache.read(tool=func_name, input=input_str)
|
||||
if cached_result is not None:
|
||||
result = (
|
||||
str(cached_result)
|
||||
if not isinstance(cached_result, str)
|
||||
else cached_result
|
||||
)
|
||||
raw_tool_result = cached_result
|
||||
result = format_native_tool_output_for_agent(output_tool, cached_result)
|
||||
from_cache = True
|
||||
|
||||
started_at = datetime.now()
|
||||
@@ -1486,12 +1505,6 @@ def execute_single_native_tool_call(
|
||||
|
||||
track_delegation_if_needed(func_name, args_dict, task)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
for structured in structured_tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
hook_blocked = False
|
||||
before_hook_context = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
@@ -1512,11 +1525,13 @@ def execute_single_native_tool_call(
|
||||
error_event_emitted = False
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
raw_tool_result = result
|
||||
elif not from_cache:
|
||||
if func_name in available_functions:
|
||||
if func_name in available_functions and output_tool is not None:
|
||||
try:
|
||||
tool_func = available_functions[func_name]
|
||||
raw_result = tool_func(**args_dict)
|
||||
raw_tool_result = raw_result
|
||||
|
||||
if tools_handler and tools_handler.cache:
|
||||
should_cache = True
|
||||
@@ -1529,11 +1544,10 @@ def execute_single_native_tool_call(
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
|
||||
result = (
|
||||
str(raw_result) if not isinstance(raw_result, str) else raw_result
|
||||
)
|
||||
result = format_native_tool_output_for_agent(output_tool, raw_result)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool: {e}"
|
||||
raw_tool_result = result
|
||||
if task:
|
||||
task.increment_tools_errors()
|
||||
crewai_event_bus.emit(
|
||||
@@ -1559,6 +1573,7 @@ def execute_single_native_tool_call(
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
try:
|
||||
for after_hook in get_after_tool_call_hooks():
|
||||
|
||||
@@ -116,6 +116,7 @@ async def aexecute_tool_and_check_finality(
|
||||
logger.log("error", f"Error in before_tool_call hook: {e}")
|
||||
|
||||
tool_result = await tool_usage.ause(tool_calling, agent_action.text)
|
||||
raw_tool_result = tool_usage.get_last_raw_result(tool_result)
|
||||
|
||||
after_hook_context = ToolCallHookContext(
|
||||
tool_name=sanitized_tool_name,
|
||||
@@ -125,6 +126,7 @@ async def aexecute_tool_and_check_finality(
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=tool_result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
@@ -234,6 +236,7 @@ def execute_tool_and_check_finality(
|
||||
logger.log("error", f"Error in before_tool_call hook: {e}")
|
||||
|
||||
tool_result = tool_usage.use(tool_calling, agent_action.text)
|
||||
raw_tool_result = tool_usage.get_last_raw_result(tool_result)
|
||||
|
||||
after_hook_context = ToolCallHookContext(
|
||||
tool_name=sanitized_tool_name,
|
||||
@@ -243,6 +246,7 @@ def execute_tool_and_check_finality(
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=tool_result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
|
||||
@@ -7,6 +7,7 @@ when the LLM supports it, across multiple providers.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
@@ -20,7 +21,7 @@ from crewai import Agent, Crew, Task
|
||||
from crewai.agents.parser import AgentFinish
|
||||
from crewai.events import crewai_event_bus
|
||||
from crewai.hooks import register_after_tool_call_hook, register_before_tool_call_hook
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext, clear_after_tool_call_hooks
|
||||
from crewai.llm import LLM
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
|
||||
@@ -1197,6 +1198,76 @@ class TestNativeToolCallingJsonParseError:
|
||||
|
||||
assert result["result"] == "ran: print(1)"
|
||||
|
||||
def test_typed_output_is_json_agent_text(self) -> None:
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for information"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.8)
|
||||
|
||||
tool = TypedSearchTool()
|
||||
executor = self._make_executor([tool])
|
||||
|
||||
from crewai.utilities.agent_utils import convert_tools_to_openai_schema
|
||||
|
||||
_, available_functions, _ = convert_tools_to_openai_schema([tool])
|
||||
|
||||
result = executor._execute_single_native_tool_call(
|
||||
call_id="call_typed",
|
||||
func_name="typed_search",
|
||||
func_args='{"query": "crew"}',
|
||||
available_functions=available_functions,
|
||||
)
|
||||
|
||||
assert json.loads(result["result"]) == {"query": "crew", "score": 0.8}
|
||||
|
||||
def test_typed_output_after_hook_includes_raw_tool_result(self) -> None:
|
||||
from crewai.utilities.agent_utils import convert_tools_to_openai_schema
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for information"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.8)
|
||||
|
||||
seen_results: list[tuple[str | None, object]] = []
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> None:
|
||||
seen_results.append((context.tool_result, context.raw_tool_result))
|
||||
|
||||
tool = TypedSearchTool()
|
||||
executor = self._make_executor([tool])
|
||||
_, available_functions, _ = convert_tools_to_openai_schema([tool])
|
||||
|
||||
clear_after_tool_call_hooks()
|
||||
register_after_tool_call_hook(after_hook)
|
||||
try:
|
||||
result = executor._execute_single_native_tool_call(
|
||||
call_id="call_typed",
|
||||
func_name="typed_search",
|
||||
func_args='{"query": "crew"}',
|
||||
available_functions=available_functions,
|
||||
)
|
||||
finally:
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
assert json.loads(result["result"]) == {"query": "crew", "score": 0.8}
|
||||
assert seen_results == [
|
||||
('{"query":"crew","score":0.8}', SearchOutput(query="crew", score=0.8))
|
||||
]
|
||||
|
||||
def test_native_tool_loop_falls_back_when_provider_rejects_tools(self) -> None:
|
||||
"""Unsupported native tools errors should continue through ReAct."""
|
||||
|
||||
|
||||
@@ -91,20 +91,24 @@ class TestToolCallHookContext:
|
||||
assert context.task == mock_task
|
||||
assert context.crew == mock_crew
|
||||
assert context.tool_result is None
|
||||
assert context.raw_tool_result is None
|
||||
|
||||
def test_context_with_result(self, mock_tool):
|
||||
"""Test that context includes result when provided."""
|
||||
tool_input = {"arg1": "value1"}
|
||||
tool_result = "Test tool result"
|
||||
raw_tool_result = {"value": 42}
|
||||
|
||||
context = ToolCallHookContext(
|
||||
tool_name="test_tool",
|
||||
tool_input=tool_input,
|
||||
tool=mock_tool,
|
||||
tool_result=tool_result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
|
||||
assert context.tool_result == tool_result
|
||||
assert context.raw_tool_result == raw_tool_result
|
||||
|
||||
def test_tool_input_is_mutable_reference(self, mock_tool):
|
||||
"""Test that modifying context.tool_input modifies the original dict."""
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.crew import Crew
|
||||
from crewai.task import Task
|
||||
from crewai.tools import BaseTool, tool
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -351,6 +352,262 @@ class TestToolDecoratorRunValidation:
|
||||
assert result == "Hello, World!"
|
||||
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
|
||||
class SearchResults(RootModel[list[SearchOutput]]):
|
||||
pass
|
||||
|
||||
|
||||
class ExplicitSearchTool(BaseTool):
|
||||
name: str = "search"
|
||||
description: str = "Search for a query"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> dict[str, object]:
|
||||
return {"query": query, "score": 0.8}
|
||||
|
||||
|
||||
class InferredSearchTool(BaseTool):
|
||||
name: str = "search"
|
||||
description: str = "Search for a query"
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.7)
|
||||
|
||||
|
||||
class RootSearchTool(BaseTool):
|
||||
name: str = "search"
|
||||
description: str = "Search for a query"
|
||||
|
||||
def _run(self, query: str) -> SearchResults:
|
||||
return SearchResults([SearchOutput(query=query, score=1.0)])
|
||||
|
||||
|
||||
class DictAnnotatedSearchTool(BaseTool):
|
||||
name: str = "search"
|
||||
description: str = "Search for a query"
|
||||
|
||||
def _run(self, query: str) -> dict[str, object]:
|
||||
return {"query": query, "score": 0.5}
|
||||
|
||||
|
||||
def _make_explicit_decorator_tool() -> BaseTool:
|
||||
@tool("search", result_schema=SearchOutput)
|
||||
def search(query: str) -> dict[str, object]:
|
||||
"""Search for a query."""
|
||||
return {"query": query, "score": 0.8}
|
||||
|
||||
return search
|
||||
|
||||
|
||||
def _make_inferred_decorator_tool() -> BaseTool:
|
||||
@tool("search")
|
||||
def search(query: str) -> SearchOutput:
|
||||
"""Search for a query."""
|
||||
return SearchOutput(query=query, score=0.6)
|
||||
|
||||
return search
|
||||
|
||||
|
||||
def _make_root_decorator_tool() -> BaseTool:
|
||||
@tool("search")
|
||||
def search(query: str) -> SearchResults:
|
||||
"""Search for a query."""
|
||||
return SearchResults([SearchOutput(query=query, score=1.0)])
|
||||
|
||||
return search
|
||||
|
||||
|
||||
class TestToolOutputSchema:
|
||||
@pytest.mark.parametrize(
|
||||
("tool_cls", "expected_raw", "expected_agent_payload"),
|
||||
[
|
||||
pytest.param(
|
||||
ExplicitSearchTool,
|
||||
{"query": "crew", "score": 0.8},
|
||||
{"query": "crew", "score": 0.8},
|
||||
id="explicit-schema",
|
||||
),
|
||||
pytest.param(
|
||||
InferredSearchTool,
|
||||
SearchOutput(query="crew", score=0.7),
|
||||
{"query": "crew", "score": 0.7},
|
||||
id="inferred-base-model",
|
||||
),
|
||||
pytest.param(
|
||||
RootSearchTool,
|
||||
SearchResults([SearchOutput(query="crew", score=1.0)]),
|
||||
[{"query": "crew", "score": 1.0}],
|
||||
id="inferred-root-model",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_base_tools_return_raw_result_and_json_agent_text(
|
||||
self,
|
||||
tool_cls: type[BaseTool],
|
||||
expected_raw: object,
|
||||
expected_agent_payload: object,
|
||||
) -> None:
|
||||
t = tool_cls()
|
||||
|
||||
raw_result = t.run(query="crew")
|
||||
|
||||
assert raw_result == expected_raw
|
||||
assert json.loads(t.format_output_for_agent(raw_result)) == (
|
||||
expected_agent_payload
|
||||
)
|
||||
|
||||
def test_base_tool_does_not_infer_non_pydantic_return_annotation(self) -> None:
|
||||
t = DictAnnotatedSearchTool()
|
||||
|
||||
raw_result = t.run(query="crew")
|
||||
|
||||
assert raw_result == {"query": "crew", "score": 0.5}
|
||||
assert t.format_output_for_agent(raw_result) == str(raw_result)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("make_tool", "expected_raw", "expected_agent_payload"),
|
||||
[
|
||||
pytest.param(
|
||||
_make_explicit_decorator_tool,
|
||||
{"query": "crew", "score": 0.8},
|
||||
{"query": "crew", "score": 0.8},
|
||||
id="explicit-schema",
|
||||
),
|
||||
pytest.param(
|
||||
_make_inferred_decorator_tool,
|
||||
SearchOutput(query="crew", score=0.6),
|
||||
{"query": "crew", "score": 0.6},
|
||||
id="inferred-base-model",
|
||||
),
|
||||
pytest.param(
|
||||
_make_root_decorator_tool,
|
||||
SearchResults([SearchOutput(query="crew", score=1.0)]),
|
||||
[{"query": "crew", "score": 1.0}],
|
||||
id="inferred-root-model",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_decorator_tools_return_raw_result_and_json_agent_text(
|
||||
self,
|
||||
make_tool: Callable[[], BaseTool],
|
||||
expected_raw: object,
|
||||
expected_agent_payload: object,
|
||||
) -> None:
|
||||
search = make_tool()
|
||||
|
||||
raw_result = search.run(query="crew")
|
||||
|
||||
assert raw_result == expected_raw
|
||||
assert json.loads(search.format_output_for_agent(raw_result)) == (
|
||||
expected_agent_payload
|
||||
)
|
||||
|
||||
def test_decorator_tool_does_not_infer_non_pydantic_return_annotation(
|
||||
self,
|
||||
) -> None:
|
||||
@tool("search")
|
||||
def search(query: str) -> dict[str, object]:
|
||||
"""Search for a query."""
|
||||
return {"query": query, "score": 0.5}
|
||||
|
||||
raw_result = search.run(query="crew")
|
||||
|
||||
assert raw_result == {"query": "crew", "score": 0.5}
|
||||
assert search.format_output_for_agent(raw_result) == str(raw_result)
|
||||
|
||||
def test_explicit_result_schema_wins_over_return_annotation(self) -> None:
|
||||
class AlternateOutput(BaseModel):
|
||||
value: str
|
||||
|
||||
@tool("search", result_schema=AlternateOutput)
|
||||
def search(query: str) -> SearchOutput:
|
||||
"""Search for a query."""
|
||||
return SearchOutput(query=query, score=0.6)
|
||||
|
||||
raw_result = search.run(query="crew")
|
||||
|
||||
with pytest.warns(RuntimeWarning, match="AlternateOutput"):
|
||||
agent_text = search.format_output_for_agent(raw_result)
|
||||
|
||||
assert raw_result == SearchOutput(query="crew", score=0.6)
|
||||
assert agent_text == str(raw_result)
|
||||
|
||||
def test_invalid_typed_output_warns_and_uses_string_agent_text(
|
||||
self,
|
||||
) -> None:
|
||||
@tool("search", result_schema=SearchOutput)
|
||||
def search(query: str) -> dict[str, object]:
|
||||
"""Search for a query."""
|
||||
return {"query": query, "score": "not-a-float"}
|
||||
|
||||
raw_result = search.run(query="crew")
|
||||
|
||||
with pytest.warns(RuntimeWarning, match="Failed to validate or serialize"):
|
||||
agent_text = search.format_output_for_agent(raw_result)
|
||||
|
||||
assert raw_result == {"query": "crew", "score": "not-a-float"}
|
||||
assert agent_text == str(raw_result)
|
||||
|
||||
def test_unserializable_typed_output_warns_and_uses_string_agent_text(
|
||||
self,
|
||||
) -> None:
|
||||
class OpaqueOutput(BaseModel):
|
||||
value: object
|
||||
|
||||
raw_result = OpaqueOutput(value=object())
|
||||
|
||||
@tool("opaque", result_schema=OpaqueOutput)
|
||||
def opaque() -> OpaqueOutput:
|
||||
"""Return an opaque object."""
|
||||
return raw_result
|
||||
|
||||
result = opaque.run()
|
||||
|
||||
with pytest.warns(RuntimeWarning, match="Failed to validate or serialize"):
|
||||
agent_text = opaque.format_output_for_agent(result)
|
||||
|
||||
assert result is raw_result
|
||||
assert agent_text == str(raw_result)
|
||||
|
||||
def test_result_schema_behavior_carries_over_to_structured_tool(self) -> None:
|
||||
structured = ExplicitSearchTool().to_structured_tool()
|
||||
|
||||
raw_result = structured.invoke({"query": "crew"})
|
||||
|
||||
assert raw_result == {"query": "crew", "score": 0.8}
|
||||
assert json.loads(structured.format_output_for_agent(raw_result)) == {
|
||||
"query": "crew",
|
||||
"score": 0.8,
|
||||
}
|
||||
|
||||
def test_custom_agent_output_formatter_carries_over_to_structured_tool(
|
||||
self,
|
||||
) -> None:
|
||||
class MarkdownSearchTool(BaseTool):
|
||||
name: str = "markdown_search"
|
||||
description: str = "Search for information"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.8)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = self.result_schema.model_validate(raw_result)
|
||||
return f"### Search result\n\n- Query: `{result.query}`\n- Score: {result.score}"
|
||||
|
||||
structured = MarkdownSearchTool().to_structured_tool()
|
||||
|
||||
raw_result = structured.invoke({"query": "crew"})
|
||||
|
||||
assert raw_result == SearchOutput(query="crew", score=0.8)
|
||||
assert structured.format_output_for_agent(raw_result) == (
|
||||
"### Search result\n\n- Query: `crew`\n- Score: 0.8"
|
||||
)
|
||||
|
||||
# Async arun() Schema Validation Tests
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -86,6 +88,118 @@ def test_from_function(basic_function):
|
||||
assert isinstance(tool.args_schema, type(BaseModel))
|
||||
|
||||
|
||||
class StructuredOutput(BaseModel):
|
||||
value: str
|
||||
count: int
|
||||
|
||||
|
||||
class StructuredOutputList(RootModel[list[StructuredOutput]]):
|
||||
pass
|
||||
|
||||
|
||||
def _build_explicit_structured_value(value: str) -> dict[str, object]:
|
||||
"""Build a value."""
|
||||
return {"value": value, "count": 1}
|
||||
|
||||
|
||||
def _build_inferred_structured_value(value: str) -> StructuredOutput:
|
||||
"""Build a value."""
|
||||
return StructuredOutput(value=value, count=1)
|
||||
|
||||
|
||||
def _build_structured_values(value: str) -> StructuredOutputList:
|
||||
"""Build values."""
|
||||
return StructuredOutputList([StructuredOutput(value=value, count=1)])
|
||||
|
||||
|
||||
def _build_plain_structured_value(value: str) -> dict[str, object]:
|
||||
"""Build a value."""
|
||||
return {"value": value, "count": 1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("func", "result_schema", "expected_raw", "expected_agent_payload"),
|
||||
[
|
||||
pytest.param(
|
||||
_build_explicit_structured_value,
|
||||
StructuredOutput,
|
||||
{"value": "crew", "count": 1},
|
||||
{"value": "crew", "count": 1},
|
||||
id="explicit-schema",
|
||||
),
|
||||
pytest.param(
|
||||
_build_inferred_structured_value,
|
||||
None,
|
||||
StructuredOutput(value="crew", count=1),
|
||||
{"value": "crew", "count": 1},
|
||||
id="inferred-base-model",
|
||||
),
|
||||
pytest.param(
|
||||
_build_structured_values,
|
||||
None,
|
||||
StructuredOutputList([StructuredOutput(value="crew", count=1)]),
|
||||
[{"value": "crew", "count": 1}],
|
||||
id="inferred-root-model",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_from_function_returns_raw_result_and_json_agent_text(
|
||||
func,
|
||||
result_schema,
|
||||
expected_raw,
|
||||
expected_agent_payload,
|
||||
):
|
||||
kwargs = {"result_schema": result_schema} if result_schema is not None else {}
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=func,
|
||||
name="build_value",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
raw_result = tool.invoke({"value": "crew"})
|
||||
|
||||
assert raw_result == expected_raw
|
||||
assert json.loads(tool.format_output_for_agent(raw_result)) == (
|
||||
expected_agent_payload
|
||||
)
|
||||
|
||||
|
||||
def test_from_function_does_not_infer_non_pydantic_result_schema():
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=_build_plain_structured_value,
|
||||
name="build_value",
|
||||
)
|
||||
|
||||
raw_result = tool.invoke({"value": "crew"})
|
||||
|
||||
assert raw_result == {"value": "crew", "count": 1}
|
||||
assert tool.format_output_for_agent(raw_result) == str(raw_result)
|
||||
|
||||
|
||||
def test_invalid_typed_output_warns_and_uses_string_agent_text():
|
||||
def build_value(value: str) -> dict[str, object]:
|
||||
"""Build a value."""
|
||||
return {"value": value, "count": "wrong"}
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=build_value,
|
||||
name="build_value",
|
||||
result_schema=StructuredOutput,
|
||||
)
|
||||
raw_result = tool.invoke({"value": "crew"})
|
||||
|
||||
with pytest.warns(
|
||||
RuntimeWarning, match="Failed to validate or serialize"
|
||||
) as warnings:
|
||||
agent_text = tool.format_output_for_agent(raw_result)
|
||||
|
||||
assert raw_result == {"value": "crew", "count": "wrong"}
|
||||
assert agent_text == str(raw_result)
|
||||
warning_message = str(warnings[0].message)
|
||||
assert "ValidationError" in warning_message
|
||||
assert "wrong" not in warning_message
|
||||
|
||||
|
||||
def test_validate_function_signature(basic_function, schema_class):
|
||||
"""Test function signature validation"""
|
||||
tool = CrewStructuredTool(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import random
|
||||
import threading
|
||||
@@ -6,6 +7,9 @@ import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai import Agent, Task
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.agents.parser import AgentAction
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.tool_usage_events import (
|
||||
ToolSelectionErrorEvent,
|
||||
@@ -14,8 +18,15 @@ from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageStartedEvent,
|
||||
ToolValidateInputErrorEvent,
|
||||
)
|
||||
from crewai.hooks.tool_hooks import (
|
||||
ToolCallHookContext,
|
||||
clear_after_tool_call_hooks,
|
||||
register_after_tool_call_hook,
|
||||
)
|
||||
from crewai.tools import BaseTool
|
||||
from crewai.tools.tool_calling import ToolCalling
|
||||
from crewai.tools.tool_usage import ToolUsage
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
from pydantic import BaseModel, Field
|
||||
import pytest
|
||||
|
||||
@@ -38,6 +49,19 @@ class RandomNumberTool(BaseTool):
|
||||
return random.randint(min_value, max_value) # noqa: S311
|
||||
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for a query"
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.7)
|
||||
|
||||
|
||||
# Example agent and task
|
||||
example_agent = Agent(
|
||||
role="Number Generator",
|
||||
@@ -117,6 +141,126 @@ def test_tool_usage_render():
|
||||
assert '"description": "The maximum value of the range (inclusive)"' in rendered
|
||||
|
||||
|
||||
def test_tool_usage_returns_json_agent_text_for_typed_output():
|
||||
tool = TypedSearchTool().to_structured_tool()
|
||||
tool_usage = ToolUsage(
|
||||
tools_handler=None,
|
||||
tools=[tool],
|
||||
task=None,
|
||||
function_calling_llm=MagicMock(),
|
||||
agent=None,
|
||||
action=MagicMock(),
|
||||
)
|
||||
|
||||
result = tool_usage.use(
|
||||
calling=ToolCalling(
|
||||
tool_name="typed_search",
|
||||
arguments={"query": "crew"},
|
||||
),
|
||||
tool_string='Action: typed_search\nAction Input: {"query": "crew"}',
|
||||
)
|
||||
|
||||
assert json.loads(result) == {"query": "crew", "score": 0.7}
|
||||
|
||||
|
||||
def test_tool_usage_cache_callback_receives_raw_typed_output():
|
||||
raw_results: list[object] = []
|
||||
|
||||
def cache_result(_args: object, result: object) -> bool:
|
||||
raw_results.append(result)
|
||||
return True
|
||||
|
||||
class CacheAwareTypedSearchTool(TypedSearchTool):
|
||||
cache_function: Callable = cache_result
|
||||
|
||||
tools_handler = MagicMock()
|
||||
tools_handler.cache = None
|
||||
tools_handler.last_used_tool = None
|
||||
tool = CacheAwareTypedSearchTool().to_structured_tool()
|
||||
tool_usage = ToolUsage(
|
||||
tools_handler=tools_handler,
|
||||
tools=[tool],
|
||||
task=None,
|
||||
function_calling_llm=MagicMock(),
|
||||
agent=None,
|
||||
action=MagicMock(),
|
||||
)
|
||||
|
||||
result = tool_usage.use(
|
||||
calling=ToolCalling(
|
||||
tool_name="typed_search",
|
||||
arguments={"query": "crew"},
|
||||
),
|
||||
tool_string='Action: typed_search\nAction Input: {"query": "crew"}',
|
||||
)
|
||||
|
||||
assert json.loads(result) == {"query": "crew", "score": 0.7}
|
||||
assert raw_results == [SearchOutput(query="crew", score=0.7)]
|
||||
tools_handler.on_tool_use.assert_called_once()
|
||||
assert tools_handler.on_tool_use.call_args.kwargs["output"] == SearchOutput(
|
||||
query="crew",
|
||||
score=0.7,
|
||||
)
|
||||
|
||||
|
||||
def test_react_tool_hooks_receive_agent_text_and_raw_cached_typed_output():
|
||||
structured_tool = TypedSearchTool().to_structured_tool()
|
||||
tools_handler = ToolsHandler(cache=CacheHandler())
|
||||
seen_results: list[tuple[str | None, object]] = []
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> None:
|
||||
seen_results.append((context.tool_result, context.raw_tool_result))
|
||||
|
||||
clear_after_tool_call_hooks()
|
||||
register_after_tool_call_hook(after_hook)
|
||||
|
||||
action = AgentAction(
|
||||
thought="",
|
||||
tool="typed_search",
|
||||
tool_input='{"query": "crew"}',
|
||||
text='Action: typed_search\nAction Input: {"query": "crew"}',
|
||||
)
|
||||
|
||||
try:
|
||||
first = execute_tool_and_check_finality(
|
||||
agent_action=action,
|
||||
tools=[structured_tool],
|
||||
tools_handler=tools_handler,
|
||||
)
|
||||
tools_handler.last_used_tool = None
|
||||
second = execute_tool_and_check_finality(
|
||||
agent_action=action,
|
||||
tools=[structured_tool],
|
||||
tools_handler=tools_handler,
|
||||
)
|
||||
finally:
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
assert json.loads(first.result) == {"query": "crew", "score": 0.7}
|
||||
assert json.loads(second.result) == {"query": "crew", "score": 0.7}
|
||||
assert seen_results == [
|
||||
('{"query":"crew","score":0.7}', SearchOutput(query="crew", score=0.7)),
|
||||
('{"query":"crew","score":0.7}', SearchOutput(query="crew", score=0.7)),
|
||||
]
|
||||
|
||||
|
||||
def test_last_raw_result_falls_back_only_until_recorded():
|
||||
tool_usage = ToolUsage(
|
||||
tools_handler=None,
|
||||
tools=[],
|
||||
task=None,
|
||||
function_calling_llm=MagicMock(),
|
||||
agent=None,
|
||||
action=MagicMock(),
|
||||
)
|
||||
|
||||
assert tool_usage.get_last_raw_result("formatted result") == "formatted result"
|
||||
|
||||
tool_usage.last_raw_result = None
|
||||
|
||||
assert tool_usage.get_last_raw_result("formatted result") is None
|
||||
|
||||
|
||||
def test_validate_tool_input_booleans_and_none():
|
||||
tool_usage = ToolUsage(
|
||||
tools_handler=MagicMock(),
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Literal, Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.hooks.tool_hooks import (
|
||||
ToolCallHookContext,
|
||||
clear_after_tool_call_hooks,
|
||||
clear_before_tool_call_hooks,
|
||||
register_after_tool_call_hook,
|
||||
)
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.utilities.agent_utils import (
|
||||
_asummarize_chunks,
|
||||
@@ -1030,6 +1037,142 @@ class TestParseToolCallArgs:
|
||||
class TestExecuteSingleNativeToolCall:
|
||||
"""Tests for execute_single_native_tool_call."""
|
||||
|
||||
def test_typed_tool_output_is_json_agent_text(self) -> None:
|
||||
clear_before_tool_call_hooks()
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for a query"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.9)
|
||||
|
||||
tool = TypedSearchTool()
|
||||
tool_call = MagicMock()
|
||||
tool_call.id = "call_1"
|
||||
tool_call.function.name = "typed_search"
|
||||
tool_call.function.arguments = '{"query": "crew"}'
|
||||
|
||||
result = execute_single_native_tool_call(
|
||||
tool_call,
|
||||
available_functions={"typed_search": tool._run},
|
||||
original_tools=[tool],
|
||||
structured_tools=[tool.to_structured_tool()],
|
||||
tools_handler=None,
|
||||
agent=None,
|
||||
task=None,
|
||||
crew=None,
|
||||
event_source=MagicMock(),
|
||||
printer=None,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
assert json.loads(result.result) == {"query": "crew", "score": 0.9}
|
||||
assert json.loads(result.tool_message["content"]) == {
|
||||
"query": "crew",
|
||||
"score": 0.9,
|
||||
}
|
||||
|
||||
def test_custom_agent_output_formatter_is_used_from_structured_tool(
|
||||
self,
|
||||
) -> None:
|
||||
clear_before_tool_call_hooks()
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class MarkdownSearchTool(BaseTool):
|
||||
name: str = "markdown_search"
|
||||
description: str = "Search for a query"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.9)
|
||||
|
||||
def format_output_for_agent(self, raw_result: Any) -> str:
|
||||
result = self.result_schema.model_validate(raw_result)
|
||||
return f"### {result.query}\n\nScore: **{result.score}**"
|
||||
|
||||
tool = MarkdownSearchTool()
|
||||
tool_call = MagicMock()
|
||||
tool_call.id = "call_1"
|
||||
tool_call.function.name = "markdown_search"
|
||||
tool_call.function.arguments = '{"query": "crew"}'
|
||||
|
||||
result = execute_single_native_tool_call(
|
||||
tool_call,
|
||||
available_functions={"markdown_search": tool._run},
|
||||
original_tools=[],
|
||||
structured_tools=[tool.to_structured_tool()],
|
||||
tools_handler=None,
|
||||
agent=None,
|
||||
task=None,
|
||||
crew=None,
|
||||
event_source=MagicMock(),
|
||||
printer=None,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
assert result.result == "### crew\n\nScore: **0.9**"
|
||||
assert result.tool_message["content"] == "### crew\n\nScore: **0.9**"
|
||||
|
||||
def test_after_hook_includes_raw_tool_result_for_typed_output(self) -> None:
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for a query"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.9)
|
||||
|
||||
seen_results: list[tuple[str | None, object]] = []
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> None:
|
||||
seen_results.append((context.tool_result, context.raw_tool_result))
|
||||
|
||||
tool = TypedSearchTool()
|
||||
tool_call = MagicMock()
|
||||
tool_call.id = "call_1"
|
||||
tool_call.function.name = "typed_search"
|
||||
tool_call.function.arguments = '{"query": "crew"}'
|
||||
|
||||
register_after_tool_call_hook(after_hook)
|
||||
try:
|
||||
result = execute_single_native_tool_call(
|
||||
tool_call,
|
||||
available_functions={"typed_search": tool._run},
|
||||
original_tools=[tool],
|
||||
structured_tools=[tool.to_structured_tool()],
|
||||
tools_handler=None,
|
||||
agent=None,
|
||||
task=None,
|
||||
crew=None,
|
||||
event_source=MagicMock(),
|
||||
printer=None,
|
||||
verbose=False,
|
||||
)
|
||||
finally:
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
assert json.loads(result.result) == {"query": "crew", "score": 0.9}
|
||||
assert seen_results == [
|
||||
('{"query":"crew","score":0.9}', SearchOutput(query="crew", score=0.9))
|
||||
]
|
||||
|
||||
def test_result_as_answer_false_on_tool_error(self) -> None:
|
||||
"""When a tool with result_as_answer=True raises, result_as_answer must be False.
|
||||
|
||||
|
||||
32
uv.lock
generated
32
uv.lock
generated
@@ -117,7 +117,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiobotocore"
|
||||
version = "3.4.0"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -129,9 +129,9 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/50/a48ed11b15f926ce3dbb33e7fb0f25af17dbb99bcb7ae3b30c763723eca7/aiobotocore-3.4.0.tar.gz", hash = "sha256:a918b5cb903f81feba7e26835aed4b5e6bb2d0149d7f42bb2dd7d8089e3d9000", size = 122360, upload-time = "2026-04-07T06:12:24.884Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/89/9533b377e9412013cc43a539d81bc5f8feeb4b6830643821ad612f78b09b/aiobotocore-3.5.0.tar.gz", hash = "sha256:d45d1c4659ad0e48b694a5aa4ff18829100386f7de96c8d146ec7757a6f12918", size = 123061, upload-time = "2026-04-21T07:25:26.993Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d8/ce9386e6d76ea79e61dee15e62aa48cff6be69e89246b0ac4a11857cb02c/aiobotocore-3.4.0-py3-none-any.whl", hash = "sha256:26290eb6830ea92d8a6f5f90b56e9f5cedd6d126074d5db63b195e281d982465", size = 88018, upload-time = "2026-04-07T06:12:22.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/05/6eeeadef45c24630af0ceae4d038b883e9a394786300529286ba8cc1e62d/aiobotocore-3.5.0-py3-none-any.whl", hash = "sha256:49ce35bb8b96b85d3251c2cbbb2ed7a028dc0cb0d0d0801f9ccca1ccd0d41ded", size = 88281, upload-time = "2026-04-21T07:25:25.258Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -657,7 +657,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "bedrock-agentcore"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "boto3" },
|
||||
@@ -669,9 +669,9 @@ dependencies = [
|
||||
{ name = "uvicorn" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/f6/2884c954343e794e3419348f5ffb0276a26f57b30af11f9fe63c4ca535c0/bedrock_agentcore-1.6.0.tar.gz", hash = "sha256:7ea176c3226dc6af8c399a8f9abb58629948cd8ed8675ece9f2f32b94e861b92", size = 512010, upload-time = "2026-03-31T23:10:06.561Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/65/69a66d812c5f86b902234fe91146004efcea907444a60f024f9afe13d150/bedrock_agentcore-1.7.0.tar.gz", hash = "sha256:cf632892f6bd055ce047eb91fe4d72f86569234faf3eb5cd1b2b614261a77d7f", size = 540824, upload-time = "2026-04-28T19:29:02.749Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/f8/bcf158979324f4f4d171588afffadb2154fa8499701290bfc7bdaf82bd3a/bedrock_agentcore-1.6.0-py3-none-any.whl", hash = "sha256:a4cd02f2bfb80fcc7a8c8835be8d55c778339f8286b071ac3aae579460dd2eb2", size = 164034, upload-time = "2026-03-31T23:10:04.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/9d/5f590afd5351e206d9a02f96777a69d1fc3edecfaa39bbba310248f21ea9/bedrock_agentcore-1.7.0-py3-none-any.whl", hash = "sha256:ee49695e613973baf01b4be400d3bc4b20ddedf3638765fb3bc6931a87fa0cd9", size = 178978, upload-time = "2026-04-28T19:29:00.944Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -685,16 +685,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.42.84"
|
||||
version = "1.42.91"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/89/2d647bd717da55a8cc68602b197f53a5fa36fb95a2f9e76c4aff11a9cfd1/boto3-1.42.84.tar.gz", hash = "sha256:6a84b3293a5d8b3adf827a54588e7dcffcf0a85410d7dadca615544f97d27579", size = 112816, upload-time = "2026-04-06T19:39:07.585Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/c0/98b8cec7ca22dde776df48c58940ae1abc425593959b7226e270760d726f/boto3-1.42.91.tar.gz", hash = "sha256:03d70532b17f7f84df37ca7e8c21553280454dea53ae12b15d1cfef9b16fcb8a", size = 113181, upload-time = "2026-04-17T19:31:06.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/31/cdf4326841613d1d181a77b3038a988800fb3373ca50de1639fba9fa87de/boto3-1.42.84-py3-none-any.whl", hash = "sha256:4d03ad3211832484037337292586f71f48707141288d9ac23049c04204f4ab03", size = 140555, upload-time = "2026-04-06T19:39:06.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/29/faba6521257c34085cc9b439ef98235b581772580f417fa3629728007270/boto3-1.42.91-py3-none-any.whl", hash = "sha256:04e72071cde022951ce7f81bd9933c90095ab8923e8ced61c8dacfe9edac0f5c", size = 140553, upload-time = "2026-04-17T19:31:02.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -718,16 +718,16 @@ bedrock-runtime = [
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.42.84"
|
||||
version = "1.42.91"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/b7/1c03423843fb0d1795b686511c00ee63fed1234c2400f469aeedfd42212f/botocore-1.42.84.tar.gz", hash = "sha256:234064604c80d9272a5e9f6b3566d260bcaa053a5e05246db90d7eca1c2cf44b", size = 15148615, upload-time = "2026-04-06T19:38:56.673Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/bc/a4b7c46471c2e789ad8c4c7acfd7f302fdb481d93ff870f441249b924ae6/botocore-1.42.91.tar.gz", hash = "sha256:d252e27bc454afdbf5ed3dc617aa423f2c855c081e98b7963093399483ecc698", size = 15213010, upload-time = "2026-04-17T19:30:50.793Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/37/0c0c90361c8a1b9e6c75222ca24ae12996a298c0e18822a72ab229c37207/botocore-1.42.84-py3-none-any.whl", hash = "sha256:15f3fe07dfa6545e46a60c4b049fe2bdf63803c595ae4a4eec90e8f8172764f3", size = 14827061, upload-time = "2026-04-06T19:38:53.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/fc/24cc0a47c824f13933e210e9ad034b4fba22f7185b8d904c0fbf5a3b2be8/botocore-1.42.91-py3-none-any.whl", hash = "sha256:7a28c3cc6bfab5724ad18899d52402b776a0de7d87fa20c3c5270bcaaf199ce8", size = 14897344, upload-time = "2026-04-17T19:30:44.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1413,7 +1413,7 @@ watson = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "a2a-sdk", marker = "extra == 'a2a'", specifier = "~=0.3.10" },
|
||||
{ name = "aiobotocore", marker = "extra == 'aws'", specifier = "~=3.4.0" },
|
||||
{ name = "aiobotocore", marker = "extra == 'aws'", specifier = "~=3.5.0" },
|
||||
{ name = "aiocache", extras = ["memcached", "redis"], marker = "extra == 'a2a'", specifier = "~=0.12.3" },
|
||||
{ name = "aiofiles", specifier = "~=24.1.0" },
|
||||
{ name = "aiosqlite", specifier = "~=0.21.0" },
|
||||
@@ -1421,8 +1421,8 @@ requires-dist = [
|
||||
{ name = "appdirs", specifier = "~=1.4.4" },
|
||||
{ name = "azure-ai-inference", marker = "extra == 'azure-ai-inference'", specifier = "~=1.0.0b9" },
|
||||
{ name = "azure-identity", marker = "extra == 'azure-ai-inference'", specifier = ">=1.17.0,<2" },
|
||||
{ name = "boto3", marker = "extra == 'aws'", specifier = "~=1.42.79" },
|
||||
{ name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.42.79" },
|
||||
{ name = "boto3", marker = "extra == 'aws'", specifier = "~=1.42.90" },
|
||||
{ name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.42.90" },
|
||||
{ name = "cel-python", specifier = ">=0.5.0,<0.6" },
|
||||
{ name = "chromadb", specifier = "~=1.1.0" },
|
||||
{ name = "click", specifier = ">=8.1.7,<9" },
|
||||
@@ -1734,7 +1734,7 @@ requires-dist = [
|
||||
{ name = "beautifulsoup4", specifier = "~=4.13.4" },
|
||||
{ name = "beautifulsoup4", marker = "extra == 'beautifulsoup4'", specifier = ">=4.12.3" },
|
||||
{ name = "beautifulsoup4", marker = "extra == 'bedrock'", specifier = ">=4.13.4" },
|
||||
{ name = "bedrock-agentcore", marker = "extra == 'bedrock'", specifier = ">=0.1.0" },
|
||||
{ name = "bedrock-agentcore", marker = "extra == 'bedrock'", specifier = ">=1.7.0,<1.8.0" },
|
||||
{ name = "browserbase", marker = "extra == 'browserbase'", specifier = ">=1.0.5" },
|
||||
{ name = "composio-core", marker = "extra == 'composio-core'", specifier = ">=0.6.11.post1" },
|
||||
{ name = "contextual-client", marker = "extra == 'contextual'", specifier = ">=0.1.0" },
|
||||
|
||||
Reference in New Issue
Block a user